├── .github
├── FUNDING.yml
└── workflows
│ ├── deploy.yml
│ ├── preview-cleanup.yml
│ └── preview.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── admin
├── Admin.tsx
├── client.tsx
├── index.html
├── rpc
│ └── admin.ts
└── state.ts
├── api
├── admin
│ └── actions.ts
├── auth
│ ├── actions.ts
│ └── types.ts
├── chat
│ ├── actions.ts
│ ├── bus.ts
│ ├── routes.ts
│ └── types.ts
├── core
│ ├── app.ts
│ ├── constants.ts
│ ├── create-bus.ts
│ ├── fetch-json.ts
│ ├── match.ts
│ ├── middleware.ts
│ ├── router.ts
│ ├── send-email.ts
│ ├── server.ts
│ └── sessions.ts
├── db.ts
├── deno.json
├── deno.lock
├── env.ts
├── import-map.production.json
├── models.ts
├── oauth
│ ├── actions.ts
│ └── routes
│ │ ├── common.ts
│ │ └── github.ts
├── profiles
│ ├── actions.ts
│ └── types.ts
├── rpc
│ └── routes.ts
├── sounds
│ └── actions.ts
├── test-watch.ts
├── test
│ ├── sanity.test.ts
│ └── temp.ts
└── ws
│ └── routes.ts
├── as
├── assembly
│ ├── alloc.ts
│ ├── ambient.d.ts
│ ├── common
│ │ └── env.ts
│ ├── dsp
│ │ ├── constants.ts
│ │ ├── core
│ │ │ ├── antialias-wavetable.ts
│ │ │ ├── clock.ts
│ │ │ ├── constants-internal.ts
│ │ │ ├── constants.ts
│ │ │ ├── engine.ts
│ │ │ ├── fft.ts
│ │ │ ├── wave.ts
│ │ │ └── wavetable.ts
│ │ ├── gen
│ │ │ ├── adsr.ts
│ │ │ ├── aosc.ts
│ │ │ ├── atan.ts
│ │ │ ├── bap.ts
│ │ │ ├── bbp.ts
│ │ │ ├── bhp.ts
│ │ │ ├── bhs.ts
│ │ │ ├── biquad.ts
│ │ │ ├── blp.ts
│ │ │ ├── bls.ts
│ │ │ ├── bno.ts
│ │ │ ├── bpk.ts
│ │ │ ├── clamp.ts
│ │ │ ├── clip.ts
│ │ │ ├── comp.ts
│ │ │ ├── daverb.ts
│ │ │ ├── dcc.ts
│ │ │ ├── dclip.ts
│ │ │ ├── dclipexp.ts
│ │ │ ├── dcliplin.ts
│ │ │ ├── delay.ts
│ │ │ ├── diode.ts
│ │ │ ├── exp.ts
│ │ │ ├── freesound.ts
│ │ │ ├── gen.ts
│ │ │ ├── gendy.ts
│ │ │ ├── grain.ts
│ │ │ ├── inc.ts
│ │ │ ├── lp.ts
│ │ │ ├── mhp.ts
│ │ │ ├── mlp.ts
│ │ │ ├── moog.ts
│ │ │ ├── noi.ts
│ │ │ ├── nrate.ts
│ │ │ ├── osc.ts
│ │ │ ├── ramp.ts
│ │ │ ├── rate.ts
│ │ │ ├── sap.ts
│ │ │ ├── saw.ts
│ │ │ ├── say.ts
│ │ │ ├── sbp.ts
│ │ │ ├── shp.ts
│ │ │ ├── sin.ts
│ │ │ ├── slp.ts
│ │ │ ├── smp.ts
│ │ │ ├── sno.ts
│ │ │ ├── spk.ts
│ │ │ ├── sqr.ts
│ │ │ ├── svf.ts
│ │ │ ├── tanh.ts
│ │ │ ├── tanha.ts
│ │ │ ├── tap.ts
│ │ │ ├── tri.ts
│ │ │ └── zero.ts
│ │ ├── graph
│ │ │ ├── copy.ts
│ │ │ ├── dc-bias-old.ts
│ │ │ ├── dc-bias.ts
│ │ │ ├── fade.ts
│ │ │ ├── fill.ts
│ │ │ ├── join.ts
│ │ │ ├── math.ts
│ │ │ └── rms.ts
│ │ ├── index.ts
│ │ ├── shared.ts
│ │ └── vm
│ │ │ ├── bin-op.ts
│ │ │ ├── dsp-shared.ts
│ │ │ ├── dsp.ts
│ │ │ ├── player.ts
│ │ │ └── sound.ts
│ ├── gfx
│ │ ├── draw.ts
│ │ ├── env.ts
│ │ ├── index.ts
│ │ ├── sketch-shared.ts
│ │ ├── sketch.ts
│ │ └── util.ts
│ ├── pkg
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ ├── math.ts
│ │ ├── player.ts
│ │ ├── rand.ts
│ │ └── shared.ts
│ ├── rms.ts
│ ├── types.ts
│ └── util.ts
└── tsconfig.json
├── asconfig-dsp-nort.json
├── asconfig-dsp.json
├── asconfig-gfx.json
├── asconfig-pkg-nort.json
├── asconfig-pkg.json
├── asconfig-rms.json
├── bunfig.toml
├── codecov.yml
├── docker-compose.yaml
├── generated
├── assembly
│ ├── dsp-factory.ts
│ ├── dsp-offsets.ts
│ ├── dsp-op.ts
│ ├── dsp-runner.ts
│ └── tsconfig.json
└── typescript
│ ├── dsp-gens.ts
│ └── dsp-vm.ts
├── index.html
├── kysely.config.ts
├── lib
├── caching-router.ts
├── cn.ts
├── happydom.ts
├── icon.ts
├── lorem.ts
├── rpc.ts
├── watcher.ts
└── ws.ts
├── migrations
├── 1727778500969_user-table.ts
├── 1728040211862_chat-table.ts
├── 1729663196543_profiles-table.ts
├── 1729687226871_sounds-table.ts
├── 1729758260210_favorites-table.ts
└── 1729771213713_sounds-remixOf-column.ts
├── package.json
├── postcss.config.js
├── public
├── apple-touch-icon-180x180.png
├── as-interop.js
├── favicon.ico
├── favicon.svg
├── maskable-icon-512x512.png
├── pwa-192x192.png
├── pwa-512x512.png
└── pwa-64x64.png
├── pwa-assets.config.ts
├── scripts
├── generate-dsp-vm.ts
├── update-dsp-factory.ts
├── update-gens-offsets.ts
└── util.ts
├── src
├── ambient.d.ts
├── as
│ ├── dsp
│ │ ├── build.ts
│ │ ├── constants.ts
│ │ ├── dsp.ts
│ │ ├── index.ts
│ │ ├── node.ts
│ │ ├── notes-shared.ts
│ │ ├── params-shared.ts
│ │ ├── pre-post.ts
│ │ ├── preview-service.ts
│ │ ├── preview-worker.ts
│ │ ├── rms.ts
│ │ ├── shared.ts
│ │ ├── util.ts
│ │ ├── value.ts
│ │ ├── wasm.ts
│ │ └── worklet.ts
│ ├── gfx
│ │ ├── anim.ts
│ │ ├── gfx.ts
│ │ ├── glsl.ts
│ │ ├── index.ts
│ │ ├── mesh-info.ts
│ │ ├── meshes.ts
│ │ ├── shapes.ts
│ │ ├── sketch-info.ts
│ │ ├── sketch.ts
│ │ ├── types.ts
│ │ ├── wasm-matrix.ts
│ │ └── wasm.ts
│ ├── init-wasm.ts
│ └── pkg
│ │ ├── player.ts
│ │ ├── service.ts
│ │ ├── shared.ts
│ │ ├── wasm.ts
│ │ ├── worker.ts
│ │ └── worklet.ts
├── client.tsx
├── comp
│ ├── AnimMode.tsx
│ ├── AuthModal.tsx
│ ├── DspEditor.tsx
│ ├── DspEditorUi.tsx
│ ├── Header.tsx
│ ├── Help.tsx
│ ├── HelpModal.tsx
│ ├── Login.tsx
│ ├── LoginOrRegister.tsx
│ ├── OAuthLogin.tsx
│ ├── Register.tsx
│ ├── ResetPassword.tsx
│ ├── Toast.tsx
│ └── VerifyEmail.tsx
├── constants.ts
├── env.ts
├── lang
│ ├── index.ts
│ ├── interpreter.test.ts
│ ├── interpreter.ts
│ ├── tokenize.test.ts
│ ├── tokenize.ts
│ └── util.ts
├── pages
│ ├── About.tsx
│ ├── App copy.tsx
│ ├── App.tsx
│ ├── AssemblyScript.tsx
│ ├── CanvasDemo.tsx
│ ├── Chat
│ │ ├── Channels.tsx
│ │ ├── Chat.tsx
│ │ ├── Messages.tsx
│ │ ├── Users.tsx
│ │ ├── VideoCall.tsx
│ │ └── util.ts
│ ├── CreateProfile.tsx
│ ├── CreateSound.tsx
│ ├── DspControls.tsx
│ ├── EditorDemo.tsx
│ ├── Home.tsx
│ ├── Logout.tsx
│ ├── OAuthRegister.tsx
│ ├── Profile.tsx
│ ├── QrCode.tsx
│ ├── Settings.tsx
│ ├── Showcase.tsx
│ ├── UiShowcase.tsx
│ ├── WebGLDemo.tsx
│ ├── WebSockets.tsx
│ └── WorkerWorklet
│ │ ├── WorkerWorkletDemo.tsx
│ │ ├── basic-processor.ts
│ │ ├── constants.ts
│ │ ├── free-queue.ts
│ │ └── worker.ts
├── pwa.ts
├── rpc
│ ├── auth.ts
│ ├── chat.ts
│ ├── oauth.ts
│ ├── profiles.ts
│ └── sounds.ts
├── screen.ts
├── service-worker
│ └── sw.ts
├── state.ts
├── test
│ └── e2e.test.tsx-fixme
├── theme.ts
├── ui
│ ├── Button.tsx
│ ├── Canvas.tsx
│ ├── DropDown.tsx
│ ├── Editor.tsx
│ ├── Fieldset.tsx
│ ├── Heading.tsx
│ ├── Input.tsx
│ ├── Label.tsx
│ ├── Layout.tsx
│ ├── Link.tsx
│ ├── editor
│ │ ├── buffer.test.ts
│ │ ├── buffer.ts
│ │ ├── caret.ts
│ │ ├── constants.ts
│ │ ├── dims.ts
│ │ ├── draw.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ ├── input.ts
│ │ ├── kbd.tsx
│ │ ├── misc.ts
│ │ ├── mouse.ts
│ │ ├── pane.ts
│ │ ├── selection.ts
│ │ ├── util
│ │ │ ├── begin-of-line.ts
│ │ │ ├── escape-regexp.ts
│ │ │ ├── find-matching-brackets.ts
│ │ │ ├── floats.ts
│ │ │ ├── geometry.ts
│ │ │ ├── index.ts
│ │ │ ├── oklch.ts
│ │ │ ├── parse-words.ts
│ │ │ ├── regexp.ts
│ │ │ ├── rgb.ts
│ │ │ ├── types.ts
│ │ │ └── waveform.ts
│ │ ├── view.tsx
│ │ ├── widget.ts
│ │ └── widgets
│ │ │ ├── error-sub.ts
│ │ │ ├── hover-mark.ts
│ │ │ ├── index.ts
│ │ │ ├── list-mark.ts
│ │ │ ├── rms-deco.ts
│ │ │ ├── wave-canvas-deco.ts
│ │ │ ├── wave-gl-deco.ts
│ │ │ └── wave-svg-deco.tsx
│ └── index.ts
└── util
│ ├── copy-ring-into.ts
│ ├── mod-wrap.ts
│ ├── parse-form.test.tsx
│ ├── parse-form.ts
│ └── stabilizer.ts
├── style.css
├── tailwind.config.js
├── tsconfig.json
├── vendor
├── as-transform-unroll.js
├── as-transform-update-dsp-gens.js
├── util.js
├── vite-plugin-assemblyscript.ts
├── vite-plugin-bundle-url.ts
├── vite-plugin-cors-coop-coep.ts
├── vite-plugin-hex-loader.ts
├── vite-plugin-open-in-editor.ts
├── vite-plugin-print-address.ts
└── vite-plugin-using.ts
└── vite.config.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: stagas # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/preview-cleanup.yml:
--------------------------------------------------------------------------------
1 | name: Cleanup
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - closed
7 |
8 | jobs:
9 | cleanup:
10 | name: PR
11 | runs-on: ubuntu-latest
12 | environment: Preview
13 |
14 | permissions:
15 | id-token: write # Needed for auth with Deno Deploy
16 | contents: read # Needed to clone the repository
17 |
18 | env:
19 | NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
20 | NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
21 |
22 | steps:
23 | - name: Get branch name
24 | id: branch_name
25 | uses: tj-actions/branch-names@v8
26 |
27 | - name: Delete Neon Branch
28 | uses: neondatabase/delete-branch-action@v3
29 | with:
30 | project_id: ${{ vars.NEON_PROJECT_ID }}
31 | branch: preview/pr-${{ github.event.number }}-${{ steps.branch_name.outputs.current_branch }}
32 | api_key: ${{ secrets.NEON_API_KEY }}
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env.development
3 | .env.production
4 | .vscode
5 | as/build
6 | coverage
7 | dist
8 | node_modules
9 | package-lock.json
10 | storage
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/admin/client.tsx:
--------------------------------------------------------------------------------
1 | import '~/lib/watcher.ts'
2 |
3 | import { cleanup, hmr, mount } from 'sigui'
4 | import { Admin } from '~/admin/Admin.tsx'
5 | import { setState, state } from '~/src/state.ts'
6 |
7 | export const start = mount('#container', target => {
8 | target.replaceChildren( as HTMLElement)
9 | return cleanup
10 | })
11 |
12 | if (import.meta.hot) {
13 | import.meta.hot.accept(hmr(start, state, setState))
14 | }
15 | else {
16 | start()
17 | }
18 |
--------------------------------------------------------------------------------
/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Vasi - Admin
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/admin/rpc/admin.ts:
--------------------------------------------------------------------------------
1 | import type * as actions from '~/api/admin/actions.ts'
2 | import { rpc } from '~/lib/rpc.ts'
3 |
4 | export const listUsers = rpc('GET', 'listUsers')
5 | export const deleteUser = rpc('POST', 'deleteUser')
6 | export const clearUsers = rpc('POST', 'clearUsers')
7 |
8 | export const listSessions = rpc('GET', 'listSessions')
9 | export const deleteSession = rpc('POST', 'deleteSession')
10 | export const clearSessions = rpc('POST', 'clearSessions')
11 |
--------------------------------------------------------------------------------
/admin/state.ts:
--------------------------------------------------------------------------------
1 | import { $ } from 'sigui'
2 | import type { UserSession } from '~/api/auth/types.ts'
3 |
4 | export let state = $({
5 | user: null as undefined | null | UserSession,
6 | url: new URL(location.href),
7 | })
8 |
9 | export function setState(newState: any) {
10 | state = newState
11 | }
12 |
--------------------------------------------------------------------------------
/api/auth/types.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export interface UiUser {
4 | nick: string
5 | }
6 |
7 | export const UserGet = z.object({
8 | nick: z.string()
9 | })
10 |
11 | export type UserRegister = z.infer
12 | export const UserRegister = z.union([
13 | z.object({
14 | nick: z.string(),
15 | email: z.string(),
16 | password: z.string().min(1), // TODO: .min(10)
17 | }),
18 | z.object({
19 | nick: z.string(),
20 | email: z.string(),
21 | emailVerified: z.boolean().optional(),
22 | }),
23 | ])
24 |
25 | export type UserLogin = z.infer
26 | export const UserLogin = z.object({
27 | nickOrEmail: z.string(),
28 | password: z.string(),
29 | })
30 |
31 | export type UserForgot = z.infer
32 | export const UserForgot = z.object({
33 | email: z.string(),
34 | })
35 |
36 | export type UserSession = z.infer
37 | export const UserSession = z.object({
38 | nick: z.string(),
39 | expires: z.date(),
40 | defaultProfile: z.string().optional(),
41 | isAdmin: z.boolean().optional(),
42 | })
43 |
44 | export type UserResetPassword = z.infer
45 | export const UserResetPassword = z.object({
46 | token: z.string(),
47 | password: z.string().min(1), // TODO: .min(10)
48 | })
49 |
--------------------------------------------------------------------------------
/api/chat/bus.ts:
--------------------------------------------------------------------------------
1 | import { subs } from "~/api/chat/routes.ts"
2 | import { ChatDirectMessage, ChatDirectMessageType, chatDirectMessageTypes, ChatMessage } from '~/api/chat/types.ts'
3 | import { createBus } from '~/api/core/create-bus.ts'
4 |
5 | export const bus = createBus(['chat', 'bus'])
6 |
7 | bus.onmessage = ({ data }: { data: ChatMessage | ChatDirectMessage }) => {
8 | if (chatDirectMessageTypes.includes(data.type)) {
9 | const target = subs.get(data.nick)
10 | if (!target) return
11 |
12 | const msg: ChatDirectMessage = {
13 | type: data.type as ChatDirectMessageType,
14 | nick: data.from as string,
15 | text: data.text
16 | }
17 |
18 | target.send(msg)
19 | }
20 | else {
21 | for (const stream of subs.values()) stream.send(data)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api/chat/routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from '~/api/core/router.ts'
2 | import { getSession } from '~/api/core/sessions.ts'
3 |
4 | export const subs = new Map()
5 |
6 | export function broadcast(data: unknown) {
7 | for (const stream of subs.values()) {
8 | stream.send(data)
9 | }
10 | }
11 |
12 | class ChatStream {
13 | constructor(public controller: ReadableStreamDefaultController) { }
14 |
15 | enqueue(data: string) {
16 | this.controller.enqueue(`data: ${data}\n\n`)
17 | }
18 |
19 | send(data: unknown) {
20 | this.enqueue(JSON.stringify(data))
21 | }
22 | }
23 |
24 | export function mount(app: Router) {
25 | app.get('/chat/events', [ctx => {
26 | const { nick } = getSession(ctx)
27 |
28 | const stream = new ReadableStream({
29 | start(controller) {
30 | const stream = new ChatStream(controller)
31 | subs.set(nick, stream)
32 | stream.send({ type: 'started', nick })
33 | },
34 | cancel() {
35 | subs.delete(nick)
36 | },
37 | })
38 |
39 | return new Response(stream.pipeThrough(new TextEncoderStream()), {
40 | headers: {
41 | 'cache-control': 'no-cache',
42 | 'content-type': 'text/event-stream',
43 | 'connection': 'keep-alive',
44 | }
45 | })
46 | }])
47 | }
48 |
--------------------------------------------------------------------------------
/api/chat/types.ts:
--------------------------------------------------------------------------------
1 | import { UiUser } from '~/api/auth/types.ts'
2 |
3 | export interface ChatChannel {
4 | name: string
5 | bus: BroadcastChannel
6 | nicks: Set
7 | }
8 |
9 | export interface UiChannel {
10 | name: string
11 | users: UiUser[]
12 | messages: ChatMessage[]
13 | }
14 |
15 | export type ChatMessageType =
16 | | 'started'
17 | | 'createChannel'
18 | | 'message'
19 | | 'join'
20 | | 'part'
21 |
22 | export interface ChatMessage {
23 | type: ChatMessageType
24 | from?: string
25 | channel?: string
26 | nick: string
27 | text: string
28 | }
29 |
30 | export type ChatDirectMessageType =
31 | | 'directMessage'
32 | | 'webrtc:offer'
33 | | 'webrtc:answer'
34 | | 'webrtc:end'
35 |
36 | export const chatDirectMessageTypes = [
37 | 'directMessage',
38 | 'webrtc:offer',
39 | 'webrtc:answer',
40 | 'webrtc:end',
41 | ]
42 |
43 | export interface ChatDirectMessage {
44 | type: ChatDirectMessageType
45 | from?: string
46 | nick: string
47 | text: string
48 | }
49 |
--------------------------------------------------------------------------------
/api/core/app.ts:
--------------------------------------------------------------------------------
1 | import { timeMs } from 'utils'
2 | import { Router } from "~/api/core/router.ts"
3 |
4 | function log(...args: unknown[]) {
5 | const now = new Date()
6 | return console.log(`\x1b[02m${timeMs(now)}\x1b[0m`, ...args)
7 | }
8 |
9 | export const kv = await Deno.openKv()
10 | export const app = Router({ log })
11 |
--------------------------------------------------------------------------------
/api/core/constants.ts:
--------------------------------------------------------------------------------
1 | export const IS_DEV = !Deno.env.get('DENO_REGION')
2 | export const SALT = 'thisisasalt'
3 | export const EMAIL_FROM = 'Ravescript '
4 |
--------------------------------------------------------------------------------
/api/core/create-bus.ts:
--------------------------------------------------------------------------------
1 | import 'https://deno.land/x/websocket_broadcastchannel@0.8.0/polyfill.ts'
2 |
3 | export function createBus(keys: readonly string[]) {
4 | const bc = new BroadcastChannel(keys.join(':'))
5 |
6 | const api = {
7 | postMessage(data: unknown) {
8 | bc.postMessage(JSON.stringify(data))
9 | },
10 | set onmessage(fn: (ev: MessageEvent) => void) {
11 | bc.onmessage = ev => {
12 | fn({
13 | data: JSON.parse(ev.data),
14 | origin: ev.origin,
15 | lastEventId: ev.lastEventId,
16 | source: ev.source,
17 | } as MessageEvent)
18 | }
19 | },
20 | set onmessageerror(fn: (error: MessageEvent) => void) {
21 | bc.onmessageerror = fn
22 | }
23 | } as BroadcastChannel
24 |
25 | return api
26 | }
27 |
--------------------------------------------------------------------------------
/api/core/fetch-json.ts:
--------------------------------------------------------------------------------
1 | export async function fetchJson(url: string, init: RequestInit) {
2 | const res = await fetch(url, init)
3 | if (!res.ok) {
4 | throw new Error(`Fetch error ${res.status}`)
5 | }
6 | return res.json()
7 | }
8 |
--------------------------------------------------------------------------------
/api/core/send-email.ts:
--------------------------------------------------------------------------------
1 | import { EMAIL_FROM } from "~/api/core/constants.ts"
2 | import { env } from '~/api/env.ts'
3 |
4 | const MAIL_ENABLED = false
5 |
6 | type SendEmailResult = {
7 | ok: true,
8 | data: {
9 | id: string
10 | }
11 | } | {
12 | ok: false,
13 | error: {
14 | statusCode: number,
15 | message: string
16 | }
17 | }
18 |
19 | export async function sendEmail({
20 | to,
21 | subject,
22 | html,
23 | text
24 | }: {
25 | to: string[],
26 | subject: string,
27 | html: string,
28 | text: string
29 | }): Promise {
30 | if (!MAIL_ENABLED) {
31 | console.log('Email skipped:')
32 | console.log({ to, subject })
33 | console.log('\nhtml:\n' + html)
34 | console.log('\ntext:\n' + text)
35 | return { ok: true, data: { id: 'mock' } }
36 | }
37 | const res = await fetch('https://api.resend.com/emails', {
38 | method: 'POST',
39 | headers: {
40 | 'Content-Type': 'application/json',
41 | Authorization: `Bearer ${env.RESEND_API_KEY}`,
42 | },
43 | body: JSON.stringify({
44 | from: EMAIL_FROM,
45 | to,
46 | subject,
47 | html,
48 | text,
49 | }),
50 | })
51 |
52 | if (res.ok) {
53 | const data: { id: string } = await res.json()
54 | return { ok: true, data }
55 | }
56 | else {
57 | const error: { statusCode: number, message: string } = await res.json()
58 | return { ok: false, error }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/api/core/server.ts:
--------------------------------------------------------------------------------
1 | import os from 'https://deno.land/x/os_paths@v7.4.0/src/mod.deno.ts'
2 | import { parseArgs } from 'jsr:@std/cli/parse-args'
3 | import * as path from 'jsr:@std/path'
4 | import * as chat from '~/api/chat/routes.ts'
5 | import { app } from '~/api/core/app.ts'
6 | import { IS_DEV } from "~/api/core/constants.ts"
7 | import { cors, files, logger, session, watcher } from "~/api/core/middleware.ts"
8 | import * as oauthCommon from '~/api/oauth/routes/common.ts'
9 | import * as oauthGitHub from '~/api/oauth/routes/github.ts'
10 | import * as rpc from '~/api/rpc/routes.ts'
11 | import * as ws from '~/api/ws/routes.ts'
12 |
13 | import '~/api/chat/actions.ts'
14 | import '~/api/oauth/actions.ts'
15 | import '~/api/profiles/actions.ts'
16 | import '~/api/sounds/actions.ts'
17 |
18 | const dist = 'dist'
19 | const home = os.home() ?? '~'
20 |
21 | const args = parseArgs(Deno.args, {
22 | string: ['port'],
23 | })
24 |
25 | const options: Record = IS_DEV
26 | ? {
27 | cert: Deno.readTextFileSync(path.join(home, '.ssl-certs', 'devito.test.pem')),
28 | key: Deno.readTextFileSync(path.join(home, '.ssl-certs', 'devito.test-key.pem')),
29 | }
30 | : {}
31 |
32 | options.port = args.port ?? '8000'
33 |
34 | Deno.serve(options, app.handler)
35 |
36 | app.use(null, [logger])
37 | app.use(null, [cors])
38 | app.use(null, [session])
39 |
40 | chat.mount(app)
41 | oauthCommon.mount(app)
42 | oauthGitHub.mount(app)
43 | rpc.mount(app)
44 | ws.mount(app)
45 |
46 | IS_DEV && app.log('Listening: https://devito.test:' + options.port)
47 | IS_DEV && app.get('/watcher', [watcher])
48 | app.use(null, [files(dist)])
49 |
--------------------------------------------------------------------------------
/api/core/sessions.ts:
--------------------------------------------------------------------------------
1 | import type { UserSession } from '~/api/auth/types.ts'
2 | import { RouteError, type Context } from "~/api/core/router.ts"
3 |
4 | export const sessions = new WeakMap()
5 |
6 | export function getSession(ctx: Context) {
7 | const session = sessions.get(ctx)
8 | if (!session) throw new RouteError(401, 'Session not found')
9 | return session
10 | }
11 |
--------------------------------------------------------------------------------
/api/db.ts:
--------------------------------------------------------------------------------
1 | import { CamelCasePlugin, Kysely, PostgresDialect } from 'kysely'
2 | import Pool from 'pg-pool'
3 | import { env } from '~/api/env.ts'
4 | import { DB } from '~/api/models.ts'
5 |
6 | const dialect = new PostgresDialect({
7 | pool: new Pool({
8 | connectionString: env.DATABASE_URL,
9 | max: 10,
10 | })
11 | })
12 |
13 | export const db = new Kysely({
14 | dialect,
15 | plugins: [new CamelCasePlugin()],
16 | })
17 |
--------------------------------------------------------------------------------
/api/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "fmt": {
3 | "singleQuote": true,
4 | "semiColons": false
5 | },
6 | "imports": {
7 | "~/api/": "./",
8 | "kysely": "npm:kysely@^0.27.4",
9 | "pg": "npm:pg",
10 | "pg-pool": "npm:pg-pool",
11 | "utils": "../node_modules/utils/src/index.ts",
12 | "zod": "https://deno.land/x/zod@v3.23.8/mod.ts"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/api/env.ts:
--------------------------------------------------------------------------------
1 | import { load } from 'https://deno.land/std@0.224.0/dotenv/mod.ts'
2 | import { z } from 'zod'
3 | import { IS_DEV } from '~/api/core/constants.ts'
4 |
5 | const Env = z.object({
6 | DENO_DEPLOYMENT_ID: z.string().optional(),
7 |
8 | VITE_API_URL: z.string(),
9 | WEB_URL: z.string(),
10 |
11 | DATABASE_URL: z.string(),
12 |
13 | RESEND_API_KEY: z.string(),
14 |
15 | OAUTH_GITHUB_CLIENT_ID: z.string(),
16 | OAUTH_GITHUB_CLIENT_SECRET: z.string(),
17 | })
18 |
19 | export const env = Env.parse(Object.assign({
20 | DENO_DEPLOYMENT_ID: Deno.env.get('DENO_DEPLOYMENT_ID'),
21 |
22 | VITE_API_URL: Deno.env.get('VITE_API_URL')!,
23 | WEB_URL: Deno.env.get('WEB_URL')!,
24 |
25 | DATABASE_URL: Deno.env.get('DATABASE_URL')!,
26 |
27 | RESEND_API_KEY: Deno.env.get('RESEND_API_KEY')!,
28 |
29 | OAUTH_GITHUB_CLIENT_ID: Deno.env.get('OAUTH_GITHUB_CLIENT_ID')!,
30 | OAUTH_GITHUB_CLIENT_SECRET: Deno.env.get('OAUTH_GITHUB_CLIENT_SECRET')!,
31 |
32 | } satisfies z.infer, await load({
33 | envPath: IS_DEV
34 | ? '.env.development'
35 | : '.env.production'
36 | })))
37 |
--------------------------------------------------------------------------------
/api/import-map.production.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "~/api/": "./",
4 | "kysely": "npm:kysely@^0.27.4",
5 | "pg": "npm:pg",
6 | "pg-pool": "npm:pg-pool",
7 | "utils": "https://raw.githubusercontent.com/stagas/utils/refs/heads/main/src/index.ts",
8 | "zod": "https://deno.land/x/zod@v3.23.8/mod.ts"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/api/oauth/actions.ts:
--------------------------------------------------------------------------------
1 | import * as authActions from '~/api/auth/actions.ts'
2 | import { kv } from '~/api/core/app.ts'
3 | import { Context, RouteError } from '~/api/core/router.ts'
4 | import { OAuthSession } from "~/api/oauth/routes/github.ts"
5 | import { actions } from '~/api/rpc/routes.ts'
6 |
7 | function pascalCase(s: string) {
8 | return s.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase())
9 | }
10 |
11 | actions.get.getLoginSession = getLoginSession
12 | export async function getLoginSession(_ctx: Context, id: string) {
13 | const entry = await kv.get(['oauth', id])
14 | if (!entry.value) throw new RouteError(404, 'OAuth session not found')
15 | const session = OAuthSession.parse(entry.value)
16 | return { login: session.login }
17 | }
18 |
19 | actions.post.registerOAuth = registerOAuth
20 | export async function registerOAuth(ctx: Context, id: string, nick: string) {
21 | const entry = await kv.get(['oauth', id])
22 | if (!entry.value) throw new RouteError(404, 'OAuth session not found')
23 | const session = OAuthSession.parse(entry.value)
24 | const oauthField = `oauth${pascalCase(session.state.provider)}` as 'oauthGithub'
25 | return await authActions.register(ctx, {
26 | nick,
27 | email: session.email,
28 | // @ts-ignore ts has issues with dynamic keys
29 | [oauthField]: true
30 | }, oauthField)
31 | }
32 |
--------------------------------------------------------------------------------
/api/oauth/routes/common.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { kv } from '~/api/core/app.ts'
3 | import { Router } from '~/api/core/router.ts'
4 | import { env } from '~/api/env.ts'
5 |
6 | const OAuthStart = z.object({
7 | provider: z.enum(['github']),
8 | })
9 |
10 | export const OAuthState = z.object({
11 | origin: z.string(),
12 | provider: z.string(),
13 | })
14 |
15 | export function mount(app: Router) {
16 | const {
17 | OAUTH_GITHUB_CLIENT_ID: client_id,
18 | } = env
19 |
20 | app.get('/oauth/start', [async ctx => {
21 | const { provider } = OAuthStart.parse(
22 | Object.fromEntries(ctx.url.searchParams.entries())
23 | )
24 |
25 | ctx.log('OAuth origin:', ctx.origin)
26 |
27 | const state = OAuthState.parse({
28 | origin: ctx.origin,
29 | provider,
30 | })
31 |
32 | const oauthStateId = crypto.randomUUID()
33 | await kv.set(['oauthState', oauthStateId], state, {
34 | expireIn: 30 * 60 * 1000
35 | })
36 |
37 | switch (provider) {
38 | case 'github': {
39 | const url = new URL('https://github.com/login/oauth/authorize')
40 | url.searchParams.set('client_id', client_id)
41 | url.searchParams.set('state', oauthStateId)
42 | return ctx.redirect(302, url.href)
43 | }
44 | }
45 | }])
46 | }
47 |
--------------------------------------------------------------------------------
/api/profiles/types.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export type ProfileCreate = z.infer
4 | export const ProfileCreate = z.object({
5 | nick: z.string(),
6 | displayName: z.string(),
7 | isDefault: z.coerce.boolean(),
8 | })
9 |
--------------------------------------------------------------------------------
/api/test-watch.ts:
--------------------------------------------------------------------------------
1 | import { debounce } from 'utils'
2 |
3 | const watcher = Deno.watchFs('./api')
4 |
5 | const run = debounce(100, async () => {
6 | try {
7 | Deno.removeSync('./coverage/deno', { recursive: true })
8 | }
9 | catch {
10 | //
11 | }
12 |
13 | {
14 | const command = new Deno.Command(Deno.execPath(), {
15 | args: [
16 | 'test',
17 | '~/api',
18 | '--no-lock',
19 | '--coverage=coverage/deno',
20 | ],
21 | })
22 | await command.output()
23 | }
24 |
25 | {
26 | const command = new Deno.Command(Deno.execPath(), {
27 | args: [
28 | 'coverage',
29 | 'coverage/deno',
30 | ],
31 | })
32 | const { stdout } = await command.output()
33 | console.log(new TextDecoder().decode(stdout))
34 | }
35 |
36 | {
37 | const command = new Deno.Command(Deno.execPath(), {
38 | args: [
39 | 'coverage',
40 | 'coverage/deno',
41 | '--lcov',
42 | '--output=coverage/lcov-deno.info'
43 | ],
44 | })
45 | await command.output()
46 | }
47 |
48 | console.log('Coverage report generated')
49 | })
50 |
51 | run()
52 | for await (const _ of watcher) {
53 | run()
54 | }
55 |
--------------------------------------------------------------------------------
/api/test/sanity.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'jsr:@std/testing/bdd'
2 | import { expect } from 'jsr:@std/expect'
3 | import { add } from './temp.ts'
4 |
5 | describe('sanity test', () => {
6 | it('should work', () => {
7 | expect(add(1, 1)).toBe(2)
8 | expect(add(2, 1)).toBe(0)
9 | expect(add(3, 1)).toBe(0)
10 | expect(add(4, 1)).toBe(0)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/api/test/temp.ts:
--------------------------------------------------------------------------------
1 | export function add(a: number, b: number) {
2 | if (a === 2) return 0
3 | if (a === 3) return 0
4 | if (a === 4) return 0
5 | return a + b
6 | }
7 |
--------------------------------------------------------------------------------
/api/ws/routes.ts:
--------------------------------------------------------------------------------
1 | import { createBus } from '~/api/core/create-bus.ts'
2 | import type { Router } from '~/api/core/router.ts'
3 | import { getSession } from '~/api/core/sessions.ts'
4 |
5 | const clients = new Set()
6 |
7 | const bus = createBus(['ws'])
8 | bus.onmessage = event => sendToLocalClients(event.data)
9 |
10 | function broadcast(this: WebSocket, { data }: { data: string }) {
11 | bus.postMessage(data)
12 | sendToLocalClients(data, this)
13 | }
14 |
15 | function sendToLocalClients(data: string, socket?: WebSocket) {
16 | clients.forEach(client => {
17 | if (client !== socket) client.send(data)
18 | })
19 | }
20 |
21 | export function mount(app: Router) {
22 | app.get('/ws', [ctx => {
23 | if (ctx.request.headers.get('upgrade') !== 'websocket') return
24 |
25 | const { nick } = getSession(ctx)
26 |
27 | ctx.log('[ws] connecting...', nick)
28 |
29 | const { response, socket: ws } = Deno.upgradeWebSocket(ctx.request, {
30 | idleTimeout: 0
31 | })
32 |
33 | ws.onmessage = broadcast
34 |
35 | ws.onopen = () => {
36 | ctx.log('[ws] open:', nick)
37 | clients.add(ws)
38 | }
39 |
40 | ws.onclose = () => {
41 | ctx.log('[ws] close:', nick)
42 | clients.delete(ws)
43 | }
44 |
45 | ws.onerror = err => {
46 | ctx.log('[ws] error:', nick, err)
47 | clients.delete(ws)
48 | }
49 |
50 | return response
51 | }])
52 | }
53 |
--------------------------------------------------------------------------------
/as/assembly/alloc.ts:
--------------------------------------------------------------------------------
1 | export function heap_alloc(size: usize): usize {
2 | return heap.alloc(size)
3 | }
4 |
5 | export function heap_free(ptr: usize): void {
6 | heap.free(ptr)
7 | }
8 |
9 | export function allocI32(length: i32): usize {
10 | return changetype(new StaticArray(length))
11 | }
12 |
13 | export function allocU32(length: i32): usize {
14 | return changetype(new StaticArray(length))
15 | }
16 |
17 | export function allocF32(length: i32): usize {
18 | return changetype(new StaticArray(length))
19 | }
20 |
--------------------------------------------------------------------------------
/as/assembly/ambient.d.ts:
--------------------------------------------------------------------------------
1 | declare function unroll(times: number, fn: () => void): void
2 |
--------------------------------------------------------------------------------
/as/assembly/common/env.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | @external('env', 'log')
3 | export declare function log(x: i32): void
4 |
--------------------------------------------------------------------------------
/as/assembly/dsp/constants.ts:
--------------------------------------------------------------------------------
1 | export const BUFFER_SIZE = 2048
2 | export const MAX_AUDIOS = 1024
3 | export const MAX_FLOATS = 4096
4 | export const MAX_LISTS = 4096
5 | export const MAX_LITERALS = 4096
6 | export const MAX_OPS = 4096
7 | export const MAX_RMSS = 1024
8 | export const MAX_SCALARS = 4096
9 | export const MAX_SOUNDS = 16
10 | export const MAX_TRACKS = 16
11 | export const MAX_VALUES = 1024
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/core/clock.ts:
--------------------------------------------------------------------------------
1 | @unmanaged
2 | export class Clock {
3 | time: f64 = 0
4 | timeStep: f64 = 0
5 | prevTime: f64 = -1
6 | startTime: f64 = 0
7 | endTime: f64 = Infinity
8 | bpm: f64 = 60
9 | coeff: f64 = 0
10 | barTime: f64 = 0
11 | barTimeStep: f64 = 0
12 | loopStart: f64 = -Infinity
13 | loopEnd: f64 = +Infinity
14 | sampleRate: u32 = 44100
15 | jumpBar: i32 = -1
16 | ringPos: u32 = 0
17 | nextRingPos: u32 = 0
18 |
19 | reset(): void {
20 | const c: Clock = this
21 | c.ringPos = 0
22 | c.nextRingPos = 0
23 | c.prevTime = -1
24 | c.time = 0
25 | c.barTime = c.startTime
26 | }
27 | update(): void {
28 | const c: Clock = this
29 |
30 | c.coeff = c.bpm / 60 / 4
31 | c.timeStep = 1.0 / c.sampleRate
32 | c.barTimeStep = c.timeStep * c.coeff
33 |
34 | let bt: f64
35 |
36 | // advance barTime
37 | bt = c.barTime + (
38 | c.prevTime >= 0
39 | ? (c.time - c.prevTime) * c.coeff
40 | : 0
41 | )
42 | c.prevTime = c.time
43 |
44 | // wrap barTime on clock.endTime
45 | const startTime = Math.max(c.loopStart, c.startTime)
46 | const endTime = Math.min(c.loopEnd, c.endTime)
47 |
48 | if (bt >= endTime) {
49 | bt = startTime + (bt % 1.0)
50 | }
51 |
52 | c.barTime = bt
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/as/assembly/dsp/core/constants-internal.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | @inline
3 | export const k2PI: f32 = 6.28318530718
4 |
--------------------------------------------------------------------------------
/as/assembly/dsp/core/constants.ts:
--------------------------------------------------------------------------------
1 | export const WAVETABLE_SIZE: u32 = 1 << 13
2 | export const DELAY_MAX_SIZE: u32 = 1 << 16
3 | export const SAMPLE_MAX_SIZE: u32 = 1 << 16
4 | export const ANTIALIAS_WAVETABLE_OVERSAMPLING: u32 = 1
5 |
--------------------------------------------------------------------------------
/as/assembly/dsp/core/engine.ts:
--------------------------------------------------------------------------------
1 | import { rateToPhaseStep } from '../../util'
2 | import { Clock } from './clock'
3 | import { WAVETABLE_SIZE } from './constants'
4 | import { Wavetable } from './wavetable'
5 |
6 | export class Core {
7 | wavetable: Wavetable
8 | constructor(public sampleRate: u32) {
9 | this.wavetable = new Wavetable(sampleRate, WAVETABLE_SIZE)
10 | }
11 | }
12 |
13 | export class Engine {
14 | wavetable: Wavetable
15 | clock: Clock
16 |
17 | rateSamples: u32
18 | rateSamplesRecip: f64
19 | rateStep: u32
20 | samplesPerMs: f64
21 |
22 | constructor(public sampleRate: u32, public core: Core) {
23 | const clock = new Clock()
24 |
25 | this.wavetable = core.wavetable
26 | this.clock = clock
27 | this.clock.sampleRate = sampleRate
28 | clock.update()
29 | clock.reset()
30 |
31 | this.rateSamples = sampleRate
32 | this.rateSamplesRecip = (1.0 / f64(sampleRate))
33 | this.rateStep = rateToPhaseStep(sampleRate)
34 | this.samplesPerMs = f64(sampleRate) / 1000
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/core/wave.ts:
--------------------------------------------------------------------------------
1 | import { WAVETABLE_SIZE } from './constants'
2 | import { rnd } from '../../util'
3 |
4 | // @ts-ignore
5 | @inline const HALF_PI: f64 = Math.PI / 2.0
6 |
7 | export class Wave {
8 | @inline static sine(phase: f64): f64 {
9 | return Math.sin(phase)
10 | }
11 |
12 | @inline static saw(phase: f64): f64 {
13 | return 1.0 - (((phase + Math.PI) / Math.PI) % 2.0)
14 | }
15 |
16 | @inline static ramp(phase: f64): f64 {
17 | return (((phase + Math.PI) / Math.PI) % 2.0) - 1.0
18 | }
19 |
20 | @inline static tri(phase: f64): f64 {
21 | return 1.0 - Math.abs(1.0 - (((phase + HALF_PI) / Math.PI) % 2.0)) * 2.0
22 | }
23 |
24 | @inline static sqr(phase: f64): f64 {
25 | return Wave.ramp(phase) < 0 ? -1 : 1
26 | }
27 |
28 | @inline static noise(phase: f64): f64 {
29 | return rnd() * 2.0 - 1.0
30 | }
31 | }
32 |
33 | const numHarmonics: u32 = 16
34 | export class Blit {
35 | @inline static saw(i: u32): f64 {
36 | let value: f64 = 0.0
37 | for (let h: u32 = 1; h <= numHarmonics; h++) {
38 | const harmonicPhase: f64 = f64(i * h) / f64(WAVETABLE_SIZE)
39 | const harmonicValue: f64 = Math.sin(harmonicPhase) / f64(h);
40 | value += harmonicValue;
41 | }
42 | return value
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/aosc.ts:
--------------------------------------------------------------------------------
1 | import { Osc } from './osc'
2 |
3 | export class Aosc extends Osc {
4 | get _tables(): StaticArray> {
5 | return this._engine.wavetable.antialias.saw
6 | }
7 |
8 | get _table(): StaticArray {
9 | return this._tables[this._tableIndex]
10 | }
11 |
12 | get _mask(): u32 {
13 | return this._engine.wavetable.antialias.tableMask
14 | }
15 |
16 | _tableIndex: u32 = 0
17 |
18 | _update(): void {
19 | super._update()
20 | this._tableIndex = this._engine.wavetable.antialias.getTableIndex(this.hz)
21 | const stepShift: i32 = this._engine.wavetable.antialias.stepShift
22 | this._step = stepShift < 0
23 | ? this._step << (-stepShift)
24 | : this._step >> stepShift
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/atan.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Atan extends Gen {
4 | _name: string = 'Atan';
5 |
6 | in: u32 = 0
7 |
8 | _audio(begin: u32, end: u32, out: usize): void {
9 | const length: u32 = end - begin
10 |
11 | let sample: f32 = 0
12 | let inp: u32 = this.in
13 |
14 | let i: u32 = begin
15 | end = i + length
16 |
17 | const offset = begin << 2
18 | inp += offset
19 | out += offset
20 |
21 | const gain: f32 = this.gain
22 |
23 | for (; i < end; i += 16) {
24 | unroll(16, () => {
25 | sample = f32.load(inp)
26 |
27 | sample = Mathf.atan(sample * gain)
28 |
29 | f32.store(out, sample)
30 | inp += 4
31 | out += 4
32 | })
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bap.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bap extends Biquad {
4 | _name: string = 'Bap'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._allpass(this.cut, this.q)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bbp.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bbp extends Biquad {
4 | _name: string = 'Bbp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._bandpass(this.cut, this.q)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bhp.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bhp extends Biquad {
4 | _name: string = 'Bhp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._highpass(this.cut, this.q)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bhs.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bhs extends Biquad {
4 | _name: string = 'Bhs'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 | amt: f32 = 1
8 |
9 | _update(): void {
10 | this._highshelf(this.cut, this.q, this.amt)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/blp.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Blp extends Biquad {
4 | _name: string = 'Blp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._lowpass(this.cut, this.q)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bls.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bls extends Biquad {
4 | _name: string = 'Bls'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 | amt: f32 = 1
8 |
9 | _update(): void {
10 | this._lowshelf(this.cut, this.q, this.amt)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bno.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bno extends Biquad {
4 | _name: string = 'Bno'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._notch(this.cut, this.q)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/bpk.ts:
--------------------------------------------------------------------------------
1 | import { Biquad } from './biquad'
2 |
3 | export class Bpk extends Biquad {
4 | _name: string = 'Bpk'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 | amt: f32 = 1
8 |
9 | _update(): void {
10 | this._peak(this.cut, this.q, this.amt)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/clamp.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Clamp extends Gen {
4 | _name: string = 'Clamp'
5 | min: f32 = -0.5;
6 | max: f32 = 0.5;
7 | in: u32 = 0
8 |
9 | _audio(begin: u32, end: u32, out: usize): void {
10 | const length: u32 = end - begin
11 | const min: f32 = this.min
12 | const max: f32 = this.max
13 |
14 | let sample: f32 = 0
15 | let inp: u32 = this.in
16 |
17 | let i: u32 = begin
18 | end = i + length
19 |
20 | const offset = begin << 2
21 | inp += offset
22 | out += offset
23 |
24 | for (; i < end; i += 16) {
25 | unroll(16, () => {
26 | sample = f32.load(inp)
27 |
28 | if (sample > max) {
29 | sample = max
30 | }
31 | else if (sample < min) {
32 | sample = min
33 | }
34 |
35 | f32.store(out, sample)
36 | inp += 4
37 | out += 4
38 | })
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/clip.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Clip extends Gen {
4 | _name: string = 'Clip'
5 | threshold: f32 = 1.0;
6 | in: u32 = 0
7 |
8 | _audio(begin: u32, end: u32, out: usize): void {
9 | const length: u32 = end - begin
10 | const threshold: f32 = this.threshold
11 |
12 | let sample: f32 = 0
13 | let inp: u32 = this.in
14 |
15 | let i: u32 = begin
16 | end = i + length
17 |
18 | const offset = begin << 2
19 | inp += offset
20 | out += offset
21 |
22 | for (; i < end; i += 16) {
23 | unroll(16, () => {
24 | sample = f32.load(inp)
25 |
26 | if (sample > threshold) {
27 | sample = threshold
28 | }
29 | else if (sample < -threshold) {
30 | sample = -threshold
31 | }
32 |
33 | f32.store(out, sample)
34 | inp += 4
35 | out += 4
36 | })
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/dcc.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Dcc extends Gen {
4 | _name: string = 'Dcc'
5 | ceil: f32 = 0.2;
6 | in: u32 = 0
7 |
8 | sample: f32 = 0
9 |
10 | _audio(begin: u32, end: u32, out: usize): void {
11 | const length: u32 = end - begin
12 | const ceil: f32 = this.ceil
13 | let prev: f32 = this.sample
14 | let sample: f32 = 0
15 | let next: f32 = 0
16 | let diff: f32 = 0
17 | let abs: f32 = 0
18 | let inp: u32 = this.in
19 |
20 | let i: u32 = begin
21 | end = i + length
22 |
23 | const offset = begin << 2
24 | inp += offset
25 | out += offset
26 |
27 | for (; i < end; i += 16) {
28 | unroll(16, () => {
29 | sample = f32.load(inp)
30 | diff = sample - prev
31 | abs = Mathf.abs(diff)
32 | if (abs > ceil) {
33 | next = prev + diff * (abs - ceil)
34 | }
35 | else {
36 | next = sample
37 | }
38 | prev = next
39 | f32.store(out, next)
40 | inp += 4
41 | out += 4
42 | })
43 | }
44 |
45 | this.sample = prev
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/dclip.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Dclip extends Gen {
4 | _name: string = 'Dclip';
5 | in: u32 = 0
6 |
7 | _audio(begin: u32, end: u32, out: usize): void {
8 | const length: u32 = end - begin
9 |
10 | let sample: f32 = 0
11 | let inp: u32 = this.in
12 |
13 | let i: u32 = begin
14 | end = i + length
15 |
16 | const offset = begin << 2
17 | inp += offset
18 | out += offset
19 |
20 | for (; i < end; i += 16) {
21 | unroll(16, () => {
22 | sample = f32.load(inp)
23 |
24 | sample = sample > 0 ? sample : 0
25 |
26 | f32.store(out, sample)
27 | inp += 4
28 | out += 4
29 | })
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/dclipexp.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Dclipexp extends Gen {
4 | _name: string = 'Dclipexp'
5 | factor: f32 = 1.0;
6 |
7 | in: u32 = 0
8 |
9 | _audio(begin: u32, end: u32, out: usize): void {
10 | const length: u32 = end - begin
11 | const factor: f32 = this.factor
12 | let sample: f32 = 0
13 | let inp: u32 = this.in
14 |
15 | let i: u32 = begin
16 | end = i + length
17 |
18 | const offset = begin << 2
19 | inp += offset
20 | out += offset
21 |
22 | for (; i < end; i += 16) {
23 | unroll(16, () => {
24 | sample = f32.load(inp)
25 |
26 | sample = f32(Math.exp(f64(sample) / f64(factor)) - 1.0)
27 |
28 | f32.store(out, sample)
29 | inp += 4
30 | out += 4
31 | })
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/dcliplin.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Dcliplin extends Gen {
4 | _name: string = 'Dcliplin';
5 | threshold: f32 = 0.5;
6 | factor: f32 = 0.5;
7 | in: u32 = 0
8 |
9 | _audio(begin: u32, end: u32, out: usize): void {
10 | const length: u32 = end - begin
11 | const threshold: f32 = this.threshold
12 | const factor: f32 = this.factor
13 |
14 | let sample: f32 = 0
15 | let inp: u32 = this.in
16 |
17 | let i: u32 = begin
18 | end = i + length
19 |
20 | const offset = begin << 2
21 | inp += offset
22 | out += offset
23 |
24 | for (; i < end; i += 16) {
25 | unroll(16, () => {
26 | sample = f32.load(inp)
27 |
28 | if (sample > threshold) {
29 | sample = threshold + (sample - threshold) * factor
30 | } else if (sample < -threshold) {
31 | sample = -threshold + (sample + threshold) * factor
32 | }
33 |
34 | f32.store(out, sample)
35 | inp += 4
36 | out += 4
37 | })
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/exp.ts:
--------------------------------------------------------------------------------
1 | import { Osc } from './osc'
2 |
3 | export class Exp extends Osc {
4 | _name: string = 'Exp'
5 | get _table(): StaticArray {
6 | return this._engine.wavetable.exp
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/freesound.ts:
--------------------------------------------------------------------------------
1 | import { Smp } from './smp'
2 |
3 | export class Freesound extends Smp {
4 | _name: string = 'Freesound';
5 | id: i32 = 0
6 |
7 | _update(): void {
8 | this._floats = !this.id ? null : changetype>(this.id)
9 | super._update()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/gen.ts:
--------------------------------------------------------------------------------
1 | import { Engine } from '../core/engine'
2 |
3 | export abstract class Gen {
4 | _name: string = 'Gen'
5 | gain: f32 = 1
6 | constructor(public _engine: Engine) { }
7 | _update(): void { }
8 | _reset(): void { }
9 | _audio(begin: u32, end: u32, out: usize): void { }
10 | _audio_stereo(begin: u32, end: u32, out_0: usize, out_1: usize): void { }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/gendy.ts:
--------------------------------------------------------------------------------
1 | import { rnd } from '../../util'
2 | import { Gen } from './gen'
3 |
4 | export class Gendy extends Gen {
5 | _name: string = 'Gendy'
6 | step: f32 = 0.00001
7 |
8 | _audio(begin: u32, end: u32, out: usize): void {
9 | const length: u32 = end - begin
10 |
11 | let i: u32 = begin
12 | end = i + length
13 |
14 | const offset = begin << 2
15 | out += offset
16 |
17 | let value: f32 = 0.0
18 |
19 | for (; i < end; i += 16) {
20 | unroll(16, () => {
21 |
22 | value += (f32(rnd()) * this.step * (f32(rnd()) > 0.1 ? 1.0 : -1.0)) //(f32(rnd()) < this.amt ? f32(rnd()) : 0.0)
23 | f32.store(out, value)
24 |
25 | out += 4
26 | })
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/grain.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 | import { rnd } from '../../util'
3 |
4 | export class Grain extends Gen {
5 | _name: string = 'Grain'
6 | amt: f32 = 1.0;
7 |
8 | _audio(begin: u32, end: u32, out: usize): void {
9 | const length: u32 = end - begin
10 |
11 | let i: u32 = begin
12 | end = i + length
13 |
14 | const offset = begin << 2
15 | out += offset
16 |
17 | let value: f32 = 0
18 |
19 | for (; i < end; i += 16) {
20 | unroll(16, () => {
21 |
22 | value = (f32(rnd()) < this.amt ? f32(rnd()) : 0.0)
23 | f32.store(out, value)
24 |
25 | out += 4
26 | })
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/inc.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Inc extends Gen {
4 | _name: string = 'Inc'
5 | amt: f32 = 1.0;
6 |
7 | /** Trigger phase sync when set to 0. */
8 | trig: f32 = -1.0
9 | _lastTrig: i32 = -1
10 |
11 | _value: f32 = 0.0
12 |
13 | _reset(): void {
14 | this.trig = -1.0
15 | this._lastTrig = -1
16 | }
17 |
18 | _update(): void {
19 | if (this._lastTrig !== i32(this.trig)) {
20 | this._value = 0.0
21 | }
22 |
23 | this._lastTrig = i32(this.trig)
24 | }
25 |
26 | _audio(begin: u32, end: u32, out: usize): void {
27 | const length: u32 = end - begin
28 | let i: u32 = begin
29 | end = i + length
30 |
31 | const offset = begin << 2
32 | out += offset
33 | const amt: f32 = this.amt * 0.001
34 | let value: f32 = this._value
35 |
36 | for (; i < end; i += 16) {
37 | unroll(16, () => {
38 | f32.store(out, value)
39 | value += amt
40 | out += 4
41 | })
42 | }
43 |
44 | this._value = value
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/lp.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Lp extends Gen {
4 | _name: string = 'Lp'
5 | cut: f32 = 500;
6 | in: u32 = 0
7 |
8 | _alpha: f32 = 0
9 | _sample: f32 = 0
10 |
11 | _update(): void {
12 | const omega: f32 = 1.0 / (2.0 * Mathf.PI * this.cut)
13 | const dt: f32 = 1.0 / f32(this._engine.sampleRate)
14 | this._alpha = dt / (omega + dt)
15 | }
16 |
17 | _audio(begin: u32, end: u32, out: usize): void {
18 | const length: u32 = end - begin
19 | const alpha: f32 = this._alpha
20 |
21 | let sample: f32 = 0
22 | let prev: f32 = this._sample
23 | let inp: u32 = this.in
24 |
25 | let i: u32 = begin
26 | end = i + length
27 |
28 | const offset = begin << 2
29 | inp += offset
30 | out += offset
31 |
32 | for (; i < end; i += 16) {
33 | unroll(16, () => {
34 | sample = f32.load(inp)
35 |
36 | sample = alpha * sample + (1.0 - alpha) * prev
37 | // Store the current sample's value for use in the next iteration
38 | prev = sample
39 | f32.store(out, sample)
40 | inp += 4
41 | out += 4
42 | })
43 | }
44 |
45 | this._sample = prev
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/mhp.ts:
--------------------------------------------------------------------------------
1 | import { Moog } from './moog'
2 |
3 | export class Mhp extends Moog {
4 | _name: string = 'Mhp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._highpass()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/mlp.ts:
--------------------------------------------------------------------------------
1 | import { Moog } from './moog'
2 |
3 | export class Mlp extends Moog {
4 | _name: string = 'Mlp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._lowpass()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/noi.ts:
--------------------------------------------------------------------------------
1 | import { Osc } from './osc'
2 |
3 | export class Noi extends Osc {
4 | _name: string = 'Noi'
5 | get _table(): StaticArray {
6 | return this._engine.wavetable.noise
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/nrate.ts:
--------------------------------------------------------------------------------
1 | import { rateToPhaseStep } from '../../util'
2 | import { Gen } from './gen'
3 |
4 | export class Nrate extends Gen {
5 | _name: string = 'Nrate'
6 | normal: f32 = 1.0
7 | _reset(): void {
8 | this.normal = 1.0
9 | this._update()
10 | }
11 | _update(): void {
12 | let samples: u32 = u32(f32(this._engine.sampleRate) * this.normal)
13 | if (samples < 1) samples = 1
14 |
15 | this._engine.rateSamples = samples
16 | this._engine.rateSamplesRecip = (1.0 / f64(this._engine.rateSamples))
17 | this._engine.rateStep = rateToPhaseStep(samples)
18 | this._engine.samplesPerMs = f64(this._engine.rateSamples) / 1000
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/osc.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export abstract class Osc extends Gen {
4 | /** Frequency. */
5 | hz: f32 = 0
6 | /** Trigger phase sync when set to 0. */
7 | trig: f32 = -1.0
8 | /** Phase offset. */
9 | offset: f32 = 0
10 |
11 | _phase: u32 = 0
12 | _step: u32 = 0
13 | _sample: f32 = 0
14 | _lastTrig: i32 = -1
15 | _offsetU32: u32 = 0
16 | _initial: boolean = true
17 |
18 | get _table(): StaticArray {
19 | return this._engine.wavetable.sine
20 | }
21 |
22 | get _mask(): u32 {
23 | return this._engine.wavetable.mask
24 | }
25 |
26 | _reset(): void {
27 | this.hz = 0
28 | this.trig = -1.0
29 | this.offset = 0
30 | this._lastTrig = -1
31 | this._phase = 0
32 | }
33 |
34 | _update(): void {
35 | // TODO: the / 8 needs to be determined and not hard coded
36 | this._step = u32(this.hz * this._engine.rateStep / 8)
37 | this._offsetU32 = u32(this.offset * 0xFFFFFFFF)
38 | this.offset = 0
39 |
40 | if (this._lastTrig !== i32(this.trig)) {
41 | this._phase = 0
42 | }
43 |
44 | // TODO: implement Sync (zero crossing reset phase)
45 |
46 | this._lastTrig = i32(this.trig)
47 | }
48 |
49 | _next(): void {
50 | this._phase += this._step
51 | }
52 |
53 | _audio(begin: u32, end: u32, targetPtr: usize): void {
54 | this._phase = this._engine.wavetable.read(
55 | changetype(this._table),
56 | this._mask,
57 | this._phase,
58 | this._offsetU32,
59 | this._step,
60 | begin,
61 | end,
62 | targetPtr
63 | )
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/ramp.ts:
--------------------------------------------------------------------------------
1 | import { Aosc } from './aosc'
2 |
3 | export class Ramp extends Aosc {
4 | _name: string = 'Ramp'
5 | get _tables(): StaticArray> {
6 | return this._engine.wavetable.antialias.ramp
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/rate.ts:
--------------------------------------------------------------------------------
1 | import { rateToPhaseStep } from '../../util'
2 | import { Engine } from '../core/engine'
3 | import { Gen } from './gen'
4 |
5 | export class Rate extends Gen {
6 | _name: string = 'Rate'
7 | samples: f32
8 | constructor(public _engine: Engine) {
9 | super(_engine)
10 | this.samples = f32(_engine.sampleRate)
11 | }
12 | _update(): void {
13 | this._engine.rateSamples = u32(this.samples)
14 | this._engine.rateSamplesRecip = (1.0 / f64(this._engine.rateSamples))
15 | this._engine.rateStep = rateToPhaseStep(u32(this.samples))
16 | this._engine.samplesPerMs = f64(this._engine.rateSamples) / 1000
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/sap.ts:
--------------------------------------------------------------------------------
1 | import { Svf } from './svf'
2 |
3 | export class Sap extends Svf {
4 | _name: string = 'Sap'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._allpass()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/saw.ts:
--------------------------------------------------------------------------------
1 | import { Aosc } from './aosc'
2 |
3 | export class Saw extends Aosc {
4 | _name: string = 'Saw'
5 | get _tables(): StaticArray> {
6 | return this._engine.wavetable.antialias.saw
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/say.ts:
--------------------------------------------------------------------------------
1 | import { Smp } from './smp'
2 |
3 | export class Say extends Smp {
4 | _name: string = 'Say'
5 | text: i32 = 0
6 |
7 | _update(): void {
8 | this._floats = !this.text ? null : changetype>(this.text)
9 | super._update()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/sbp.ts:
--------------------------------------------------------------------------------
1 | import { Svf } from './svf'
2 |
3 | export class Sbp extends Svf {
4 | _name: string = 'Sbp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._bandpass()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/shp.ts:
--------------------------------------------------------------------------------
1 | import { Svf } from './svf'
2 |
3 | export class Shp extends Svf {
4 | _name: string = 'Shp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._highpass()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/sin.ts:
--------------------------------------------------------------------------------
1 | import { Osc } from './osc'
2 |
3 | export class Sin extends Osc {
4 | _name: string = 'Sin'
5 | get _table(): StaticArray {
6 | return this._engine.wavetable.sine
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/slp.ts:
--------------------------------------------------------------------------------
1 | import { Svf } from './svf'
2 |
3 | export class Slp extends Svf {
4 | _name: string = 'Slp'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._lowpass()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/sno.ts:
--------------------------------------------------------------------------------
1 | import { Svf } from './svf'
2 |
3 | export class Sno extends Svf {
4 | _name: string = 'Sno'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._notch()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/spk.ts:
--------------------------------------------------------------------------------
1 | import { Svf } from './svf'
2 |
3 | export class Spk extends Svf {
4 | _name: string = 'Spk'
5 | cut: f32 = 500
6 | q: f32 = 0.5
7 |
8 | _update(): void {
9 | this._updateCoeffs(this.cut, this.q)
10 | }
11 |
12 | _audio(begin: u32, end: u32, out: usize): void {
13 | const length: u32 = end - begin
14 |
15 | let sample: f32 = 0
16 | let inp: u32 = this.in
17 |
18 | let i: u32 = begin
19 | end = i + length
20 |
21 | const offset = begin << 2
22 | inp += offset
23 | out += offset
24 |
25 | for (; i < end; i += 16) {
26 | unroll(16, () => {
27 | sample = f32.load(inp)
28 | this._process(sample)
29 | sample = this._peak()
30 | f32.store(out, sample)
31 | inp += 4
32 | out += 4
33 | })
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/sqr.ts:
--------------------------------------------------------------------------------
1 | import { Aosc } from './aosc'
2 |
3 | export class Sqr extends Aosc {
4 | _name: string = 'Sqr'
5 | get _tables(): StaticArray> {
6 | return this._engine.wavetable.antialias.sqr
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/tanh.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Tanh extends Gen {
4 | _name: string = 'Tanh';
5 | in: u32 = 0
6 |
7 | _audio(begin: u32, end: u32, out: usize): void {
8 | const length: u32 = end - begin
9 |
10 | let sample: f32 = 0
11 | let inp: u32 = this.in
12 |
13 | let i: u32 = begin
14 | end = i + length
15 |
16 | const offset = begin << 2
17 | inp += offset
18 | out += offset
19 |
20 | const gain: f32 = this.gain
21 |
22 | for (; i < end; i += 16) {
23 | unroll(16, () => {
24 | sample = f32.load(inp)
25 | sample = Mathf.tanh(sample * gain)
26 | f32.store(out, sample)
27 | inp += 4
28 | out += 4
29 | })
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/tanha.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Tanha extends Gen {
4 | _name: string = 'Tanha';
5 | in: u32 = 0
6 |
7 | _gainv: v128 = f32x4.splat(1.0)
8 |
9 | _update(): void {
10 | this._gainv = f32x4.splat(this.gain)
11 | }
12 |
13 | _audio(begin: u32, end: u32, out: usize): void {
14 | const gainv: v128 = this._gainv
15 |
16 | let in0: u32 = this.in
17 |
18 | let x: v128
19 | let resv: v128
20 |
21 | let i: u32 = begin
22 |
23 | const offset = begin << 2
24 | in0 += offset
25 | out += offset
26 |
27 | let x2: v128
28 |
29 | for (; i < end; i += 64) {
30 | unroll(16, () => {
31 | x = v128.load(in0)
32 |
33 | x = f32x4.mul(x, gainv)
34 |
35 | // Calculate x * x
36 | x2 = f32x4.mul(x, x)
37 |
38 | // Calculate 5.0 + x * x
39 | resv = f32x4.add(f32x4.splat(5.0), x2)
40 |
41 | // Calculate x * x / (5.0 + x * x)
42 | resv = f32x4.div(x2, resv)
43 |
44 | // Calculate 3.0 + x * x / (5.0 + x * x)
45 | resv = f32x4.add(f32x4.splat(3.0), resv)
46 |
47 | // Calculate x / (1.0 + x * x / (3.0 + x * x / 5.0))
48 | resv = f32x4.div(x, f32x4.add(f32x4.splat(1.0), f32x4.div(x2, resv)))
49 |
50 | // Calculate min(x / (1.0 + x * x / (3.0 + x * x / 5.0)), 1.0)
51 | resv = f32x4.min(resv, f32x4.splat(1.0))
52 |
53 | // Calculate max(-1.0, min(x / (1.0 + x * x / (3.0 + x * x / 5.0)), 1.0))
54 | resv = f32x4.max(resv, f32x4.splat(-1.0))
55 |
56 | v128.store(out, resv)
57 |
58 | in0 += 16
59 | out += 16
60 | })
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/tap.ts:
--------------------------------------------------------------------------------
1 | import { cubic } from '../../util'
2 | import { DELAY_MAX_SIZE } from '../core/constants'
3 | import { Gen } from './gen'
4 |
5 | export class Tap extends Gen {
6 | _name: string = 'Tap'
7 | ms: f32 = 200;
8 | in: u32 = 0;
9 |
10 | _floats: StaticArray = new StaticArray(DELAY_MAX_SIZE)
11 | _mask: u32 = DELAY_MAX_SIZE - 1
12 | _index: u32 = 0
13 | _stepf: f32 = 0
14 | _targetf: f32 = 0
15 |
16 | _update(): void {
17 | this._targetf = Mathf.min(DELAY_MAX_SIZE - 1, (this.ms * 0.001) * this._engine.rateStep)
18 | if (this._stepf === 0) this._stepf = this._targetf
19 | }
20 |
21 | _audio(begin: u32, end: u32, out: usize): void {
22 | const length: u32 = end - begin
23 |
24 | let sample: f32 = 0
25 | let inp: u32 = this.in
26 |
27 | let i: u32 = begin
28 | end = i + length
29 |
30 | const offset = begin << 2
31 | inp += offset
32 | out += offset
33 |
34 | const mask: u32 = this._mask
35 | let index: u32 = this._index
36 | let delay: f32 = 0
37 | let stepf: f32 = this._stepf
38 | const targetf: f32 = this._targetf
39 |
40 | for (; i < end; i += 16) {
41 | unroll(16, () => {
42 | sample = f32.load(inp)
43 |
44 | delay = cubic(this._floats, (index - stepf), mask)
45 | this._floats[index] = sample
46 | f32.store(out, delay)
47 |
48 | inp += 4
49 | out += 4
50 |
51 | index = (index + 1) & mask
52 | stepf += (targetf - stepf) * 0.0008
53 | })
54 | }
55 |
56 | this._index = index
57 | this._stepf = stepf
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/tri.ts:
--------------------------------------------------------------------------------
1 | import { Aosc } from './aosc'
2 |
3 | export class Tri extends Aosc {
4 | _name: string = 'Tri'
5 | get _tables(): StaticArray> {
6 | return this._engine.wavetable.antialias.tri
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/dsp/gen/zero.ts:
--------------------------------------------------------------------------------
1 | import { Gen } from './gen'
2 |
3 | export class Zero extends Gen {
4 | _name: string = 'Zero'
5 | _audio(begin: u32, end: u32, out: usize): void {
6 | const zerov: v128 = f32x4.splat(0)
7 |
8 | let i: u32 = begin
9 |
10 | const offset = begin << 2
11 | out += offset
12 |
13 | for (; i < end; i += 64) {
14 | unroll(16, () => {
15 | v128.store(out, zerov)
16 | out += 16
17 | })
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/as/assembly/dsp/graph/copy.ts:
--------------------------------------------------------------------------------
1 | export function copyMem(
2 | inp: usize,
3 | out: usize,
4 | size: usize
5 | ): void {
6 | memory.copy(out, inp, size)
7 |
8 | // let inpv: v128
9 |
10 | // let i: u32 = 0
11 | // length = length << 2
12 |
13 | // for (; i < length; i += 64) {
14 | // unroll(16, () => {
15 | // inpv = v128.load(inp)
16 | // v128.store(out, inpv)
17 | // inp += 16
18 | // out += 16
19 | // })
20 | // }
21 | }
22 |
23 | export function copyInto(
24 | begin: u32,
25 | end: u32,
26 | inp: usize,
27 | out: usize,
28 | ): void {
29 | const size: usize = (end - begin) << 2
30 | const offset: usize = begin << 2
31 | memory.copy(
32 | out + offset,
33 | inp + offset,
34 | size
35 | )
36 | }
37 |
38 | export function copyAt(
39 | begin: u32,
40 | end: u32,
41 | inp: usize,
42 | out: usize,
43 | ): void {
44 | const size: usize = (end - begin) << 2
45 | const offset: usize = begin << 2
46 | memory.copy(
47 | out,
48 | inp + offset,
49 | size
50 | )
51 | }
52 |
53 | // export function copyInto(
54 | // begin: u32,
55 | // end: u32,
56 | // inp: usize,
57 | // out: usize,
58 | // ): void {
59 | // let inpv: v128
60 |
61 | // let i: u32 = begin
62 |
63 | // const offset = begin << 2
64 | // inp += offset
65 | // out += offset
66 |
67 | // for (; i < end; i += 64) {
68 | // unroll(16, () => {
69 | // inpv = v128.load(inp)
70 | // v128.store(out, inpv)
71 | // inp += 16
72 | // out += 16
73 | // })
74 | // }
75 | // }
76 |
--------------------------------------------------------------------------------
/as/assembly/dsp/graph/dc-bias-old.ts:
--------------------------------------------------------------------------------
1 | import { logf } from '../../env'
2 |
3 | export function dcBias(
4 | begin: u32,
5 | end: u32,
6 | block: usize,
7 | ): void {
8 | const ptr: usize = block
9 | const length: u32 = end - begin
10 | let i: u32 = begin
11 | let resv: v128 = f32x4.splat(0)
12 |
13 | end = i + (length >> 2) // divide length by 4 because we process 4 elements at a time
14 |
15 | for (; i < end; i += 32) {
16 | unroll(32, () => {
17 | resv = f32x4.add(resv, v128.load(block))
18 | block += 16
19 | })
20 | }
21 |
22 | const sum: f32 =
23 | f32x4.extract_lane(resv, 0)
24 | + f32x4.extract_lane(resv, 1)
25 | + f32x4.extract_lane(resv, 2)
26 | + f32x4.extract_lane(resv, 3)
27 |
28 | const mean: f32 = sum / f32(length)
29 | logf(mean)
30 | const meanv: v128 = f32x4.splat(mean)
31 |
32 | block = ptr
33 | i = begin
34 | for (; i < end; i += 32) {
35 | unroll(32, () => {
36 | resv = v128.load(block)
37 | resv = f32x4.sub(resv, meanv)
38 | v128.store(block, resv)
39 | block += 16
40 | })
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/as/assembly/dsp/graph/dc-bias.ts:
--------------------------------------------------------------------------------
1 | import { logf } from '../../env'
2 |
3 | export function dcBias(
4 | begin: u32,
5 | end: u32,
6 | block: usize,
7 | ): void {
8 | const length: u32 = end - begin
9 | let sample: f32 = 0
10 | let prev: f32 = 0
11 | let diff: f32 = 0
12 | let abs: f32 = 0
13 | const threshold: f32 = 0.6
14 | let alpha: f32 = 0
15 |
16 | let i: u32 = begin
17 | end = i + (length << 2)
18 |
19 | const offset = begin << 2
20 | block += offset
21 |
22 | for (; i < end; i += 16) {
23 | unroll(16, () => {
24 | sample = f32.load(block)
25 | // logf(sample)
26 | diff = sample - prev
27 | abs = Mathf.abs(diff)
28 | if (abs > threshold) {
29 | alpha = (threshold - abs) / threshold
30 | if (alpha > 1) alpha = 1
31 | else if (alpha < 0) alpha = 0
32 | prev = sample
33 | sample = prev + alpha * diff
34 | // logf(sample)
35 | f32.store(block, sample)
36 | }
37 | else {
38 | prev = sample
39 | }
40 | block += 4
41 | })
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/as/assembly/dsp/graph/fill.ts:
--------------------------------------------------------------------------------
1 | export function fill(value: f32, begin: u32, end: u32, out: usize): void {
2 | const v: v128 = f32x4.splat(value)
3 |
4 | let i: u32 = begin
5 |
6 | const offset = begin << 2
7 | out += offset
8 |
9 | for (; i < end; i += 64) {
10 | unroll(16, () => {
11 | v128.store(out, v)
12 | out += 16
13 | })
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/as/assembly/dsp/graph/join.ts:
--------------------------------------------------------------------------------
1 | import { logi } from '../../env'
2 |
3 | export function join21g(
4 | begin: u32,
5 | end: u32,
6 | in0: usize,
7 | in1: usize,
8 | out: usize,
9 | gain0: f32,
10 | gain1: f32,
11 | ): void {
12 | const gain0v: v128 = f32x4.splat(gain0)
13 | const gain1v: v128 = f32x4.splat(gain1)
14 |
15 | let in0v: v128
16 | let in1v: v128
17 | let resv: v128
18 |
19 | let i: u32 = begin
20 |
21 | const offset = begin << 2
22 | in0 += offset
23 | in1 += offset
24 | out += offset
25 |
26 | for (; i < end; i += 64) {
27 | unroll(16, () => {
28 | in0v = v128.load(in0)
29 | in1v = v128.load(in1)
30 | resv = f32x4.add(
31 | f32x4.mul(in0v, gain0v),
32 | f32x4.mul(in1v, gain1v)
33 | )
34 | v128.store(out, resv)
35 | in0 += 16
36 | in1 += 16
37 | out += 16
38 | })
39 | }
40 | }
41 |
42 | // TODO: consolidate with math.add_audio_audio
43 | export function join21(
44 | begin: u32,
45 | end: u32,
46 | in0: usize,
47 | in1: usize,
48 | out: usize,
49 | ): void {
50 | let in0v: v128
51 | let in1v: v128
52 | let resv: v128
53 |
54 | let i: u32 = begin
55 |
56 | const offset = begin << 2
57 | in0 += offset
58 | in1 += offset
59 | out += offset
60 |
61 | for (; i < end; i += 64) {
62 | unroll(16, () => {
63 | in0v = v128.load(in0)
64 | in1v = v128.load(in1)
65 | resv = f32x4.add(in0v, in1v)
66 | v128.store(out, resv)
67 | in0 += 16
68 | in1 += 16
69 | out += 16
70 | })
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/as/assembly/dsp/shared.ts:
--------------------------------------------------------------------------------
1 | @unmanaged
2 | export class Track {
3 | run_ops$: usize = 0
4 | setup_ops$: usize = 0
5 | literals$: usize = 0
6 | lists$: usize = 0
7 | audio_LR$: i32 = 0
8 | }
9 |
10 | @unmanaged
11 | export class Out {
12 | L$: usize = 0
13 | R$: usize = 0
14 | }
15 |
16 | @unmanaged
17 | export class SoundValue {
18 | kind: i32 = 0
19 | ptr: i32 = 0
20 | scalar$: i32 = 0
21 | audio$: i32 = 0
22 | }
23 |
--------------------------------------------------------------------------------
/as/assembly/dsp/vm/dsp-shared.ts:
--------------------------------------------------------------------------------
1 | export enum SoundValueKind {
2 | Null,
3 | I32,
4 | Floats,
5 | Literal,
6 | Scalar,
7 | Audio,
8 | Dynamic,
9 | }
10 |
11 | export enum DspBinaryOp {
12 | // math: commutative
13 | Add,
14 | Mul,
15 |
16 | // math: non-commutative
17 | Sub,
18 | Div,
19 | Pow,
20 | }
21 |
--------------------------------------------------------------------------------
/as/assembly/dsp/vm/player.ts:
--------------------------------------------------------------------------------
1 | import { run as dspRun } from '../../../../generated/assembly/dsp-runner'
2 | import { Track } from '../shared'
3 | import { Sound } from './sound'
4 |
5 | export class Player {
6 | sound: Sound
7 | track$: usize = 0
8 | lastTrack$: usize = 0
9 |
10 | constructor(public sound$: usize, public out$: usize) {
11 | this.sound = changetype(sound$)
12 | }
13 |
14 | process(begin: u32, end: u32): void {
15 | const sound = this.sound
16 |
17 | const track$ = this.track$
18 |
19 | if (track$ !== this.lastTrack$) {
20 | this.lastTrack$ = track$
21 | // sound.reset() // ??
22 | // ideally we should compare gens and move
23 | // the reused gens to the new context
24 | sound.clear()
25 | sound.setupTrack(track$)
26 | }
27 | // console.log(`${track$}`)
28 | sound.fillTrack(track$, begin, end, this.out$)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/as/assembly/gfx/env.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | @external('env', 'flushSketch')
3 | export declare function flushSketch(count: i32): void
4 |
--------------------------------------------------------------------------------
/as/assembly/gfx/index.ts:
--------------------------------------------------------------------------------
1 | import { Sketch } from './sketch'
2 | import { Box, Line, Matrix, Note, Notes, ParamValue, Params, Wave } from './sketch-shared'
3 |
4 | export * from '../alloc'
5 | export * from './draw'
6 |
7 | export function createSketch(
8 | a_vert$: usize,
9 | a_style$: usize,
10 | ): Sketch {
11 | return new Sketch(
12 | a_vert$,
13 | a_style$,
14 | )
15 | }
16 |
17 | export function createBox(): usize {
18 | return changetype(new Box())
19 | }
20 |
21 | export function createLine(): usize {
22 | return changetype(new Line())
23 | }
24 |
25 | export function createWave(): usize {
26 | return changetype(new Wave())
27 | }
28 |
29 | export function createNotes(): usize {
30 | return changetype(new Notes())
31 | }
32 |
33 | export function createNote(): usize {
34 | return changetype(new Note())
35 | }
36 |
37 | export function createParams(): usize {
38 | return changetype(new Params())
39 | }
40 |
41 | export function createParamValue(): usize {
42 | return changetype(new ParamValue())
43 | }
44 |
45 | export function createMatrix(): usize {
46 | return changetype(new Matrix())
47 | }
48 |
--------------------------------------------------------------------------------
/as/assembly/gfx/sketch.ts:
--------------------------------------------------------------------------------
1 | import { Floats } from '../types'
2 | import { flushSketch } from './env'
3 | import { MAX_GL_INSTANCES, VertOpts } from './sketch-shared'
4 |
5 | /**
6 | * Sketch holds the data that is sent to WebGL.
7 | */
8 | export class Sketch {
9 | ptr: u32 = 0
10 | a_vert: Floats
11 | a_style: Floats
12 | constructor(
13 | public a_vert$: usize,
14 | public a_style$: usize,
15 | ) {
16 | this.a_vert = changetype(a_vert$)
17 | this.a_style = changetype(a_style$)
18 | }
19 | @inline
20 | flush(): void {
21 | flushSketch(this.ptr)
22 | this.ptr = 0
23 | }
24 | @inline
25 | advance(): void {
26 | if (++this.ptr === MAX_GL_INSTANCES) {
27 | this.flush()
28 | }
29 | }
30 | @inline
31 | drawBox(
32 | x: f32, y: f32, w: f32, h: f32,
33 | color: f32,
34 | alpha: f32,
35 | ): void {
36 | const ptr = this.ptr
37 | const ptr4 = (ptr * 4) << 2
38 | store4(this.a_vert$ + ptr4, x, y, w, h)
39 | store4(this.a_style$ + ptr4, color, alpha, f32(VertOpts.Box), 1.0)
40 | this.advance()
41 | }
42 | @inline
43 | drawLine(
44 | x0: f32, y0: f32,
45 | x1: f32, y1: f32,
46 | color: f32,
47 | alpha: f32,
48 | lineWidth: f32
49 | ): void {
50 | const ptr = this.ptr
51 | const ptr4 = (ptr * 4) << 2
52 | store4(this.a_vert$ + ptr4, x0, y0, x1, y1)
53 | store4(this.a_style$ + ptr4, color, alpha, f32(VertOpts.Line), lineWidth)
54 | this.advance()
55 | }
56 | }
57 |
58 | // @ts-ignore
59 | @inline
60 | function store4(ptr: usize, x: f32, y: f32, z: f32, w: f32): void {
61 | const v = f32x4(x, y, z, w)
62 | v128.store(ptr, v)
63 | }
64 |
65 | // @ts-ignore
66 | @inline
67 | function store2(ptr: usize, x: f32, y: f32): void {
68 | f32.store(ptr, x)
69 | f32.store(ptr, y, 4)
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/as/assembly/gfx/util.ts:
--------------------------------------------------------------------------------
1 |
2 | export function lineIntersectsRect(
3 | x0: f32, y0: f32, x1: f32, y1: f32,
4 | rectX: f32, rectY: f32, rectWidth: f32, rectHeight: f32
5 | ): boolean {
6 | const minX: f32 = Mathf.min(x0, x1)
7 | const minY: f32 = Mathf.min(y0, y1)
8 | const maxX: f32 = Mathf.max(x0, x1)
9 | const maxY: f32 = Mathf.max(y0, y1)
10 |
11 | if (maxX < rectX || minX > rectX + rectWidth || maxY < rectY || minY > rectY + rectHeight) {
12 | return false
13 | }
14 |
15 | return true
16 | }
17 |
--------------------------------------------------------------------------------
/as/assembly/pkg/constants.ts:
--------------------------------------------------------------------------------
1 | export const BUFFER_SIZE = 128
2 |
--------------------------------------------------------------------------------
/as/assembly/pkg/index.ts:
--------------------------------------------------------------------------------
1 | import { Player } from './player'
2 | import { Out } from './shared'
3 |
4 | export * from '../alloc'
5 | export * from './math'
6 | export * from './rand'
7 |
8 | export function createPlayer(sampleRate: u32): Player {
9 | return new Player(sampleRate)
10 | }
11 |
12 | export function playerProcess(player$: usize, begin: u32, end: u32, out$: usize): void {
13 | const player = changetype(player$)
14 | player.process(begin, end, out$)
15 | }
16 |
17 | export function createOut(): usize {
18 | return changetype(new Out())
19 | }
20 |
--------------------------------------------------------------------------------
/as/assembly/pkg/math.ts:
--------------------------------------------------------------------------------
1 | export function multiply(a: f32, b: f32): f32 {
2 | return a * b
3 | }
4 |
--------------------------------------------------------------------------------
/as/assembly/pkg/player.ts:
--------------------------------------------------------------------------------
1 | import { rand } from './rand'
2 | import { Out } from './shared'
3 |
4 | export class Player {
5 | constructor(public sampleRate: u32) { }
6 | process(begin: u32, end: u32, out$: usize): void {
7 | const out = changetype(out$)
8 | const out_L = out.L$
9 | const out_R = out.R$
10 |
11 | let pos: u32 = begin
12 | let offset: u32
13 | let sample: f32
14 |
15 | for (; pos < end; pos++) {
16 | offset = pos << 2
17 | sample = 0.1 * rand() as f32
18 | store(out_L + offset, sample)
19 | store(out_R + offset, sample)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/as/assembly/pkg/rand.ts:
--------------------------------------------------------------------------------
1 | let seed: u32 = 0
2 | export function rand(amt: f64 = 1): f64 {
3 | seed += 0x6D2B79F5
4 | let t: u32 = seed
5 | t = (t ^ t >>> 15) * (t | 1)
6 | t ^= t + (t ^ t >>> 7) * (t | 61)
7 | return (f64((t ^ t >>> 14) >>> 0) / 4294967296.0) * amt
8 | }
9 |
--------------------------------------------------------------------------------
/as/assembly/pkg/shared.ts:
--------------------------------------------------------------------------------
1 | @unmanaged
2 | export class Out {
3 | L$: usize = 0
4 | R$: usize = 0
5 | }
6 |
--------------------------------------------------------------------------------
/as/assembly/rms.ts:
--------------------------------------------------------------------------------
1 | import { BUFFER_SIZE } from './dsp/constants'
2 | import { rms } from './dsp/graph/rms'
3 | import { clamp01 } from './util'
4 |
5 | export const floats = changetype(new StaticArray(BUFFER_SIZE))
6 |
7 | export function run(): f32 {
8 | return clamp01(rms(changetype(floats), 0, BUFFER_SIZE))
9 | }
10 |
--------------------------------------------------------------------------------
/as/assembly/types.ts:
--------------------------------------------------------------------------------
1 | export type Floats = StaticArray
2 |
--------------------------------------------------------------------------------
/as/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "assemblyscript/std/assembly.json",
3 | "include": [
4 | "./assembly/**/*.ts"
5 | ],
6 | "compilerOptions": {
7 | "baseUrl": "..",
8 | "experimentalDecorators": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/asconfig-dsp-nort.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "debug": {
4 | "outFile": "./as/build/dsp-nort.wasm",
5 | "textFile": "./as/build/dsp-nort.wat",
6 | "sourceMap": true,
7 | "debug": true,
8 | "noAssert": true
9 | },
10 | "release": {
11 | "outFile": "./as/build/dsp-nort.wasm",
12 | "textFile": "./as/build/dsp-nort.wat",
13 | "sourceMap": true,
14 | "debug": false,
15 | "optimizeLevel": 0,
16 | "shrinkLevel": 0,
17 | "converge": false,
18 | "noAssert": true
19 | }
20 | },
21 | "options": {
22 | "enable": [
23 | "simd",
24 | "relaxed-simd",
25 | "threads"
26 | ],
27 | "sharedMemory": true,
28 | "importMemory": true,
29 | "initialMemory": 2000,
30 | "maximumMemory": 2000,
31 | "bindings": "raw",
32 | "runtime": false,
33 | "exportRuntime": false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/asconfig-dsp.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "debug": {
4 | "outFile": "./as/build/dsp.wasm",
5 | "textFile": "./as/build/dsp.wat",
6 | "sourceMap": true,
7 | "debug": true,
8 | "noAssert": true
9 | },
10 | "release": {
11 | "outFile": "./as/build/dsp.wasm",
12 | "textFile": "./as/build/dsp.wat",
13 | "sourceMap": true,
14 | "debug": false,
15 | "optimizeLevel": 0,
16 | "shrinkLevel": 0,
17 | "converge": false,
18 | "noAssert": true
19 | }
20 | },
21 | "options": {
22 | "enable": [
23 | "simd",
24 | "relaxed-simd",
25 | "threads"
26 | ],
27 | "sharedMemory": true,
28 | "importMemory": false,
29 | "initialMemory": 2000,
30 | "maximumMemory": 2000,
31 | "bindings": "raw",
32 | "runtime": "incremental",
33 | "exportRuntime": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/asconfig-gfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "debug": {
4 | "outFile": "./as/build/gfx.wasm",
5 | "textFile": "./as/build/gfx.wat",
6 | "sourceMap": true,
7 | "debug": true,
8 | "noAssert": true
9 | },
10 | "release": {
11 | "outFile": "./as/build/gfx.wasm",
12 | "textFile": "./as/build/gfx.wat",
13 | "sourceMap": true,
14 | "debug": false,
15 | "optimizeLevel": 0,
16 | "shrinkLevel": 0,
17 | "converge": false,
18 | "noAssert": true
19 | }
20 | },
21 | "options": {
22 | "enable": [
23 | "simd",
24 | "relaxed-simd",
25 | "threads"
26 | ],
27 | "sharedMemory": true,
28 | "importMemory": false,
29 | "initialMemory": 2000,
30 | "maximumMemory": 2000,
31 | "bindings": "raw",
32 | "runtime": "incremental",
33 | "exportRuntime": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/asconfig-pkg-nort.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "debug": {
4 | "outFile": "./as/build/pkg-nort.wasm",
5 | "textFile": "./as/build/pkg-nort.wat",
6 | "sourceMap": true,
7 | "debug": true,
8 | "noAssert": true
9 | },
10 | "release": {
11 | "outFile": "./as/build/pkg-nort.wasm",
12 | "textFile": "./as/build/pkg-nort.wat",
13 | "sourceMap": true,
14 | "debug": false,
15 | "optimizeLevel": 0,
16 | "shrinkLevel": 0,
17 | "converge": false,
18 | "noAssert": true
19 | }
20 | },
21 | "options": {
22 | "enable": [
23 | "simd",
24 | "relaxed-simd",
25 | "threads"
26 | ],
27 | "sharedMemory": true,
28 | "importMemory": true,
29 | "initialMemory": 500,
30 | "maximumMemory": 500,
31 | "bindings": "raw",
32 | "runtime": false,
33 | "exportRuntime": false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/asconfig-pkg.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "debug": {
4 | "outFile": "./as/build/pkg.wasm",
5 | "textFile": "./as/build/pkg.wat",
6 | "sourceMap": true,
7 | "debug": true,
8 | "noAssert": true
9 | },
10 | "release": {
11 | "outFile": "./as/build/pkg.wasm",
12 | "textFile": "./as/build/pkg.wat",
13 | "sourceMap": true,
14 | "debug": false,
15 | "optimizeLevel": 0,
16 | "shrinkLevel": 0,
17 | "converge": false,
18 | "noAssert": true
19 | }
20 | },
21 | "options": {
22 | "enable": [
23 | "simd",
24 | "relaxed-simd",
25 | "threads"
26 | ],
27 | "sharedMemory": true,
28 | "importMemory": false,
29 | "initialMemory": 500,
30 | "maximumMemory": 500,
31 | "bindings": "raw",
32 | "runtime": "incremental",
33 | "exportRuntime": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/asconfig-rms.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "debug": {
4 | "outFile": "./as/build/rms.wasm",
5 | "textFile": "./as/build/rms.wat",
6 | "sourceMap": true,
7 | "debug": true,
8 | "noAssert": true
9 | },
10 | "release": {
11 | "outFile": "./as/build/rms.wasm",
12 | "textFile": "./as/build/rms.wat",
13 | "sourceMap": true,
14 | "debug": false,
15 | "optimizeLevel": 0,
16 | "shrinkLevel": 0,
17 | "converge": false,
18 | "noAssert": true
19 | }
20 | },
21 | "options": {
22 | "enable": [
23 | "simd",
24 | "relaxed-simd",
25 | "threads"
26 | ],
27 | "sharedMemory": true,
28 | "importMemory": false,
29 | "initialMemory": 10,
30 | "maximumMemory": 10,
31 | "bindings": "raw",
32 | "runtime": false,
33 | "exportRuntime": false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/bunfig.toml:
--------------------------------------------------------------------------------
1 | [test]
2 | preload = "./lib/happydom.ts"
3 | coverageSkipTestFiles = true
4 | coverageReporter = ["text", "lcov"]
5 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | patch:
4 | default:
5 | target: 10%
6 | project:
7 | default:
8 | target: 10%
9 |
10 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | postgres:
3 | image: "postgres:16.4-alpine"
4 | environment:
5 | POSTGRES_USER: postgres
6 | POSTGRES_PASSWORD: postgres
7 | POSTGRES_DB: postgres
8 | ports:
9 | - "5432:5432"
10 | pg_proxy:
11 | image: ghcr.io/neondatabase/wsproxy:latest
12 | environment:
13 | APPEND_PORT: "postgres:5432"
14 | ALLOW_ADDR_REGEX: ".*"
15 | LOG_TRAFFIC: "true"
16 | ports:
17 | - "5433:80"
18 | depends_on:
19 | - postgres
20 | s3service:
21 | image: quay.io/minio/minio:latest
22 | volumes:
23 | - ./storage/:/storage
24 | command: server --console-address ":9001" /storage
25 | ports:
26 | - "9000:9000"
27 | - "9001:9001"
28 | env_file: .env.development
29 | initialize-s3service:
30 | image: quay.io/minio/mc
31 | depends_on:
32 | - s3service
33 | entrypoint: >
34 | /bin/sh -c '
35 | until (/usr/bin/mc config host add s3service http://s3service:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}") do echo '...waiting...' && sleep 1; done;
36 | /usr/bin/mc mb s3service/"$${S3_BUCKET_NAME}";
37 | /usr/bin/mc admin user add s3service "$${S3_ACCESS_KEY}" "$${S3_SECRET_KEY}";
38 | /usr/bin/mc admin policy attach s3service readwrite --user "$${S3_ACCESS_KEY}";
39 | exit 0;
40 | '
41 | env_file: .env.development
42 |
--------------------------------------------------------------------------------
/generated/assembly/dsp-op.ts:
--------------------------------------------------------------------------------
1 | // TypeScript + AssemblyScript Ops Enum
2 | // auto-generated from scripts
3 | export enum Op {
4 | End,
5 | Begin,
6 | CreateGen,
7 | CreateAudios,
8 | CreateValues,
9 | AudioToScalar,
10 | LiteralToAudio,
11 | Pick,
12 | Pan,
13 | SetValue,
14 | SetValueDynamic,
15 | SetProperty,
16 | UpdateGen,
17 | ProcessAudio,
18 | ProcessAudioStereo,
19 | BinaryOp
20 | }
21 |
--------------------------------------------------------------------------------
/generated/assembly/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "assemblyscript/std/assembly.json",
3 | "include": [
4 | "./**/*.ts"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 | ravescript - make music with code
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/kysely.config.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv'
2 | import { CamelCasePlugin, PostgresDialect } from 'kysely'
3 | import { defineConfig } from 'kysely-ctl'
4 | import { Pool } from 'pg'
5 |
6 | const DATABASE_URL = process.env.DATABASE_URL
7 |
8 | dotenv.config({
9 | override: true,
10 | path: process.env.NODE_ENV === 'production'
11 | ? '.env.production'
12 | : '.env.development'
13 | })
14 |
15 | if (DATABASE_URL) process.env.DATABASE_URL = DATABASE_URL
16 | const connectionString = process.env.DATABASE_URL
17 | if (!connectionString) {
18 | throw new Error('kysely: No connectionString found.')
19 | }
20 |
21 | console.log('NODE_ENV:', process.env.NODE_ENV)
22 |
23 | const dialect = new PostgresDialect({
24 | pool: new Pool({
25 | connectionString,
26 | max: 10,
27 | })
28 | })
29 |
30 | export default defineConfig({
31 | dialect,
32 | migrations: {
33 | migrationFolder: 'migrations',
34 | },
35 | plugins: [new CamelCasePlugin()],
36 | // seeds: {
37 | // seedFolder: 'seeds',
38 | // }
39 | })
40 |
--------------------------------------------------------------------------------
/lib/caching-router.ts:
--------------------------------------------------------------------------------
1 | import { dispose } from 'sigui'
2 |
3 | export function CachingRouter(routes: Record JSX.Element>) {
4 | const cache = new Map()
5 | let shouldDispose = false
6 | return function (pathname: string) {
7 | if (shouldDispose) {
8 | dispose()
9 | shouldDispose = false
10 | }
11 | if (('!' + pathname) in routes) {
12 | shouldDispose = true
13 | return routes['!' + pathname]()
14 | }
15 | if (!(pathname in routes)) return
16 | let el = cache.get(pathname)
17 | if (!el) cache.set(pathname, el = routes[pathname]())
18 | else requestAnimationFrame(() => el!.focus?.())
19 | return el
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/cn.ts:
--------------------------------------------------------------------------------
1 | export function cn(...args: any[]) {
2 | return () => {
3 | const classes = []
4 | for (const arg of args) {
5 | if (typeof arg === 'string') {
6 | classes.push(arg)
7 | }
8 | else if (typeof arg === 'function') {
9 | classes.push(arg())
10 | }
11 | else if (arg) {
12 | classes.push(Object.keys(arg).filter(k => {
13 | const fnOrBooley = arg[k]
14 | return typeof fnOrBooley === 'function' ? fnOrBooley() : fnOrBooley
15 | }).join(' '))
16 | }
17 | }
18 | return classes.join(' ')
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/happydom.ts:
--------------------------------------------------------------------------------
1 | import { GlobalRegistrator } from '@happy-dom/global-registrator'
2 |
3 | GlobalRegistrator.register({ url: 'https://localhost' })
4 |
--------------------------------------------------------------------------------
/lib/icon.ts:
--------------------------------------------------------------------------------
1 | import { createElement, type IconNode } from 'lucide'
2 |
3 | export function icon(i: IconNode, p: Record = { size: 16 }) {
4 | const svg = createElement(i)
5 | p.width = p.height = p.size
6 | for (const k in p) {
7 | svg.setAttribute(k, `${p[k]}`)
8 | }
9 | return svg
10 | }
11 |
--------------------------------------------------------------------------------
/lib/rpc.ts:
--------------------------------------------------------------------------------
1 | import type { Rpc } from '~/api/rpc/routes.ts'
2 | import { state } from '~/src/state.ts'
3 |
4 | export type RpcFn unknown> =
5 | T extends (first: never, ...rest: infer U) => infer V
6 | ? (...args: U) => V
7 | : never
8 |
9 | type RpcMethod = 'GET' | 'POST'
10 | type RpcResponse = {
11 | error?: string
12 | } | null | undefined
13 |
14 | const headers = { 'content-type': 'application/json' }
15 |
16 | export function rpc any>(
17 | method: RpcMethod,
18 | fn: string,
19 | ) {
20 | return async function (...args: unknown[]) {
21 | const url = new URL(`${state.apiUrl}rpc`)
22 |
23 | url.searchParams.set('fn', fn)
24 |
25 | const init: RequestInit = {
26 | method,
27 | headers,
28 | credentials: 'include',
29 | }
30 |
31 | switch (method) {
32 | case 'GET':
33 | for (const arg of args) {
34 | url.searchParams.append('args', JSON.stringify(arg))
35 | }
36 | break
37 |
38 | case 'POST':
39 | init.body = JSON.stringify({ args } satisfies Rpc)
40 | break
41 | }
42 |
43 | const res = await fetch(url, init)
44 |
45 | const json = await res.json() as RpcResponse
46 |
47 | if (json?.error) {
48 | throw new Error(json.error)
49 | }
50 |
51 | return json as never
52 | } as RpcFn
53 | }
54 |
--------------------------------------------------------------------------------
/lib/watcher.ts:
--------------------------------------------------------------------------------
1 | if (import.meta.env.DEV) {
2 | const url = location.origin.includes('devito')
3 | ? import.meta.env.VITE_API_URL + '/watcher'
4 | : Object.assign(new URL(location.origin), { port: 8000 }).href + 'watcher'
5 | const es = new EventSource(url)
6 | es.onopen = () => es.onopen = () => (location.href = location.href)
7 | }
8 |
--------------------------------------------------------------------------------
/migrations/1727778500969_user-table.ts:
--------------------------------------------------------------------------------
1 | import { sql, type Kysely } from 'kysely'
2 |
3 | export async function up(db: Kysely): Promise {
4 | // up migration code goes here...
5 | // note: up migrations are mandatory. you must implement this function.
6 | // For more info, see: https://kysely.dev/docs/migrations
7 | await db.schema
8 | .createTable('users')
9 | .ifNotExists()
10 | .addColumn('nick', 'text', col => col.primaryKey())
11 | .addColumn('email', 'text', col => col.unique().notNull())
12 | .addColumn('emailVerified', 'boolean', col => col.defaultTo(false))
13 | .addColumn('password', 'text')
14 | .addColumn('oauthGithub', 'boolean')
15 | .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull())
16 | .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull())
17 | .execute()
18 |
19 | await db.schema
20 | .createIndex('users_email_index')
21 | .ifNotExists()
22 | .on('users')
23 | .column('email')
24 | .execute()
25 | }
26 |
27 | export async function down(db: Kysely): Promise {
28 | // down migration code goes here...
29 | // note: down migrations are optional. you can safely delete this function.
30 | // For more info, see: https://kysely.dev/docs/migrations
31 | await db.schema
32 | .dropTable('users')
33 | .ifExists()
34 | .cascade()
35 | .execute()
36 | }
37 |
--------------------------------------------------------------------------------
/migrations/1729663196543_profiles-table.ts:
--------------------------------------------------------------------------------
1 | import { sql, type Kysely } from 'kysely'
2 |
3 | export async function up(db: Kysely): Promise {
4 | // up migration code goes here...
5 | // note: up migrations are mandatory. you must implement this function.
6 | // For more info, see: https://kysely.dev/docs/migrations
7 | await db.schema
8 | .createTable('profiles')
9 | .ifNotExists()
10 | .addColumn('ownerNick', 'text', col => col.references('users.nick').notNull())
11 | .addColumn('nick', 'text', col => col.primaryKey())
12 | .addColumn('displayName', 'text', col => col.notNull())
13 | .addColumn('bio', 'text')
14 | .addColumn('avatar', 'text')
15 | .addColumn('banner', 'text')
16 | .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull())
17 | .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull())
18 | .execute()
19 |
20 | await db.schema
21 | .createIndex('profiles_ownerNick_index')
22 | .ifNotExists()
23 | .on('profiles')
24 | .column('ownerNick')
25 | .execute()
26 |
27 | await db.schema
28 | .createIndex('profiles_displayName_index')
29 | .ifNotExists()
30 | .on('profiles')
31 | .column('displayName')
32 | .execute()
33 |
34 | await db.schema
35 | .alterTable('users')
36 | .addColumn('defaultProfile', 'text', col => col.references('profiles.nick'))
37 | .execute()
38 | }
39 |
40 | export async function down(db: Kysely): Promise {
41 | // down migration code goes here...
42 | // note: down migrations are optional. you can safely delete this function.
43 | // For more info, see: https://kysely.dev/docs/migrations
44 | await db.schema
45 | .dropTable('profiles')
46 | .ifExists()
47 | .cascade()
48 | .execute()
49 |
50 | await db.schema
51 | .alterTable('users')
52 | .dropColumn('defaultProfile')
53 | .execute()
54 | }
55 |
--------------------------------------------------------------------------------
/migrations/1729687226871_sounds-table.ts:
--------------------------------------------------------------------------------
1 | import { sql, type Kysely } from 'kysely'
2 |
3 | export async function up(db: Kysely): Promise {
4 | // up migration code goes here...
5 | // note: up migrations are mandatory. you must implement this function.
6 | // For more info, see: https://kysely.dev/docs/migrations
7 | await db.schema
8 | .createTable('sounds')
9 | .ifNotExists()
10 | .addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql`gen_random_uuid()`))
11 | .addColumn('ownerProfileNick', 'text', col => col.references('profiles.nick').notNull())
12 | .addColumn('title', 'text', col => col.notNull())
13 | .addColumn('code', 'text', col => col.notNull())
14 | .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull())
15 | .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull())
16 | .execute()
17 |
18 | await db.schema
19 | .createIndex('sounds_ownerProfileNick_index')
20 | .ifNotExists()
21 | .on('sounds')
22 | .column('ownerProfileNick')
23 | .execute()
24 |
25 | await db.schema
26 | .createIndex('sounds_title_index')
27 | .ifNotExists()
28 | .on('sounds')
29 | .column('title')
30 | .execute()
31 | }
32 |
33 | export async function down(db: Kysely): Promise {
34 | // down migration code goes here...
35 | // note: down migrations are optional. you can safely delete this function.
36 | // For more info, see: https://kysely.dev/docs/migrations
37 | await db.schema
38 | .dropTable('sounds')
39 | .ifExists()
40 | .cascade()
41 | .execute()
42 | }
43 |
--------------------------------------------------------------------------------
/migrations/1729758260210_favorites-table.ts:
--------------------------------------------------------------------------------
1 | import { sql, type Kysely } from 'kysely'
2 |
3 | export async function up(db: Kysely): Promise {
4 | await db.schema
5 | .createTable('favorites')
6 | .ifNotExists()
7 | .addColumn('profileNick', 'text', col => col.references('profiles.nick').onDelete('cascade').notNull())
8 | .addColumn('soundId', 'uuid', col => col.references('sounds.id').onDelete('cascade').notNull())
9 | .addColumn('createdAt', 'timestamp', col => col.defaultTo(sql`now()`).notNull())
10 | .addPrimaryKeyConstraint('favorites_pkey', ['profileNick', 'soundId'])
11 | .execute()
12 |
13 | // Index for faster lookups when querying a profile's favorites
14 | await db.schema
15 | .createIndex('favorites_profileId_idx')
16 | .ifNotExists()
17 | .on('favorites')
18 | .column('profileNick')
19 | .execute()
20 |
21 | // Index for faster lookups when querying who favorited a sound
22 | await db.schema
23 | .createIndex('favorites_soundId_idx')
24 | .ifNotExists()
25 | .on('favorites')
26 | .column('soundId')
27 | .execute()
28 | }
29 |
30 | export async function down(db: Kysely): Promise {
31 | await db.schema
32 | .dropTable('favorites')
33 | .ifExists()
34 | .cascade()
35 | .execute()
36 | }
37 |
--------------------------------------------------------------------------------
/migrations/1729771213713_sounds-remixOf-column.ts:
--------------------------------------------------------------------------------
1 | import type { Kysely } from 'kysely'
2 |
3 | export async function up(db: Kysely): Promise {
4 | // up migration code goes here...
5 | // note: up migrations are mandatory. you must implement this function.
6 | // For more info, see: https://kysely.dev/docs/migrations
7 |
8 | // Add remixOf column to sounds table
9 | await db.schema
10 | .alterTable('sounds')
11 | .addColumn('remixOf', 'uuid', col => col.references('sounds.id'))
12 | .execute()
13 | }
14 |
15 | export async function down(db: Kysely): Promise {
16 | // down migration code goes here...
17 | // note: down migrations are optional. you can safely delete this function.
18 | // For more info, see: https://kysely.dev/docs/migrations
19 | await db.schema
20 | .alterTable('sounds')
21 | .dropColumn('remixOf')
22 | .execute()
23 | }
24 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagas/ravescript/a2e44d68c974b852d517f5833934be6a1b87c04f/public/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/as-interop.js:
--------------------------------------------------------------------------------
1 | // KEEP: required for AssemblyScript interop.
2 | globalThis.unmanaged = () => {
3 | // noop
4 | setTimeout(() => { }, 1)
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagas/ravescript/a2e44d68c974b852d517f5833934be6a1b87c04f/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/maskable-icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagas/ravescript/a2e44d68c974b852d517f5833934be6a1b87c04f/public/maskable-icon-512x512.png
--------------------------------------------------------------------------------
/public/pwa-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagas/ravescript/a2e44d68c974b852d517f5833934be6a1b87c04f/public/pwa-192x192.png
--------------------------------------------------------------------------------
/public/pwa-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagas/ravescript/a2e44d68c974b852d517f5833934be6a1b87c04f/public/pwa-512x512.png
--------------------------------------------------------------------------------
/public/pwa-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stagas/ravescript/a2e44d68c974b852d517f5833934be6a1b87c04f/public/pwa-64x64.png
--------------------------------------------------------------------------------
/pwa-assets.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineConfig,
3 | minimal2023Preset as preset,
4 | } from '@vite-pwa/assets-generator/config'
5 |
6 | export default defineConfig({
7 | headLinkOptions: {
8 | preset: '2023',
9 | },
10 | preset,
11 | images: ['public/favicon.svg'],
12 | })
13 |
--------------------------------------------------------------------------------
/scripts/update-dsp-factory.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { basename, join } from 'path'
3 | import { capitalize, writeIfNotEqual } from '~/scripts/util.ts'
4 |
5 | const gensRoot = './as/assembly/dsp/gen'
6 | const files = fs.readdirSync(gensRoot).sort()
7 |
8 | const extendsRegExp = /extends\s([^\s]+)/
9 |
10 | let out: string[] = []
11 | out.push(`import { Engine } from '../../as/assembly/dsp/core/engine'`)
12 | const factories: string[] = []
13 | const ctors: string[] = []
14 | for (const file of files) {
15 | const base = basename(file, '.ts')
16 | const filename = join(gensRoot, file)
17 | const text = fs.readFileSync(filename, 'utf-8')
18 | const parentCtor = text.match(extendsRegExp)?.[1]
19 | const ctor = capitalize(base)
20 | ctors.push(`'${ctor}'`)
21 | const factory = `create${ctor}`
22 | out.push(`import { ${ctor} } from '../.${gensRoot}/${base}'`)
23 | if (['osc', 'gen'].includes(base)) {
24 | factories.push(`createZero`) // dummy because they are abstract
25 | continue
26 | }
27 | factories.push(factory)
28 | out.push(`function ${factory}(engine: Engine): ${ctor} { return new ${ctor}(engine) }`)
29 | }
30 |
31 | out.push(`export const Factory: ((engine: Engine) => Gen)[] = [${factories}]`)
32 | out.push(`export const Ctors: string[] = [${ctors}]`)
33 |
34 | const targetPath = './generated/assembly/dsp-factory.ts'
35 | const text = out.join('\n')
36 | writeIfNotEqual(targetPath, text)
37 |
38 | console.log('done update-dsp-factory.')
39 |
--------------------------------------------------------------------------------
/scripts/update-gens-offsets.ts:
--------------------------------------------------------------------------------
1 | import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts'
2 | import { capitalize, writeIfNotEqual } from '~/scripts/util.ts'
3 | import { getAllPropsDetailed } from '~/src/as/dsp/util.ts'
4 |
5 | let out: string[] = []
6 | const offsets: string[] = []
7 | for (const k in dspGens) {
8 | const props = getAllPropsDetailed(k as keyof Gen)
9 | out.push(`import { ${capitalize(k)} } from '../../as/assembly/dsp/gen/${k.toLowerCase()}'`)
10 | offsets.push(` [${props.map(x => `offsetof<${capitalize(x.ctor)}>('${x.name}')`)}]`)
11 | }
12 |
13 | out.push('export const Offsets: usize[][] = [')
14 | out.push(offsets.join(',\n'))
15 | out.push(']')
16 |
17 | const targetPath = './generated/assembly/dsp-offsets.ts'
18 | const text = out.join('\n')
19 | writeIfNotEqual(targetPath, text)
20 |
21 | console.log('done update-gens-offsets.')
22 |
--------------------------------------------------------------------------------
/scripts/util.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 |
3 | export const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1)
4 |
5 | export function writeIfNotEqual(filename: string, text: string): void {
6 | let existingText = ''
7 |
8 | try {
9 | existingText = fs.readFileSync(filename, 'utf-8')
10 | }
11 | catch (e) {
12 | const error: NodeJS.ErrnoException = e as any
13 | if (error.code !== 'ENOENT') {
14 | throw error
15 | }
16 | }
17 |
18 | if (existingText !== text) {
19 | fs.writeFileSync(filename, text, 'utf-8')
20 | console.log(`File "${filename}" ${existingText ? 'updated' : 'created'}.`)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ambient.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
6 | declare module '*?raw-hex' {
7 | const src: string
8 | export default src
9 | }
10 |
--------------------------------------------------------------------------------
/src/as/dsp/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEBUG = false
2 |
--------------------------------------------------------------------------------
/src/as/dsp/index.ts:
--------------------------------------------------------------------------------
1 | export * from '~/as/assembly/dsp/constants.ts'
2 | export * from './build.ts'
3 | export * from './constants.ts'
4 | export * from './node.ts'
5 | export * from './preview-service.ts'
6 | export * from './shared.ts'
7 | export * from './util.ts'
8 | export * from './value.ts'
9 | export * from './wasm.ts'
10 |
--------------------------------------------------------------------------------
/src/as/dsp/notes-shared.ts:
--------------------------------------------------------------------------------
1 | import { Struct } from 'utils'
2 |
3 | export interface Note {
4 | n: number
5 | time: number
6 | length: number
7 | vel: number
8 | }
9 |
10 | export type NoteView = ReturnType
11 |
12 | export const NoteView = Struct({
13 | n: 'f32',
14 | time: 'f32',
15 | length: 'f32',
16 | vel: 'f32',
17 | })
18 |
--------------------------------------------------------------------------------
/src/as/dsp/params-shared.ts:
--------------------------------------------------------------------------------
1 | import { Struct } from 'utils'
2 |
3 | export interface ParamValue {
4 | time: number
5 | length: number
6 | slope: number
7 | amt: number
8 | }
9 |
10 | export type ParamValueView = ReturnType
11 |
12 | export const ParamValueView = Struct({
13 | time: 'f32',
14 | length: 'f32',
15 | slope: 'f32',
16 | amt: 'f32',
17 | })
18 |
--------------------------------------------------------------------------------
/src/as/dsp/pre-post.ts:
--------------------------------------------------------------------------------
1 | import { tokenize } from '~/src/lang/tokenize.ts'
2 |
3 | export const preTokens = Array.from(tokenize({
4 | // we implicit call [nrate 1] before our code
5 | // so that the sample rate is reset.
6 | code: ` [nrate 1] `
7 | // some builtin procedures
8 | // + ` { .5* .5+ } norm= `
9 | // + ` { at= p= sp= 1 [inc sp co* at] clip - p^ } dec= `
10 | + ` { x= 2 x 69 - 12 / ^ 440 * } ntof= `
11 | + ` [zero] `
12 | }))
13 |
14 | export const postTokens = Array.from(tokenize({
15 | code: `@`
16 | }))
17 |
--------------------------------------------------------------------------------
/src/as/dsp/preview-service.ts:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { Deferred, getMemoryView, rpc, type MemoryView } from 'utils'
3 | import type { PreviewWorker } from './preview-worker.ts'
4 | import PreviewWorkerFactory from './preview-worker.ts?worker'
5 |
6 | export type PreviewService = ReturnType
7 |
8 | export function PreviewService(ctx: AudioContext) {
9 | using $ = Sigui()
10 |
11 | const deferred = Deferred()
12 | const isReady = deferred.promise
13 | const worker = new PreviewWorkerFactory()
14 | const service = rpc(worker, {
15 | async isReady() {
16 | deferred.resolve()
17 | }
18 | })
19 |
20 | const info = $({
21 | isReady: null as null | true,
22 | dsp: null as null | Awaited>,
23 | view: null as null | MemoryView
24 | })
25 |
26 | isReady.then(() => {
27 | info.isReady = true
28 | })
29 |
30 | $.fx(() => {
31 | const { isReady } = $.of(info)
32 | $().then(async () => {
33 | const dsp = await service.createDsp(ctx.sampleRate)
34 | const view = getMemoryView(dsp.memory)
35 | $.batch(() => {
36 | info.dsp = dsp
37 | info.view = view
38 | })
39 | })
40 | })
41 |
42 | function dispose() {
43 | worker.terminate()
44 | }
45 |
46 | return { info, isReady, service, dispose }
47 | }
48 |
--------------------------------------------------------------------------------
/src/as/dsp/rms.ts:
--------------------------------------------------------------------------------
1 | import { instantiate } from '~/as/build/rms.js'
2 | import url from '~/as/build/rms.wasm?url'
3 | import { hexToBinary } from '~/src/as/init-wasm.ts'
4 |
5 | let mod: WebAssembly.Module
6 |
7 | if (import.meta.env && import.meta.env.MODE !== 'production') {
8 | const hex = (await import('~/as/build/rms.wasm?raw-hex')).default
9 | const wasmMapUrl = new URL('/as/build/rms.wasm.map', location.origin).href
10 | const binary = hexToBinary(hex, wasmMapUrl)
11 | mod = await WebAssembly.compile(binary)
12 | }
13 | else {
14 | mod = await WebAssembly.compileStreaming(fetch(new URL(url, location.href)))
15 | }
16 |
17 | export const wasm = await instantiate(mod, {
18 | env: {
19 | log: console.log,
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/src/as/dsp/shared.ts:
--------------------------------------------------------------------------------
1 | import { Struct } from 'utils'
2 |
3 | export const enum DspWorkletMode {
4 | Idle,
5 | Reset,
6 | Stop,
7 | Play,
8 | Pause,
9 | }
10 |
11 | export type Clock = typeof Clock.type
12 | export const Clock = Struct({
13 | time: 'f64',
14 | timeStep: 'f64',
15 | prevTime: 'f64',
16 | startTime: 'f64',
17 | endTime: 'f64',
18 | bpm: 'f64',
19 | coeff: 'f64',
20 | barTime: 'f64',
21 | barTimeStep: 'f64',
22 | loopStart: 'f64',
23 | loopEnd: 'f64',
24 | sampleRate: 'u32',
25 | jumpBar: 'i32',
26 | ringPos: 'u32',
27 | nextRingPos: 'u32',
28 | })
29 |
30 | export type Track = typeof Track.type
31 | export const Track = Struct({
32 | run_ops$: 'usize',
33 | setup_ops$: 'usize',
34 | literals$: 'usize',
35 | lists$: 'usize',
36 | audio_LR$: 'i32',
37 | })
38 |
39 | export type Out = typeof Out.type
40 | export const Out = Struct({
41 | L$: 'usize',
42 | R$: 'usize',
43 | })
44 |
45 | export type SoundValue = typeof SoundValue.type
46 | export const SoundValue = Struct({
47 | kind: 'i32',
48 | ptr: 'i32',
49 | scalar$: 'i32',
50 | audio$: 'i32',
51 | })
52 |
--------------------------------------------------------------------------------
/src/as/dsp/util.ts:
--------------------------------------------------------------------------------
1 | import { ValuesOf } from 'utils'
2 | import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts'
3 |
4 | export function getAllProps(k: keyof Gen) {
5 | const gen = dspGens[k]
6 | const props: ValuesOf<(typeof dspGens)[keyof Gen]['props']>[] = []
7 | if ('inherits' in gen && gen.inherits) {
8 | props.push(...getAllProps(gen.inherits))
9 | }
10 | props.push(...gen.props)
11 | return props
12 | }
13 |
14 | export function getAllPropsReverse(k: keyof Gen) {
15 | const gen = dspGens[k]
16 | const props: ValuesOf<(typeof dspGens)[keyof Gen]['props']>[] = []
17 | props.push(...gen.props)
18 | if ('inherits' in gen && gen.inherits) {
19 | props.push(...getAllPropsReverse(gen.inherits))
20 | }
21 | return props
22 | }
23 |
24 | export function getAllPropsDetailed(k: keyof Gen) {
25 | const gen = dspGens[k]
26 | const props: { name: ValuesOf<(typeof dspGens)[keyof Gen]['props']>, ctor: typeof k }[] = []
27 | if ('inherits' in gen && gen.inherits) {
28 | props.push(...getAllPropsDetailed(gen.inherits))
29 | }
30 | props.push(...gen.props.map(x => ({ name: x, ctor: k })))
31 | return props
32 | }
33 |
--------------------------------------------------------------------------------
/src/as/dsp/wasm.ts:
--------------------------------------------------------------------------------
1 | import { instantiate } from '~/as/build/dsp.js'
2 | import url from '~/as/build/dsp.wasm?url'
3 | import { hexToBinary, initWasm } from '~/src/as/init-wasm.ts'
4 |
5 | let mod: WebAssembly.Module
6 |
7 | if (import.meta.env && import.meta.env.MODE !== 'production') {
8 | const hex = (await import('~/as/build/dsp.wasm?raw-hex')).default
9 | const wasmMapUrl = new URL('/as/build/dsp.wasm.map', location.origin).href
10 | const binary = hexToBinary(hex, wasmMapUrl)
11 | mod = await WebAssembly.compile(binary)
12 | }
13 | else {
14 | mod = await WebAssembly.compileStreaming(fetch(new URL(url, location.href)))
15 | }
16 |
17 | const wasmInstance = await instantiate(mod, {
18 | env: {
19 | log: console.warn,
20 | }
21 | })
22 |
23 | const { alloc } = initWasm(wasmInstance)
24 |
25 | export const wasm = Object.assign(wasmInstance, { alloc })
26 |
--------------------------------------------------------------------------------
/src/as/gfx/anim.ts:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { AnimMode } from '~/src/constants.ts'
3 | import { state } from '~/src/state.ts'
4 |
5 | const DEBUG = false //true
6 |
7 | export type Anim = ReturnType
8 |
9 | export function Anim() {
10 | DEBUG && console.log('[anim] create')
11 | using $ = Sigui()
12 |
13 | const Modes = Object.values(AnimMode)
14 |
15 | const info = $({
16 | isRunning: false,
17 | mode: state.$.animMode,
18 | epoch: 0,
19 | })
20 |
21 | const ticks = new Set<() => boolean | void>()
22 |
23 | let lastEpoch = -1 // cause initial draw to happen
24 | let animFrame: any
25 |
26 | const tick = $.fn(function tick() {
27 | DEBUG && console.log('[anim] tick', info.epoch)
28 |
29 | if (info.epoch === lastEpoch && info.mode === AnimMode.Auto) {
30 | info.isRunning = false
31 | DEBUG && console.log('[anim] exit')
32 | return
33 | }
34 |
35 | lastEpoch = info.epoch
36 |
37 | for (const tick of ticks) {
38 | if (tick()) info.epoch++
39 | }
40 |
41 | animFrame = requestAnimationFrame(tick)
42 | })
43 |
44 | function cycle() {
45 | info.mode = Modes[
46 | (Modes.indexOf(info.mode) + 1) % Modes.length
47 | ]
48 | }
49 |
50 | function stop() {
51 | cancelAnimationFrame(animFrame)
52 | info.isRunning = false
53 | }
54 |
55 | function start() {
56 | if (info.isRunning) return
57 | stop()
58 | info.isRunning = true
59 | animFrame = requestAnimationFrame(tick)
60 | }
61 |
62 | $.fx(() => {
63 | const { epoch, mode } = info
64 | $()
65 | DEBUG && console.log('[anim]', mode, epoch)
66 | if (mode === AnimMode.Off) {
67 | stop()
68 | }
69 | else {
70 | start()
71 | }
72 | })
73 |
74 | $.fx(() => () => {
75 | DEBUG && console.log('[anim] dispose')
76 | stop()
77 | })
78 |
79 | state.animCycle = cycle
80 |
81 | return { info, ticks, cycle }
82 | }
83 |
--------------------------------------------------------------------------------
/src/as/gfx/gfx.ts:
--------------------------------------------------------------------------------
1 | import { Matrix, Meshes, Rect, Shapes, Sketch } from 'gfx'
2 | import { initGL } from 'gl-util'
3 | import { Sigui } from 'sigui'
4 |
5 | const DEBUG = true
6 |
7 | export function Gfx({ canvas }: {
8 | canvas: HTMLCanvasElement
9 | }) {
10 | using $ = Sigui()
11 |
12 | const GL = initGL(canvas, {
13 | antialias: true,
14 | alpha: true,
15 | preserveDrawingBuffer: true
16 | })
17 |
18 | function createContext(view: Rect, matrix: Matrix) {
19 | const meshes = Meshes(GL, view)
20 | const sketch = Sketch(GL, view)
21 | meshes.add($, sketch)
22 |
23 | function createShapes() {
24 | return Shapes(view, matrix)
25 | }
26 |
27 | return { meshes, sketch, createShapes }
28 | }
29 |
30 | $.fx(() => () => {
31 | DEBUG && console.debug('[gfx] dispose')
32 | GL.reset()
33 | })
34 |
35 | return {
36 | createContext
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/as/gfx/index.ts:
--------------------------------------------------------------------------------
1 | export * from './anim.ts'
2 | export * from './gfx.ts'
3 | export * from './glsl.ts'
4 | export * from './mesh-info.ts'
5 | export * from './meshes.ts'
6 | export * from './shapes.ts'
7 | export * from './sketch-info.ts'
8 | export * from './sketch.ts'
9 | export * from './types.ts'
10 | export * from './wasm-matrix.ts'
11 | export * from './wasm.ts'
12 |
13 |
--------------------------------------------------------------------------------
/src/as/gfx/mesh-info.ts:
--------------------------------------------------------------------------------
1 | import { GL, GLBuffer, GLBufferTarget } from 'gl-util'
2 | import { Sigui } from 'sigui'
3 |
4 | export interface MeshSetup<
5 | U extends Parameters[0]
6 | > {
7 | vertex: string | ((gl?: WebGL2RenderingContext) => string)
8 | fragment: string | ((gl?: WebGL2RenderingContext) => string)
9 | vao?: U
10 | }
11 |
12 | export type MeshInfo = ReturnType
13 |
14 | export function MeshInfo<
15 | T extends GLBufferTarget,
16 | U extends Parameters[0]
17 | >(GL: GL, setup: MeshSetup) {
18 | using $ = Sigui()
19 |
20 | const { gl } = GL
21 |
22 | const shaders = GL.createShaders(setup)
23 | const program = GL.createProgram(shaders)
24 |
25 | let use = () => GL.useProgram(program)
26 | let useProgram = () => GL.useProgram(program)
27 |
28 | const info = {
29 | program,
30 | use,
31 | useProgram,
32 | vao: undefined as WebGLVertexArrayObject | undefined,
33 | attribs: {} as { [K in keyof U]: GLBuffer> },
34 | get uniforms() {
35 | GL.useProgram(program)
36 | return GL.uniforms
37 | }
38 | }
39 |
40 | if (setup.vao) {
41 | info.vao = GL.createVertexArray()
42 | info.attribs = GL.addVertexAttribs(setup.vao) as any
43 | info.use = () => GL.use(program, info.vao!)
44 | info.useProgram = () => GL.useProgram(program)
45 | }
46 |
47 | $.fx(() => () => {
48 | GL.deleteShaders(shaders)
49 | gl.deleteProgram(program)
50 | if (info.vao) {
51 | gl.deleteVertexArray(info.vao)
52 | GL.deleteAttribs(info.attribs!)
53 | }
54 | })
55 |
56 | return info
57 | }
58 |
--------------------------------------------------------------------------------
/src/as/gfx/meshes.ts:
--------------------------------------------------------------------------------
1 | import type { Rect } from 'gfx'
2 | import { GL } from 'gl-util'
3 | import { Sigui } from 'sigui'
4 |
5 | const DEBUG = false
6 |
7 | export interface MeshProps {
8 | GL: GL
9 | view: Rect
10 | }
11 |
12 | export interface Mesh {
13 | draw(): void
14 | }
15 |
16 | export function Meshes(GL: GL, view: Rect) {
17 | DEBUG && console.debug('[meshes] create')
18 | using $ = Sigui()
19 |
20 | const { gl, canvas } = GL
21 | const meshes = new Set()
22 |
23 | function clear() {
24 | const x = view.x_pr
25 | const y = canvas.height - view.h_pr - view.y_pr
26 | const w = Math.max(0, view.w_pr)
27 | const h = Math.max(0, view.h_pr)
28 | gl.viewport(x, y, w, h)
29 | gl.scissor(x, y, w, h)
30 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
31 | }
32 |
33 | function draw() {
34 | DEBUG && console.debug('[meshes] draw', meshes.size)
35 | clear()
36 | for (const mesh of meshes) {
37 | mesh.draw()
38 | }
39 | }
40 |
41 | function add($: Sigui, mesh: Mesh) {
42 | $.fx(() => {
43 | meshes.add(mesh)
44 | return () => {
45 | meshes.delete(mesh)
46 | }
47 | })
48 | }
49 |
50 | $.fx(() => () => {
51 | DEBUG && console.debug('[meshes] clear')
52 | meshes.clear()
53 | })
54 |
55 | return { GL, draw, add }
56 | }
57 |
--------------------------------------------------------------------------------
/src/as/gfx/sketch.ts:
--------------------------------------------------------------------------------
1 | import { SketchInfo, wasm, type Rect, type Shapes } from 'gfx'
2 | import { GL } from 'gl-util'
3 |
4 | const DEBUG = false
5 |
6 | export type Sketch = ReturnType
7 |
8 | export function Sketch(GL: GL, view: Rect) {
9 | const sketch = SketchInfo(GL, view)
10 | const scene = new Set()
11 |
12 | const { gl } = GL
13 | const { info, finish, writeGL, draw: sketchDraw } = sketch
14 | const { use } = info
15 |
16 | function flush(count: number) {
17 | DEBUG && console.log('[sketch] flush', count)
18 | writeGL(count)
19 | gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count)
20 | }
21 |
22 | function draw() {
23 | use()
24 | wasm.setFlushSketchFn(flush)
25 | DEBUG && console.log('[sketch] draw', scene.size)
26 | for (const shapes of scene) {
27 | sketchDraw(shapes)
28 | }
29 | finish()
30 | }
31 |
32 | return { draw, scene, info, view }
33 | }
34 |
--------------------------------------------------------------------------------
/src/as/gfx/types.ts:
--------------------------------------------------------------------------------
1 | import { $, type Signal } from 'sigui'
2 | import { screen } from '~/src/screen.ts'
3 |
4 | export type Matrix = ReturnType
5 |
6 | class MatrixInfo {
7 | a = 1
8 | b = 0
9 | c = 0
10 | d = 1
11 | e = 0
12 | f = 0
13 | _values?: [
14 | sx: number, cy: number,
15 | cx: number, sy: number,
16 | tx: number, ty: number,
17 | ]
18 | get values() {
19 | const { a, b, c, d, e, f } = this
20 | const o = (this._values ??= [
21 | 1, 0, 0,
22 | 1, 0, 0,
23 | ])
24 | o[0] = a
25 | o[1] = b
26 | o[2] = c
27 | o[3] = d
28 | o[4] = e
29 | o[5] = f
30 | return this._values
31 | }
32 | }
33 |
34 | export function Matrix(
35 | a: number | Signal = 1,
36 | b: number | Signal = 0,
37 | c: number | Signal = 0,
38 | d: number | Signal = 1,
39 | e: number | Signal = 0,
40 | f: number | Signal = 0,
41 | ) {
42 | return $(new MatrixInfo(), { a, b, c, d, e, f })
43 | }
44 |
45 | export type Rect = ReturnType
46 |
47 | class RectInfo {
48 | pr = 0
49 | x = 0
50 | y = 0
51 | w = 0
52 | h = 0
53 | get x_pr() { return this.x * this.pr }
54 | get y_pr() { return this.y * this.pr }
55 | get w_pr() { return this.w * this.pr }
56 | get h_pr() { return this.h * this.pr }
57 | width = $.alias(this, 'w')
58 | height = $.alias(this, 'h')
59 | }
60 |
61 | export function Rect(
62 | x: number | Signal = 0,
63 | y: number | Signal = 0,
64 | w: number | Signal = 0,
65 | h: number | Signal = 0,
66 | ) {
67 | return $(new RectInfo(), {
68 | pr: screen.$.pr,
69 | x, y, w, h
70 | })
71 | }
72 |
--------------------------------------------------------------------------------
/src/as/gfx/wasm-matrix.ts:
--------------------------------------------------------------------------------
1 | import { wasm, type Matrix, type Rect } from 'gfx'
2 | import { Sigui } from 'sigui'
3 |
4 | const DEBUG = false
5 |
6 | export type WasmMatrix = ReturnType
7 |
8 | export function WasmMatrix(view: Rect, matrix: Matrix) {
9 | using $ = Sigui()
10 |
11 | const mat2d = new Float64Array(wasm.memory.buffer, wasm.createMatrix(), 6)
12 |
13 | $.fx(() => {
14 | const { a, d, e, f } = matrix
15 | const { pr, h } = view
16 | $()
17 | mat2d.set(matrix.values)
18 | DEBUG && console.log(a)
19 | })
20 |
21 | return mat2d
22 | }
23 |
--------------------------------------------------------------------------------
/src/as/gfx/wasm.ts:
--------------------------------------------------------------------------------
1 | import { instantiate } from '~/as/build/gfx.js'
2 | import url from '~/as/build/gfx.wasm?url'
3 | import { hexToBinary, initWasm } from '~/src/as/init-wasm.ts'
4 |
5 | const DEBUG = false
6 |
7 | let mod: WebAssembly.Module
8 |
9 | if (import.meta.env && import.meta.env.MODE !== 'production') {
10 | const hex = (await import('~/as/build/gfx.wasm?raw-hex')).default
11 | const wasmMapUrl = new URL('/as/build/gfx.wasm.map', location.origin).href
12 | const binary = hexToBinary(hex, wasmMapUrl)
13 | mod = await WebAssembly.compile(binary)
14 | }
15 | else {
16 | mod = await WebAssembly.compileStreaming(fetch(new URL(url, location.href)))
17 | }
18 |
19 | let flushSketchFn = (count: number) => { }
20 | function setFlushSketchFn(fn: (count: number) => void) {
21 | flushSketchFn = fn
22 | }
23 |
24 | const wasmInstance = await instantiate(mod, {
25 | env: {
26 | log: console.log,
27 | flushSketch(count: number) {
28 | DEBUG && console.debug('[flush]', count)
29 | flushSketchFn(count)
30 | }
31 | }
32 | })
33 |
34 | const { alloc } = initWasm(wasmInstance)
35 |
36 | export const wasm = Object.assign(wasmInstance, { alloc, setFlushSketchFn })
37 |
--------------------------------------------------------------------------------
/src/as/pkg/service.ts:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { Deferred, rpc } from 'utils'
3 | import type { PkgWorker } from '~/src/as/pkg/worker.ts'
4 | import PkgWorkerFactory from '~/src/as/pkg/worker.ts?worker'
5 |
6 | export type PkgService = ReturnType
7 |
8 | export function PkgService() {
9 | using $ = Sigui()
10 |
11 | const deferred = Deferred()
12 | const ready = deferred.promise
13 | const worker = new PkgWorkerFactory()
14 |
15 | const service = rpc(worker, {
16 | async isReady() {
17 | deferred.resolve()
18 | }
19 | })
20 |
21 | const info = $({
22 | pkg: $.unwrap(() => ready.then(() => service.create()))
23 | })
24 |
25 | function terminate() {
26 | worker.terminate()
27 | console.log('[pkg-worker] terminated')
28 | }
29 |
30 | return { info, ready, service, worker, terminate }
31 | }
32 |
--------------------------------------------------------------------------------
/src/as/pkg/shared.ts:
--------------------------------------------------------------------------------
1 | import { Struct } from 'utils'
2 |
3 | export const enum PlayerMode {
4 | Idle,
5 | Reset,
6 | Stop,
7 | Play,
8 | Pause,
9 | }
10 |
11 | export const Out = Struct({
12 | L$: 'usize',
13 | R$: 'usize',
14 | })
15 |
16 | export const PlayerTrack = Struct({
17 | len: 'u32',
18 | offset: 'i32',
19 | coeff: 'f64',
20 | floats_L$: 'usize',
21 | floats_R$: 'usize',
22 | floats_LR$: 'usize',
23 | out_L$: 'usize',
24 | out_R$: 'usize',
25 | out_LR$: 'usize',
26 | pan: 'f32',
27 | vol: 'f32',
28 | })
29 |
30 | export type BarBox = ReturnType
31 |
32 | export const BarBox = Struct({
33 | timeBegin: 'f64',
34 | pt$: 'usize',
35 | })
36 |
--------------------------------------------------------------------------------
/src/as/pkg/wasm.ts:
--------------------------------------------------------------------------------
1 | import { instantiate } from '~/as/build/pkg.js'
2 | import url from '~/as/build/pkg.wasm?url'
3 | import { hexToBinary, initWasm } from '~/src/as/init-wasm.ts'
4 |
5 | let mod: WebAssembly.Module
6 |
7 | if (import.meta.env && import.meta.env.MODE !== 'production') {
8 | const hex = (await import('~/as/build/pkg.wasm?raw-hex')).default
9 | const wasmMapUrl = new URL('/as/build/pkg.wasm.map', location.origin).href
10 | const binary = hexToBinary(hex, wasmMapUrl)
11 | mod = await WebAssembly.compile(binary)
12 | }
13 | else {
14 | mod = await WebAssembly.compileStreaming(fetch(new URL(url, location.href)))
15 | }
16 |
17 | const wasm = await instantiate(mod, {
18 | env: {
19 | log: console.log,
20 | }
21 | })
22 |
23 | const { alloc } = initWasm(wasm)
24 |
25 | export default Object.assign(wasm, { alloc })
26 |
--------------------------------------------------------------------------------
/src/as/pkg/worker.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | self.document = {
3 | querySelectorAll() { return [] as any },
4 | baseURI: location.origin
5 | }
6 |
7 | import { rpc } from 'utils'
8 | import pkg from './wasm.ts'
9 |
10 | export type PkgWorker = typeof worker
11 |
12 | const worker = {
13 | async create() {
14 | return {
15 | memory: pkg.memory,
16 | }
17 | },
18 | async multiply(a: number, b: number) {
19 | console.log('multiply', a, b)
20 | return pkg.multiply(a, b)
21 | }
22 | }
23 |
24 | const host = rpc<{ isReady(): void }>(self, worker)
25 | host.isReady()
26 | console.log('[pkg-worker] started')
27 |
--------------------------------------------------------------------------------
/src/client.tsx:
--------------------------------------------------------------------------------
1 | import '~/lib/watcher.ts'
2 |
3 | import { cleanup, hmr, mount } from 'sigui'
4 | import { App } from '~/src/pages/App.tsx'
5 | import { setState, state } from '~/src/state.ts'
6 |
7 | export const start = mount('#container', target => {
8 | state.container = target
9 | target.replaceChildren( as HTMLElement)
10 | return cleanup
11 | })
12 |
13 | if (import.meta.hot) {
14 | import.meta.hot.accept(hmr(start, state, setState))
15 | }
16 | else {
17 | start()
18 | }
19 |
--------------------------------------------------------------------------------
/src/comp/AnimMode.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '~/lib/cn.ts'
2 | import type { Anim } from '~/src/as/gfx/anim.ts'
3 | import { state } from '~/src/state.ts'
4 | import { Button } from '~/src/ui/Button.tsx'
5 |
6 | export function AnimMode({ anim }: { anim: Anim }) {
7 | return
8 |
anim:
9 |
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/comp/AuthModal.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from 'sigui'
2 | import { LoginOrRegister } from '~/src/comp/LoginOrRegister.tsx'
3 | import { state } from '~/src/state.ts'
4 |
5 | export function showAuthModal() {
6 | state.modalIsCancelled = false
7 |
8 | const off = $.fx(() => {
9 | const { modalIsCancelled } = state
10 | if (modalIsCancelled) {
11 | off()
12 | return
13 | }
14 | const { user } = state
15 | $()
16 | if (user == null) {
17 | state.modal =
18 | state.modalIsOpen = true
19 | state.modalIsCancelled = false
20 | }
21 | else {
22 | state.modal = null
23 | state.modalIsOpen = false
24 | queueMicrotask(() => off())
25 | }
26 | })
27 | }
28 |
29 | export function wrapActionAuth(fn: () => void) {
30 | return () => {
31 | if (state.user) return fn()
32 |
33 | showAuthModal()
34 |
35 | const off = $.fx(() => {
36 | const { modalIsCancelled } = state
37 | if (modalIsCancelled) {
38 | off()
39 | return
40 | }
41 | const { user, favorites } = $.of(state)
42 | $()
43 | fn()
44 | off()
45 | })
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/comp/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from 'ui'
2 |
3 | export function Header({ children }: { children?: any }) {
4 | return
5 |
6 |
7 | {children}
8 |
9 |
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/comp/HelpModal.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from 'sigui'
2 | import { Help } from '~/src/comp/Help.tsx'
3 | import { state } from '~/src/state.ts'
4 |
5 | export function showHelpModal() {
6 | state.modalIsCancelled = false
7 |
8 | const off = $.fx(() => {
9 | const { modalIsCancelled } = state
10 | if (modalIsCancelled) {
11 | off()
12 | return
13 | }
14 | const { user } = state
15 | $()
16 | if (user == null) {
17 | state.modal =
18 | state.modalIsOpen = true
19 | state.modalIsCancelled = false
20 | }
21 | else {
22 | state.modal = null
23 | state.modalIsOpen = false
24 | queueMicrotask(() => off())
25 | }
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/src/comp/LoginOrRegister.tsx:
--------------------------------------------------------------------------------
1 | import { Login } from '~/src/comp/Login.tsx'
2 | import { OAuthLogin } from '~/src/comp/OAuthLogin.tsx'
3 | import { Register } from '~/src/comp/Register.tsx'
4 |
5 | export function LoginOrRegister() {
6 | return
7 |
8 |
9 | or
10 |
11 |
12 |
13 |
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/comp/OAuthLogin.tsx:
--------------------------------------------------------------------------------
1 | import { on } from 'utils'
2 | import { loginUserSession, maybeLogin, whoami } from '~/src/rpc/auth.ts'
3 | import { Button } from '~/src/ui/index.ts'
4 |
5 | export function OAuthLogin() {
6 | function oauthLogin(provider: string) {
7 | const h = 700
8 | const w = 500
9 | const x = window.outerWidth / 2 + window.screenX - (w / 2)
10 | const y = window.outerHeight / 2 + window.screenY - (h / 2)
11 |
12 | const url = new URL(`${location.origin}/oauth/popup`)
13 | url.searchParams.set('provider', provider)
14 | const popup = window.open(
15 | url,
16 | 'oauth',
17 | `width=${w}, height=${h}, top=${y}, left=${x}`
18 | )
19 |
20 | if (!popup) alert('Something went wrong')
21 |
22 | on(window, 'storage', () => {
23 | popup!.close()
24 | if (localStorage.oauth?.startsWith('complete')) {
25 | maybeLogin()
26 | }
27 | else {
28 | alert('OAuth failed.\n\nTry logging in using a different method.')
29 | }
30 | }, { once: true })
31 | }
32 |
33 | return
36 | }
37 |
--------------------------------------------------------------------------------
/src/comp/Register.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { UserRegister } from '~/api/auth/types.ts'
3 | import * as actions from '~/src/rpc/auth.ts'
4 | import { Button, Fieldset, Input, Label } from '~/src/ui/index.ts'
5 | import { parseForm } from '~/src/util/parse-form.ts'
6 |
7 | export function Register() {
8 | using $ = Sigui()
9 |
10 | const info = $({
11 | error: ''
12 | })
13 |
14 | function onSubmit(ev: Event & { target: HTMLFormElement }) {
15 | ev.preventDefault()
16 | actions
17 | .register(parseForm(ev.target, UserRegister))
18 | .then(actions.loginUserSession)
19 | .catch(err => info.error = err.message)
20 | return false
21 | }
22 |
23 | return
59 | }
60 |
--------------------------------------------------------------------------------
/src/comp/ResetPassword.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { UserResetPassword } from '~/api/auth/types.ts'
3 | import * as actions from '~/src/rpc/auth.ts'
4 | import { go } from '~/src/ui/Link.tsx'
5 | import { parseForm } from '~/src/util/parse-form.ts'
6 |
7 | export function ResetPassword() {
8 | using $ = Sigui()
9 |
10 | const info = $({
11 | nick: null as null | string,
12 | error: ''
13 | })
14 |
15 | function onSubmit(ev: Event & { target: HTMLFormElement }) {
16 | ev.preventDefault()
17 | const { token, password } = parseForm(ev.target, UserResetPassword)
18 | actions
19 | .changePassword(token, password)
20 | .then(session => {
21 | go('/')
22 | actions.loginUserSession(session)
23 | })
24 | .catch(err => info.error = err.message)
25 | return false
26 | }
27 |
28 | const token = new URLSearchParams(location.search).get('token')
29 | if (!token) return Token not found
30 |
31 | actions
32 | .getResetPasswordUserNick(token)
33 | .then(nick => info.nick = nick)
34 | .catch(err => info.error = err.message)
35 |
36 | return {() => info.nick ?
37 |
Reset Password
38 |
39 | Hello {info.nick}! Please enter your new password below:
40 |
41 | : info.error ? info.error : 'Loading...'}
54 | }
55 |
--------------------------------------------------------------------------------
/src/comp/Toast.tsx:
--------------------------------------------------------------------------------
1 | import { state } from '~/src/state.ts'
2 |
3 | export function Toast() {
4 | return
5 | {() => state.toastMessages.length ?
6 | {() => state.toastMessages.map(item =>
7 |
{
8 | state.toastMessages = state.toastMessages.filter(i => i !== item)
9 | }}>{item.stack ?? item.message}
10 | )}
11 |
:
}
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/comp/VerifyEmail.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import * as actions from '~/src/rpc/auth.ts'
3 |
4 | export function VerifyEmail() {
5 | using $ = Sigui()
6 |
7 | const info = $({
8 | isVerified: false,
9 | error: ''
10 | })
11 |
12 | const token = new URLSearchParams(location.search).get('token')
13 | if (!token) return Token not found
14 |
15 | actions
16 | .verifyEmail(token)
17 | .then(() => info.isVerified = true)
18 | .catch(err => info.error = err.message)
19 |
20 | return {
21 | () => info.isVerified
22 | ?
23 |
24 | Your email has been verified! You can now
proceed.
25 |
26 | : info.error
27 | || 'Loading...'
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const ICON_16 = { size: 16, 'stroke-width': 2.05 }
2 | export const ICON_24 = { size: 24, 'stroke-width': 1.3 }
3 | export const ICON_32 = { size: 32, 'stroke-width': 1.2 }
4 | export const ICON_48 = { size: 48, 'stroke-width': 0.8 }
5 |
6 | export enum AnimMode {
7 | Auto = 'auto',
8 | On = 'on',
9 | Off = 'off',
10 | }
11 |
--------------------------------------------------------------------------------
/src/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | const Env = z.object({
4 | VITE_API_URL: z.string(),
5 | })
6 |
7 | export const env = Env.parse(Object.assign({
8 | VITE_API_URL: location.origin
9 | }, import.meta.env))
10 |
11 | const url = new URL(location.origin)
12 | if (url.port.length) {
13 | url.port = '8000'
14 | env.VITE_API_URL = url.href.slice(0, -1) // trim trailing slash
15 | }
16 |
--------------------------------------------------------------------------------
/src/lang/index.ts:
--------------------------------------------------------------------------------
1 | export * from './interpreter.ts'
2 | export * from './tokenize.ts'
3 | export * from './util.ts'
4 |
5 |
--------------------------------------------------------------------------------
/src/lang/interpreter.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it, mock } from "bun:test"
2 | import { interpret } from '~/src/lang/interpreter.ts'
3 | import { tokenize } from '~/src/lang/tokenize.ts'
4 |
5 | describe('interpret', () => {
6 | it('works', () => {
7 | const tokens = Array.from(tokenize({ code: '[sin 42]' }))
8 | const api = {
9 | gen: { sin: mock() }
10 | } as any
11 | const result = interpret(api, {}, tokens)
12 | expect(api.gen.sin).toHaveBeenCalledTimes(1)
13 | expect(api.gen.sin).toHaveBeenCalledWith({ hz: { value: 42, format: 'f', digits: 0 } })
14 | })
15 | it('procedure', () => {
16 | const tokens = Array.from(tokenize({
17 | code: `
18 | { x= x 2 * } double=
19 | [sin 21 double]
20 | ` }))
21 | const api = {
22 | math: {
23 | '*': mock((a: any, b: any) => {
24 | return { value: a.value * b.value }
25 | })
26 | },
27 | gen: { sin: mock() }
28 |
29 | } as any
30 | const result = interpret(api, {}, tokens)
31 | expect(api.gen.sin).toHaveBeenCalledTimes(1)
32 | expect(api.gen.sin).toBeCalledWith({ hz: { value: 42 } })
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/lang/tokenize.test.ts:
--------------------------------------------------------------------------------
1 | import { Token, tokenize } from '~/src/lang/tokenize.ts'
2 |
3 | describe('tokenize', () => {
4 | it('simple', () => {
5 | const source = { code: 'hello world\n123' }
6 | const tokens = [...tokenize(source)]
7 | expect(tokens.length).toBe(3)
8 | expect(tokens[0]).toMatchObject({
9 | type: Token.Type.Id,
10 | text: 'hello',
11 | line: 0,
12 | col: 0,
13 | right: 5,
14 | bottom: 0,
15 | index: 0,
16 | length: 5,
17 | source,
18 | })
19 | expect(tokens[1]).toMatchObject({
20 | type: Token.Type.Id,
21 | text: 'world',
22 | line: 0,
23 | col: 6,
24 | right: 11,
25 | bottom: 0,
26 | index: 6,
27 | length: 5,
28 | source,
29 | })
30 | expect(tokens[2]).toMatchObject({
31 | type: Token.Type.Number,
32 | text: '123',
33 | line: 1,
34 | col: 0,
35 | right: 3,
36 | bottom: 1,
37 | index: 12,
38 | length: 3,
39 | source,
40 | })
41 | })
42 |
43 | it('multiline', () => {
44 | const source = { code: '[; hello world\n123 ]' }
45 | const tokens = [...tokenize(source)]
46 | expect(tokens.length).toBe(2)
47 | expect(tokens[0]).toMatchObject({
48 | type: Token.Type.Comment,
49 | text: '[; hello world',
50 | line: 0,
51 | col: 0,
52 | right: 14,
53 | bottom: 0,
54 | index: 0,
55 | length: 14,
56 | source,
57 | })
58 | expect(tokens[1]).toMatchObject({
59 | type: Token.Type.Comment,
60 | text: '123 ]',
61 | line: 1,
62 | col: 0,
63 | right: 5,
64 | bottom: 1,
65 | index: 15,
66 | length: 5,
67 | source,
68 | })
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/src/lang/util.ts:
--------------------------------------------------------------------------------
1 | export type NumberFormat = 'f' | 'd' | 'h' | 'k' | '#'
2 |
3 | export interface NumberInfo {
4 | value: number
5 | format: 'f' | 'd' | 'h' | 'k' | '#'
6 | digits: number
7 | }
8 |
9 | const testModifierRegExp = /[\.khd#]/
10 |
11 | export function parseNumber(x: string): NumberInfo {
12 | let value: number
13 | let format: NumberFormat = 'f'
14 | let digits = 0
15 |
16 | let res: any
17 | out: {
18 | if (res = testModifierRegExp.exec(x)) {
19 | switch (res[0]) {
20 | case '.': {
21 | const [, b = ''] = x.split('.')
22 | digits = b.length
23 | value = Number(x)
24 | break out
25 | }
26 |
27 | case 'k': {
28 | const [a, b = ''] = x.split('k')
29 | format = 'k'
30 | digits = b.length
31 | value = Number(a) * 1000 + Number(b) * (1000 / (10 ** digits))
32 | break out
33 | }
34 |
35 | case 'h': {
36 | const [a, b = ''] = x.split('h')
37 | format = 'h'
38 | digits = b.length
39 | value = Number(a) * 100 + Number(b) * (100 / (10 ** digits))
40 | break out
41 | }
42 |
43 | case 'd': {
44 | const [a, b = ''] = x.split('d')
45 | format = 'd'
46 | digits = b.length
47 | value = Number(a) * 10 + Number(b) * digits
48 | break out
49 | }
50 |
51 | case '#': {
52 | format = '#'
53 | value = parseInt(x.slice(1), 16)
54 | break out
55 | }
56 | }
57 | }
58 |
59 | value = Number(x)
60 | }
61 |
62 | return { value, format, digits }
63 | }
64 |
--------------------------------------------------------------------------------
/src/pages/About.tsx:
--------------------------------------------------------------------------------
1 | export function About() {
2 | return
3 | About Page
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/AssemblyScript.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { Player } from '~/src/as/pkg/player.ts'
3 | import { PkgService } from '~/src/as/pkg/service.ts'
4 | import pkg from '~/src/as/pkg/wasm.ts'
5 | import { Button, Input } from '~/src/ui/index.ts'
6 |
7 | let audioContext: AudioContext
8 |
9 | export function AssemblyScript() {
10 | using $ = Sigui()
11 |
12 | const info = $({
13 | fromWorker: null as null | number,
14 | n1: 2,
15 | n2: 3,
16 | })
17 |
18 | audioContext ??= new AudioContext()
19 |
20 | const pkgPlayer = Player(audioContext)
21 | $.fx(() => () => pkgPlayer.destroy())
22 |
23 | const pkgService = PkgService()
24 | $.fx(() => () => pkgService.terminate())
25 |
26 | $.fx(() => {
27 | const { n1, n2 } = info
28 | const { pkg } = $.of(pkgService.info)
29 | $().then(async () => {
30 | info.fromWorker = await pkgService.service.multiply(n1, n2)
31 | })
32 | })
33 |
34 | return
35 | Welcome from AssemblyScript!
36 |
37 | Direct: {pkg.multiply(2, 3)}
38 |
39 |
40 | Worker: {
42 | info.n1 = +(ev.target as HTMLInputElement).value
43 | }}
44 | class="w-8"
45 | value={() => info.n1}
46 | /> + {
48 | info.n2 = +(ev.target as HTMLInputElement).value
49 | }}
50 | class="w-8"
51 | value={() => info.n2}
52 | /> = {() => info.fromWorker}
53 |
54 |
55 | Worklet:
56 |
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/pages/CanvasDemo.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui, type Signal } from 'sigui'
2 | import { drawText } from 'utils'
3 | import { screen } from '~/src/screen.ts'
4 | import { Canvas } from '~/src/ui/Canvas.tsx'
5 | import { H2 } from '~/src/ui/Heading.tsx'
6 |
7 | export function CanvasDemo({ width, height }: {
8 | width: Signal
9 | height: Signal
10 | }) {
11 | using $ = Sigui()
12 |
13 | const canvas = as HTMLCanvasElement
14 |
15 | const info = $({
16 | c: null as null | CanvasRenderingContext2D,
17 | pr: screen.$.pr,
18 | width,
19 | height,
20 | })
21 |
22 | const c = canvas.getContext('2d')!
23 | document.fonts.ready.then(() => {
24 | info.c = c
25 | })
26 |
27 | let i = 0
28 | let animFrame: any
29 | function tick() {
30 | animFrame = requestAnimationFrame(tick)
31 | c.restore()
32 | c.save()
33 | c.rotate(0.12 * i++)
34 | drawText(c, { x: 10, y: 10 }, 'Hello World', `hsl(${i % 360}, 50%, 50%)`, 4, '#000')
35 | }
36 |
37 | $.fx(() => {
38 | const { pr, c, width, height } = $.of(info)
39 | $()
40 | c.scale(pr, pr)
41 | c.textBaseline = 'top'
42 | c.textRendering = 'optimizeSpeed'
43 | c.miterLimit = 1.5
44 | c.font = '32px "Fustat"'
45 | })
46 |
47 | $.fx(() => {
48 | const { c, width, height } = $.of(info)
49 | $()
50 | c.translate(width / 2, height / 2)
51 | tick()
52 | return () => cancelAnimationFrame(animFrame)
53 | })
54 |
55 | return
56 |
Canvas demo
57 | {canvas}
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/pages/Chat/Users.tsx:
--------------------------------------------------------------------------------
1 | import { colorizeNick } from '~/src/pages/Chat/util.ts'
2 | import { state } from '~/src/state.ts'
3 | import { H3 } from '~/src/ui/Heading.tsx'
4 | import { Link } from '~/src/ui/Link.tsx'
5 |
6 | export function Users({ onUserClick }: {
7 | onUserClick(nick: string): void
8 | }) {
9 | if (!state.user) return
10 |
11 | const { nick } = state.user
12 |
13 | return
14 |
Users
15 |
16 |
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/Chat/util.ts:
--------------------------------------------------------------------------------
1 | import { state } from '~/src/state.ts'
2 |
3 | export function byName(a: { name: string }, b: { name: string }) {
4 | return a.name.localeCompare(b.name)
5 | }
6 |
7 | export function byNick(a: { nick: string }, b: { nick: string }) {
8 | return a.nick.localeCompare(b.nick)
9 | }
10 |
11 | export function colorizeNick(nick: string = '') {
12 | const hash = [...nick].reduce((acc, char) => char.charCodeAt(0) + acc, 0)
13 | const hue = hash % 360
14 | return `hsl(${hue}, 60%, 55%)`
15 | }
16 |
17 | export function hasChannel(channelName: string) {
18 | return state.channelsList.find(c => c.name === channelName)
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/CreateSound.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { H2 } from 'ui'
3 | import { dspEditorUi } from '~/src/comp/DspEditorUi.tsx'
4 | import { getDspControls } from '~/src/pages/DspControls.tsx'
5 |
6 | export function CreateSound() {
7 | using $ = Sigui()
8 |
9 | const info = $({
10 | code: '[sin 300] [exp 1] *'
11 | })
12 |
13 | $.fx(() => {
14 | $()
15 | const { info: controlsInfo, dspEditorUi } = getDspControls()
16 | controlsInfo.loadedSound = null
17 | controlsInfo.isLoadingSound = false
18 | const { pane } = dspEditorUi().dspEditor.editor.info
19 | const newPane = dspEditorUi().dspEditor.editor.createPane({ rect: pane.rect, code: info.$.code })
20 | dspEditorUi().info.code = info.code
21 | dspEditorUi().dspEditor.editor.addPane(newPane)
22 | dspEditorUi().dspEditor.editor.removePane(pane)
23 | dspEditorUi().dspEditor.editor.info.pane = newPane
24 | })
25 |
26 | return
27 |
28 | Create Sound
29 |
30 |
{() => getDspControls().el}
31 |
32 |
33 |
{() => dspEditorUi().el}
34 |
as HTMLDivElement
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/Logout.tsx:
--------------------------------------------------------------------------------
1 | import { logout } from '~/src/rpc/auth.ts'
2 | import { state } from '~/src/state.ts'
3 |
4 | export function logoutAction() {
5 | logout()
6 | .then(() => {
7 | state.user =
8 | state.profile =
9 | state.favorites =
10 | null
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/OAuthRegister.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { z } from 'zod'
3 | import * as oauth from '~/src/rpc/oauth.ts'
4 | import { go } from '~/src/ui/Link.tsx'
5 | import { parseForm } from '~/src/util/parse-form.ts'
6 |
7 | const formSchema = z.object({
8 | nick: z.string(),
9 | })
10 |
11 | export function OAuthRegister() {
12 | using $ = Sigui()
13 |
14 | const id = new URL(location.href).searchParams.get('id')
15 | if (!id) return OAuth session id not found
16 |
17 | const info = $({
18 | nick: undefined as undefined | string,
19 | error: null as null | string
20 | })
21 |
22 | oauth.getLoginSession(id).then(loginSession => {
23 | info.nick = loginSession.login
24 | })
25 |
26 | return
27 | Pick a nick:
28 |
29 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/QrCode.tsx:
--------------------------------------------------------------------------------
1 | import { encodeData, generateSVGQRCode } from 'easygenqr'
2 | import { Sigui } from 'sigui'
3 | import { state } from '~/src/state.ts'
4 |
5 | export function QrCode() {
6 | using $ = Sigui()
7 |
8 | const info = $({
9 | qrCode: ,
10 | })
11 |
12 | $.fx(() => {
13 | const { url } = state
14 | $()
15 | const qr = encodeData({
16 | text: url.href,
17 | errorCorrectionLevel: 2
18 | })
19 | info.qrCode.innerHTML = generateSVGQRCode(qr, {
20 | bgColor: "#ccc",
21 | dotColor: "#333",
22 | dotMode: 1,
23 | markerColor: "#333",
24 | markerMode: 1,
25 | })
26 | })
27 |
28 | return
29 | {info.qrCode}
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/pages/Settings.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { H2, H3, Link } from 'ui'
3 | import type { z } from 'zod'
4 | import type { Profiles } from '~/api/models.ts'
5 | import { listProfilesForNick } from '~/src/rpc/profiles.ts'
6 | import { state } from '~/src/state.ts'
7 |
8 | export function Settings() {
9 | using $ = Sigui()
10 |
11 | const info = $({
12 | profiles: [] as z.infer[],
13 | })
14 |
15 | $.fx(() => {
16 | const { user } = $.of(state)
17 | const { nick } = user
18 | $().then(async () => {
19 | info.profiles = await listProfilesForNick(nick)
20 | })
21 | })
22 |
23 | return
24 |
Settings Page
25 |
26 |
27 |
28 | Profiles
29 |
30 | Create New Profile
31 |
32 |
33 |
{
34 | () => info.profiles.map(profile =>
35 |
36 | {() => profile.displayName} {() => state.user && profile.nick === state.user.defaultProfile ? '(default)' : ''}
37 |
38 |
)
39 | }
40 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/Showcase.tsx:
--------------------------------------------------------------------------------
1 | import { Login } from '~/src/comp/Login.tsx'
2 | import { OAuthLogin } from '~/src/comp/OAuthLogin.tsx'
3 | import { Register } from '~/src/comp/Register.tsx'
4 | import { state } from '~/src/state.ts'
5 | import { Link } from '~/src/ui/Link.tsx'
6 |
7 | export function Showcase() {
8 | return
9 | {() => state.user === undefined
10 | ?
Loading...
11 | : state.user === null
12 | ?
13 |
14 |
15 |
16 | or
17 |
18 |
19 |
20 |
21 |
22 | :
23 |
24 | {state.user.isAdmin &&
Admin}
25 |
UI Showcase
26 |
Chat
27 |
WebSockets
28 |
Canvas
29 |
WebGL
30 |
Editor
31 |
Dsp
32 |
Worker-Worklet
33 |
AssemblyScript
34 |
QrCode
35 |
Create Profile
36 |
About
37 |
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/pages/UiShowcase.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from 'lucide'
2 | import { Button, DropDown, Fieldset, H1, H2, H3, Input, Label, Link } from 'ui'
3 | import { icon } from '~/lib/icon.ts'
4 |
5 | function UiGroup({ name, children }: { name: string, children?: any }) {
6 | return
7 |
{name}
8 | {children}
9 |
10 | }
11 |
12 | export function UiShowcase() {
13 | return (
14 |
15 |
16 |
UI Showcase
17 |
18 |
19 | Heading 1
20 | Heading 2
21 | Heading 3
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 | console.log('Item 1')],
38 | ['Item 2', () => console.log('Item 2')],
39 | ['Item 3', () => console.log('Item 3')],
40 | ]} />
41 |
42 | console.log('Item 1')],
44 | ['Item 2', () => console.log('Item 2')],
45 | ['Item 3', () => console.log('Item 3')],
46 | ]} />
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | About
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/src/pages/WebSockets.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 | import { dom, isMobile } from 'utils'
3 | import { createWebSocket } from '~/lib/ws.ts'
4 | import { env } from '~/src/env.ts'
5 | import { colorizeNick } from '~/src/pages/Chat/util.ts'
6 | import { state } from '~/src/state.ts'
7 |
8 | export function WebSockets() {
9 | using $ = Sigui()
10 |
11 | const ws = createWebSocket('/ws', env.VITE_API_URL)
12 | $.fx(() => () => ws.close())
13 |
14 | const el = as HTMLDivElement
15 | const pointers = new Map()
16 |
17 | ws.onmessage = ({ data }) => {
18 | const [nick, x, y] = data.split(',')
19 |
20 | let pointer = pointers.get(nick)
21 | if (!pointer) {
22 | pointers.set(nick, pointer = as HTMLDivElement)
23 | el.append(pointer)
24 | }
25 | pointer.style.left = x + 'px'
26 | pointer.style.top = y + 'px'
27 | }
28 |
29 | $.fx(() => [
30 | dom.on(el, isMobile() ? 'touchmove' : 'pointermove', ev => {
31 | ev.preventDefault()
32 |
33 | const p: { pageX: number, pageY: number } = ev.type === 'touchmove'
34 | ? (ev as TouchEvent).touches[0]!
35 | : ev as PointerEvent
36 |
37 | if (state.user && ws.state() == 'open') {
38 | ws.send(`${state.user.nick},${p.pageX.toFixed(1)},${p.pageY.toFixed(1)}`)
39 | }
40 | }, { passive: false })
41 | ].filter(Boolean))
42 |
43 | return el
44 | }
45 |
--------------------------------------------------------------------------------
/src/pages/WorkerWorklet/basic-processor.ts:
--------------------------------------------------------------------------------
1 | import { FRAME_SIZE, RENDER_QUANTUM } from '~/src/pages/WorkerWorklet/constants.ts'
2 | import { FreeQueue } from '~/src/pages/WorkerWorklet/free-queue.ts'
3 |
4 | /**
5 | * A simple AudioWorkletProcessor node.
6 | *
7 | * @class BasicProcessor
8 | * @extends AudioWorkletProcessor
9 | */
10 | class BasicProcessor extends AudioWorkletProcessor {
11 | inputQueue: FreeQueue
12 | outputQueue: FreeQueue
13 | atomicState: Int32Array
14 |
15 | /**
16 | * Constructor to initialize, input and output FreeQueue instances
17 | * and atomicState to synchronise Worker with AudioWorklet
18 | * @param {Object} options AudioWorkletProcessor options
19 | * to initialize inputQueue, outputQueue and atomicState
20 | */
21 | constructor(options: AudioWorkletNodeOptions) {
22 | super()
23 |
24 | this.inputQueue = options.processorOptions.inputQueue
25 | this.outputQueue = options.processorOptions.outputQueue
26 | this.atomicState = options.processorOptions.atomicState
27 | Object.setPrototypeOf(this.inputQueue, FreeQueue.prototype)
28 | Object.setPrototypeOf(this.outputQueue, FreeQueue.prototype)
29 | }
30 |
31 | process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
32 | const input = inputs[0]
33 | const output = outputs[0]
34 |
35 | // Push data from input into inputQueue.
36 | this.inputQueue.push(input, RENDER_QUANTUM)
37 |
38 | // Try to pull data out of outputQueue and store it in output.
39 | const didPull = this.outputQueue.pull(output, RENDER_QUANTUM)
40 | if (!didPull) {
41 | // console.log("failed to pull.")
42 | }
43 |
44 | // Wake up worker to process a frame of data.
45 | if (this.inputQueue.isFrameAvailable(FRAME_SIZE)) {
46 | Atomics.notify(this.atomicState, 0, 1)
47 | }
48 |
49 | return true
50 | }
51 | }
52 |
53 | registerProcessor('basic-processor', BasicProcessor)
54 |
--------------------------------------------------------------------------------
/src/pages/WorkerWorklet/constants.ts:
--------------------------------------------------------------------------------
1 | export const KERNEL_LENGTH = 30
2 | export const RENDER_QUANTUM = 128
3 | export const FRAME_SIZE = KERNEL_LENGTH * RENDER_QUANTUM
4 | export const QUEUE_SIZE = 4096
5 |
--------------------------------------------------------------------------------
/src/pages/WorkerWorklet/worker.ts:
--------------------------------------------------------------------------------
1 | import { FRAME_SIZE } from "./constants.ts"
2 | import { FreeQueue } from "./free-queue.ts"
3 |
4 | /**
5 | * Worker message event handler.
6 | * This will initialize worker with FreeQueue instance and set loop for audio
7 | * processing.
8 | */
9 | self.onmessage = (msg) => {
10 | if (msg.data.type === "init") {
11 | let { inputQueue, outputQueue, atomicState, cmd, state } = msg.data.data as {
12 | inputQueue: FreeQueue
13 | outputQueue: FreeQueue
14 | atomicState: Int32Array
15 | cmd: Uint8Array
16 | state: Uint8Array
17 | }
18 | Object.setPrototypeOf(inputQueue, FreeQueue.prototype)
19 | Object.setPrototypeOf(outputQueue, FreeQueue.prototype)
20 |
21 | // buffer for storing data pulled out from queue.
22 | const input = new Float32Array(FRAME_SIZE)
23 |
24 | let hz = 300
25 | let phase = 0
26 | let t = 0
27 | const decoder = new TextDecoder()
28 | // loop for processing data.
29 | while (Atomics.wait(atomicState, 0, 0) === 'ok') {
30 |
31 | // pull data out from inputQueue.
32 | const didPull = inputQueue.pull([input], FRAME_SIZE)
33 |
34 | if (didPull) {
35 | // If pulling data out was successfull, process it and push it to
36 | // outputQueue
37 | const output = input.map(() => {
38 | const s = Math.sin(phase) * 0.2
39 | phase += (1 / 48000) * hz * Math.PI * 2
40 | return s
41 | })
42 | outputQueue.push([output], FRAME_SIZE)
43 | }
44 |
45 | if (cmd[0]) {
46 | const st = JSON.parse(decoder.decode(state.slice(0, cmd[0]))) as { hz: number }
47 | hz = st.hz
48 | // console.log('set hz', hz)
49 | cmd[0] = 0
50 | }
51 |
52 | // }
53 |
54 | Atomics.store(atomicState, 0, 0)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/rpc/auth.ts:
--------------------------------------------------------------------------------
1 | import { $ } from 'sigui'
2 | import type * as actions from '~/api/auth/actions.ts'
3 | import type { UserSession } from '~/api/auth/types.ts'
4 | import { rpc } from '~/lib/rpc.ts'
5 | import { state } from '~/src/state.ts'
6 |
7 | export const whoami = rpc('POST', 'whoami')
8 |
9 | export const login = rpc('POST', 'login')
10 | export const logout = rpc('POST', 'logout')
11 | export const register = rpc('POST', 'register')
12 |
13 | export const sendVerificationEmail = rpc('POST', 'sendVerificationEmail')
14 | export const verifyEmail = rpc('POST', 'verifyEmail')
15 |
16 | export const forgotPassword = rpc('POST', 'forgotPassword')
17 | export const getResetPasswordUserNick = rpc('GET', 'getResetPasswordUserNick')
18 | export const changePassword = rpc('POST', 'changePassword')
19 |
20 | export function loginUserSession(userSession: UserSession | null) {
21 | if (!userSession) throw new Error('No user session')
22 | state.user = userSession ? $(userSession) : userSession
23 | }
24 |
25 | export async function maybeLogin(orRedirect?: string) {
26 | if (state.user) return
27 | try {
28 | const userSession = await whoami()
29 | loginUserSession(userSession)
30 | }
31 | catch (error) {
32 | console.warn(error)
33 | state.user = null
34 | if (orRedirect) location.href = orRedirect
35 | }
36 | }
37 |
38 | export async function logoutUser() {
39 | await logout()
40 | state.user =
41 | state.profile =
42 | state.favorites =
43 | null
44 | }
45 |
--------------------------------------------------------------------------------
/src/rpc/chat.ts:
--------------------------------------------------------------------------------
1 | import type * as actions from '~/api/chat/actions.ts'
2 | import { rpc } from '~/lib/rpc.ts'
3 |
4 | export const listChannels = rpc('GET', 'listChannels')
5 | export const createChannel = rpc('POST', 'createChannel')
6 | export const deleteChannel = rpc('POST', 'deleteChannel')
7 | export const getChannel = rpc('GET', 'getChannel')
8 | export const joinChannel = rpc('POST', 'joinChannel')
9 | export const sendMessageToChannel = rpc('POST', 'sendMessageToChannel')
10 | export const sendMessageToUser = rpc('POST', 'sendMessageToUser')
11 |
--------------------------------------------------------------------------------
/src/rpc/oauth.ts:
--------------------------------------------------------------------------------
1 | import type * as actions from '~/api/oauth/actions.ts'
2 | import { rpc } from '~/lib/rpc.ts'
3 |
4 | export const getLoginSession = rpc('GET', 'getLoginSession')
5 | export const registerOAuth = rpc('POST', 'registerOAuth')
6 |
--------------------------------------------------------------------------------
/src/rpc/profiles.ts:
--------------------------------------------------------------------------------
1 | import type * as actions from '~/api/profiles/actions.ts'
2 | import { rpc } from '~/lib/rpc.ts'
3 |
4 | export const createProfile = rpc('POST', 'createProfile')
5 | export const makeDefaultProfile = rpc('POST', 'makeDefaultProfile')
6 | export const getProfile = rpc('GET', 'getProfile')
7 | export const listProfilesForNick = rpc('GET', 'listProfilesForNick')
8 | export const deleteProfile = rpc('POST', 'deleteProfile')
9 |
--------------------------------------------------------------------------------
/src/rpc/sounds.ts:
--------------------------------------------------------------------------------
1 | import type * as actions from '~/api/sounds/actions.ts'
2 | import { rpc } from '~/lib/rpc.ts'
3 |
4 | export const publishSound = rpc('POST', 'publishSound')
5 | export const overwriteSound = rpc('POST', 'overwriteSound')
6 | export const deleteSound = rpc('POST', 'deleteSound')
7 | export const listSounds = rpc('GET', 'listSounds')
8 | export const listRecentSounds = rpc('GET', 'listRecentSounds')
9 | export const getSound = rpc('GET', 'getSound')
10 | export const addSoundToFavorites = rpc('POST', 'addSoundToFavorites')
11 | export const removeSoundFromFavorites = rpc('POST', 'removeSoundFromFavorites')
12 | export const listFavorites = rpc('GET', 'listFavorites')
13 |
--------------------------------------------------------------------------------
/src/screen.ts:
--------------------------------------------------------------------------------
1 | import { $ } from 'sigui'
2 | import { dom } from 'utils'
3 |
4 | export const screen = $({
5 | pr: window.devicePixelRatio,
6 | width: window.visualViewport!.width,
7 | height: window.visualViewport!.height,
8 | get sm() {
9 | return screen.width < 680
10 | },
11 | get md() {
12 | return screen.width >= 680
13 | },
14 | get lg() {
15 | return screen.width >= 768
16 | },
17 | })
18 |
19 | $.fx(() => [
20 | dom.on(window, 'resize', $.fn(() => {
21 | const viewport = window.visualViewport!
22 | screen.pr = window.devicePixelRatio
23 | screen.width = viewport.width
24 | screen.height = viewport.height
25 | }), { unsafeInitial: true }),
26 | ])
27 |
--------------------------------------------------------------------------------
/src/service-worker/sw.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
3 | import { clientsClaim } from 'workbox-core'
4 | import { NavigationRoute, registerRoute } from 'workbox-routing'
5 |
6 | declare let self: ServiceWorkerGlobalScope
7 |
8 | // self.__WB_MANIFEST is the default injection point
9 | precacheAndRoute(self.__WB_MANIFEST)
10 |
11 | // clean old assets
12 | cleanupOutdatedCaches()
13 |
14 | let allowlist: RegExp[] = []
15 | // in dev mode, we disable precaching to avoid caching issues
16 | if (import.meta.env.DEV)
17 | allowlist = [/^\/$/]
18 |
19 | // to allow work offline
20 | registerRoute(new NavigationRoute(
21 | createHandlerBoundToURL('index.html'),
22 | { allowlist },
23 | ))
24 |
25 | self.skipWaiting()
26 | clientsClaim()
27 |
--------------------------------------------------------------------------------
/src/test/e2e.test.tsx-fixme:
--------------------------------------------------------------------------------
1 | // disabled because bun test can't handle worker imports
2 | import { App } from '~/src/pages/App.tsx'
3 | import { state } from '~/src/state.ts'
4 |
5 | // @ts-ignore
6 | globalThis.fetch = () => {
7 | return { json: () => { } }
8 | }
9 |
10 | describe('App', () => {
11 | it('works', () => {
12 | const app =
13 | expect(app).toBeInstanceOf(Element)
14 | })
15 |
16 | it('routes', async () => {
17 | const app =
18 | expect(app).toBeInstanceOf(Element)
19 | state.url = new URL('/about', location.origin)
20 | state.url = new URL('/verify-email', location.origin)
21 | state.url = new URL('/reset-password', location.origin)
22 | state.url = new URL('/oauth/popup', location.origin)
23 | state.url = new URL('/oauth/register', location.origin)
24 | state.url = new URL('/oauth/complete', location.origin)
25 | })
26 |
27 | it('user', async () => {
28 | const app =
29 | expect(app).toBeInstanceOf(Element)
30 | state.user = null
31 | state.user = {
32 | nick: 'foo',
33 | expires: new Date(),
34 | }
35 | state.user = {
36 | nick: 'foo',
37 | expires: new Date(),
38 | isAdmin: true
39 | }
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import resolveConfig from 'tailwindcss/resolveConfig'
2 | import tailwindConfig from '../tailwind.config.js'
3 |
4 | export const { theme } = resolveConfig(tailwindConfig)
5 |
--------------------------------------------------------------------------------
/src/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '~/lib/cn.ts'
2 |
3 | export function Button(props: Record & { bare?: boolean }) {
4 | return
19 | }
20 |
--------------------------------------------------------------------------------
/src/ui/Canvas.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui, type Signal } from 'sigui'
2 | import { cn } from '~/lib/cn.ts'
3 | import { screen } from '~/src/screen.ts'
4 |
5 | export function Canvas({ width, height, class: className }: {
6 | width: Signal
7 | height: Signal
8 | class?: string
9 | }) {
10 | using $ = Sigui()
11 |
12 | const canvas = as HTMLCanvasElement
17 |
18 | const info = $({
19 | pr: screen.$.pr,
20 | width,
21 | height,
22 | })
23 |
24 | $.fx(() => {
25 | const { width, height, pr } = $.of(info)
26 | $()
27 | canvas.width = width * pr
28 | canvas.height = height * pr
29 | canvas.style.width = `${width}px`
30 | canvas.style.height = `${height}px`
31 | })
32 |
33 | return canvas
34 | }
35 |
--------------------------------------------------------------------------------
/src/ui/Fieldset.tsx:
--------------------------------------------------------------------------------
1 | export function Fieldset({ legend, children }: { legend: string, children?: any }) {
2 | return
8 | }
9 |
--------------------------------------------------------------------------------
/src/ui/Heading.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '~/lib/cn.ts'
2 |
3 | export function H1({ children }: { children?: any }) {
4 | return
5 | {children}
6 |
7 | }
8 |
9 | export function H2({ class: _class = '', children }: { class?: string, children?: any }) {
10 | return
11 | {children}
12 |
13 | }
14 |
15 | export function H3({ children }: { children?: any }) {
16 | return
17 | {children}
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/ui/Input.tsx:
--------------------------------------------------------------------------------
1 | import { Sigui, type Signal } from 'sigui'
2 | import { cn } from '~/lib/cn.ts'
3 |
4 | export function Input(props: Record) {
5 | return
12 | }
13 |
--------------------------------------------------------------------------------
/src/ui/Label.tsx:
--------------------------------------------------------------------------------
1 | export function Label({ text, children }: { text: string, children?: any }) {
2 | return
10 | }
11 |
--------------------------------------------------------------------------------
/src/ui/Layout.tsx:
--------------------------------------------------------------------------------
1 | export function Layout({ children }: { children?: any }) {
2 | return
3 |
4 | {children}
5 |
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/ui/Link.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from 'sigui'
2 |
3 | export const link = $({
4 | url: new URL(location.href)
5 | })
6 |
7 | window.onpopstate = () => {
8 | link.url = new URL(location.href)
9 | }
10 |
11 | export function go(href: string) {
12 | history.pushState({}, '', href)
13 | link.url = new URL(location.href)
14 | }
15 |
16 | export function Link({
17 | href = '#',
18 | class: _class = '',
19 | title,
20 | style,
21 | onclick = go,
22 | children
23 | }: {
24 | href?: string | (() => string)
25 | class?: string | (() => string)
26 | title?: string
27 | style?: any
28 | onclick?: (href: string) => unknown
29 | children?: any
30 | }) {
31 | return {
32 | ev.preventDefault()
33 | onclick(typeof href === 'function' ? href() : href)
34 | }}>{children}
35 | }
36 |
--------------------------------------------------------------------------------
/src/ui/editor/constants.ts:
--------------------------------------------------------------------------------
1 | export const CLICK_TIMEOUT = 350
2 |
--------------------------------------------------------------------------------
/src/ui/editor/dims.ts:
--------------------------------------------------------------------------------
1 | import { Point, type Rect } from '~/src/ui/editor/util/types.ts'
2 | import { Sigui, type $ } from 'sigui'
3 | import { isMobile } from 'utils'
4 |
5 | export type Dims = ReturnType
6 |
7 | export function Dims({ rect }: {
8 | rect: $
9 | }) {
10 | using $ = Sigui()
11 |
12 | const info = $({
13 | rect,
14 |
15 | caretWidth: 1.5,
16 |
17 | charWidth: 1,
18 | charHeight: 1,
19 |
20 | lineHeight: 19,
21 |
22 | pageHeight: 1,
23 | pageWidth: 1,
24 |
25 | innerSize: $(Point()),
26 |
27 | scrollX: 0,
28 | scrollY: 0,
29 | scrollbarHandleSize: isMobile() ? 30 : 10,
30 | scrollbarViewSize: 5,
31 | })
32 |
33 | return { info }
34 | }
35 |
--------------------------------------------------------------------------------
/src/ui/editor/index.ts:
--------------------------------------------------------------------------------
1 | export * from './buffer.ts'
2 | export * from './caret.ts'
3 | export * from './constants.ts'
4 | export * from './dims.ts'
5 | export * from './draw.ts'
6 | export * from './history.ts'
7 | export * from './input.ts'
8 | export * from './kbd.tsx'
9 | export * from './misc.ts'
10 | export * from './mouse.ts'
11 | export * from './pane.ts'
12 | export * from './selection.ts'
13 | export * from './util/index.ts'
14 | export * from './view.tsx'
15 | export * from './widget.ts'
16 |
--------------------------------------------------------------------------------
/src/ui/editor/misc.ts:
--------------------------------------------------------------------------------
1 | import { Sigui } from 'sigui'
2 |
3 | export type Misc = ReturnType
4 |
5 | export function Misc() {
6 | using $ = Sigui()
7 |
8 | const info = $({
9 | commentSingle: ';',
10 | commentDouble: ['[;', ']'],
11 | })
12 |
13 | return { info }
14 | }
15 |
--------------------------------------------------------------------------------
/src/ui/editor/util/begin-of-line.ts:
--------------------------------------------------------------------------------
1 | export function beginOfLine(line: string) {
2 | return line.match(/[^\s]|$/m)!.index!
3 | }
4 |
--------------------------------------------------------------------------------
/src/ui/editor/util/escape-regexp.ts:
--------------------------------------------------------------------------------
1 | export function escapeRegExp(string: string) {
2 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
3 | }
4 |
--------------------------------------------------------------------------------
/src/ui/editor/util/find-matching-brackets.ts:
--------------------------------------------------------------------------------
1 | export const Open = {
2 | '(': ')',
3 | '[': ']',
4 | '{': '}',
5 | } as any
6 |
7 | export const Close = {
8 | ')': '(',
9 | ']': '[',
10 | '}': '{',
11 | } as any
12 |
13 | export const openers = new Set(Object.keys(Open))
14 | export const closers = new Set(Object.keys(Close))
15 |
16 | export function findMatchingBrackets(s: string, i: number): [number, number] | undefined {
17 | let char: string
18 | const stack: string[] = []
19 | let max = 1000
20 |
21 | --i
22 |
23 | const L = s[i]
24 | const R = s[i + 1]
25 | const LO = Open[L]
26 | const RO = Open[R]
27 | const LC = Close[L]
28 | const RC = Close[R]
29 |
30 | if (LC && RO) i++
31 | else if ((LO || RO) && (LC || RC)) { }
32 | else if (RO && !LO) i++
33 | else if (LC && !RC) i--
34 |
35 | while (i >= 0) {
36 | if (!--max) return
37 | char = s[i--]!
38 | if (closers.has(char)) {
39 | stack.push(Close[char])
40 | }
41 | else if (stack.at(-1) === char) {
42 | stack.pop()
43 | }
44 | else if (openers.has(char)) {
45 | stack.push(char)
46 | break
47 | }
48 | }
49 | const openIndex = ++i
50 | const open = stack.at(-1)
51 | while (i < s.length) {
52 | if (!--max) return
53 | char = s[i++]!
54 | if (openers.has(char)) {
55 | stack.push(Open[char])
56 | }
57 | else if (stack.at(-1) === char) {
58 | stack.pop()
59 | if (stack.length === 1 && Close[char] === open) return [openIndex, i - 1]
60 | }
61 | else if (closers.has(char)) {
62 | return
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/ui/editor/util/floats.ts:
--------------------------------------------------------------------------------
1 | import { wasm } from 'gfx'
2 | import { WAVE_MIPMAPS } from '~/as/assembly/gfx/sketch-shared.ts'
3 |
4 | export type Floats = ReturnType
5 |
6 | export function Floats(waveform: Float32Array) {
7 | const len = waveform.length
8 |
9 | const targets = Array.from({ length: WAVE_MIPMAPS }, (_, i) => ({
10 | divisor: 2 ** (i + 1),
11 | len: 0,
12 | ptr: 0
13 | }))
14 |
15 | const size = targets.reduce((p, n) => {
16 | n.ptr = p
17 | n.len = Math.floor(len / n.divisor)
18 | return n.ptr + n.len
19 | }, len)
20 |
21 | const floats = Object.assign(
22 | wasm.alloc(Float32Array, size),
23 | { len }
24 | )
25 | floats.set(waveform)
26 |
27 | for (const { divisor, len, ptr } of targets) {
28 | for (let n = 0; n < len; n++) {
29 | const n0 = Math.floor(n * divisor)
30 | const n1 = Math.ceil((n + 1) * divisor)
31 |
32 | let min = Infinity, max = -Infinity
33 | let s
34 | for (let i = n0; i < n1; i++) {
35 | s = waveform[i]
36 | if (s < min) min = s
37 | if (s > max) max = s
38 | }
39 |
40 | if (
41 | !isFinite(min) &&
42 | !isFinite(max)
43 | ) min = max = 0
44 |
45 | if (!isFinite(min)) min = max
46 | if (!isFinite(max)) max = min
47 |
48 | const p = ptr + n
49 | floats[p] = Math.abs(min) > Math.abs(max) ? min : max
50 | }
51 | }
52 |
53 | return floats
54 | }
55 |
--------------------------------------------------------------------------------
/src/ui/editor/util/geometry.ts:
--------------------------------------------------------------------------------
1 | import type { Point, Rect } from 'editor'
2 |
3 | export function isPointInRect(p: Point, r: Rect) {
4 | return p.x >= r.x && p.x < r.x + r.w && p.y >= r.y && p.y < r.y + r.h
5 | }
6 |
--------------------------------------------------------------------------------
/src/ui/editor/util/index.ts:
--------------------------------------------------------------------------------
1 | export * from './begin-of-line.ts'
2 | export * from './escape-regexp.ts'
3 | export * from './find-matching-brackets.ts'
4 | export * from './floats.ts'
5 | export * from './geometry.ts'
6 | export * from './oklch.ts'
7 | export * from './parse-words.ts'
8 | export * from './regexp.ts'
9 | export * from './rgb.ts'
10 | export * from './types.ts'
11 | export * from './waveform.ts'
12 |
--------------------------------------------------------------------------------
/src/ui/editor/util/oklch.ts:
--------------------------------------------------------------------------------
1 | // chatgpt
2 |
3 | export function oklchToHex(oklchString: string): string | null {
4 | // Extracting values from the oklch string
5 | const match = oklchString.match(/oklch\((\d+)%\s+(\d+)\s+(\d+)\)/)
6 |
7 | if (!match) {
8 | // Invalid input format
9 | return null
10 | }
11 |
12 | const lightness = parseInt(match[1], 10)
13 | const chroma = parseInt(match[2], 10)
14 | const hue = parseInt(match[3], 10)
15 |
16 | // Convert oklch to RGB
17 | const rgb = oklchToRGB(lightness, chroma, hue)
18 |
19 | // Convert RGB to hexadecimal
20 | const hex = rgbToHex(rgb)
21 |
22 | return hex
23 | }
24 |
25 | function oklchToRGB(lightness: number, chroma: number, hue: number): number[] {
26 | const h = hue / 360
27 | const s = chroma / 100
28 | const l = lightness / 100
29 |
30 | const x = chroma * (1 - Math.abs((h * 6) % 2 - 1))
31 |
32 | let r, g, b
33 |
34 | if (0 <= h && h < 1) {
35 | [r, g, b] = [chroma, x, 0]
36 | } else if (1 <= h && h < 2) {
37 | [r, g, b] = [x, chroma, 0]
38 | } else if (2 <= h && h < 3) {
39 | [r, g, b] = [0, chroma, x]
40 | } else if (3 <= h && h < 4) {
41 | [r, g, b] = [0, x, chroma]
42 | } else if (4 <= h && h < 5) {
43 | [r, g, b] = [x, 0, chroma]
44 | } else if (5 <= h && h < 6) {
45 | [r, g, b] = [chroma, 0, x]
46 | } else {
47 | [r, g, b] = [0, 0, 0]
48 | }
49 |
50 | const m = l - chroma / 2
51 |
52 | return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]
53 | }
54 |
55 | function rgbToHex(rgb: number[]): string {
56 | return '#' + rgb.map(value => value.toString(16).padStart(2, '0')).join('')
57 | }
58 |
--------------------------------------------------------------------------------
/src/ui/editor/util/parse-words.ts:
--------------------------------------------------------------------------------
1 | export function parseWords(regexp: RegExp, text: string) {
2 | regexp.lastIndex = 0
3 | let word
4 | const words: RegExpExecArray[] = []
5 | while ((word = regexp.exec(text))) words.push(word)
6 | return words
7 | }
8 |
--------------------------------------------------------------------------------
/src/ui/editor/util/regexp.ts:
--------------------------------------------------------------------------------
1 | export const NONSPACE = /[^\s]/g
2 | export const SPACE = /\s/g
3 | export const WORD = /\n|[\s]{2,}|[./\\()"'\-:,.;<>~!@#$%^&*|+=[\]{}`~?\b ]{1}|\w+/g
4 | export const TOKEN = /\s+|[\w\.]+|[\W]/g
5 | export const BRACKET = /[\[\]\(\)\{\}]/ // used with .test so not a /g
6 |
--------------------------------------------------------------------------------
/src/ui/editor/util/rgb.ts:
--------------------------------------------------------------------------------
1 | import { hexToRgb } from 'utils'
2 | import { oklchToHex } from './oklch.ts'
3 |
4 | type f32 = number
5 | type i32 = number
6 |
7 | function i32(x: number) {
8 | return Math.floor(x)
9 | }
10 |
11 | export function rgbToInt(r: f32, g: f32, b: f32): i32 {
12 | return (clamp255(r * 255) << 16) | (clamp255(g * 255) << 8) | clamp255(b * 255)
13 | }
14 |
15 | export function hexToInt(hex: string) {
16 | const [r, g, b] = hexToRgb(hex)
17 | return rgbToInt(r, g, b)
18 | }
19 |
20 | export function intToHex(x: number) {
21 | return '#' + x.toString(16).padStart(6, '0')
22 | }
23 |
24 | export function clamp255(x: f32): i32 {
25 | if (x > 255) x = 255
26 | if (x < 0) x = 0
27 | return i32(x)
28 | }
29 |
30 | export function toHex(x: string) {
31 | return !x ? '#ffffff' : x.startsWith('oklch') ? oklchToHex(x) ?? x : x
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/ui/editor/util/types.ts:
--------------------------------------------------------------------------------
1 | export type Rect = ReturnType
2 |
3 | export function Rect() {
4 | return { x: 0, y: 0, w: 0, h: 0 }
5 | }
6 |
7 | export type Point = ReturnType
8 |
9 | export function Point() {
10 | return { x: 0, y: 0 }
11 | }
12 |
13 | export type Linecol = ReturnType
14 |
15 | export function Linecol() {
16 | return { line: 0, col: 0 }
17 | }
18 |
19 | export function pointToLinecol(p: Point): Linecol {
20 | return { line: p.y, col: p.x }
21 | }
22 |
23 | export function linecolToPoint(linecol: Linecol): Point {
24 | return { x: linecol.col, y: linecol.line }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ui/editor/util/waveform.ts:
--------------------------------------------------------------------------------
1 | const waveformLength = 2048
2 |
3 | export function makeWaveform(length: number, startTime: number, frequency: number) {
4 | return Float32Array.from({ length }, (_, i) =>
5 | Math.sin(((i + startTime) / length) * Math.PI * 2 * frequency)
6 | )
7 | }
8 |
9 | export const waveform = makeWaveform(2048, 0, 1)
10 |
--------------------------------------------------------------------------------
/src/ui/editor/widget.ts:
--------------------------------------------------------------------------------
1 | import { Rect } from 'gfx'
2 | import { Sigui } from 'sigui'
3 | import type { Token } from '~/src/lang/tokenize.ts'
4 |
5 | function Bounds(): Token.Bounds {
6 | using $ = Sigui()
7 | return $({ line: 0, col: 0, right: 0, bottom: 0, index: 0, length: 0 })
8 | }
9 |
10 | interface WidgetLineInfo {
11 | deco: number
12 | subs: number
13 | mark: number
14 | }
15 |
16 | export type Widgets = ReturnType
17 |
18 | export function Widgets({ c }: {
19 | c: CanvasRenderingContext2D
20 | }) {
21 | const deco = new Set()
22 | const subs = new Set()
23 | const mark = new Set()
24 |
25 | const types = ['deco', 'subs', 'mark'] as const
26 |
27 | function widgetDraw(widget: Widget) {
28 | widget.draw(c)
29 | }
30 |
31 | const lines = new Map()
32 |
33 | const heights = {
34 | deco: 40,
35 | subs: 20,
36 | mark: 16,
37 | } as const
38 |
39 | function update() {
40 | lines.clear()
41 | types.forEach(type => {
42 | widgets[type].forEach(w => {
43 | let y = w.bounds.line
44 | let line = lines.get(y)
45 | if (!line) lines.set(y, line = { deco: 0, subs: 0, mark: 0 })
46 | line[type] = heights[type]
47 | })
48 | })
49 | }
50 |
51 | function drawDecoMark() {
52 | update()
53 | deco.forEach(widgetDraw)
54 | mark.forEach(widgetDraw)
55 | }
56 |
57 | function drawSubs() {
58 | update()
59 | subs.forEach(widgetDraw)
60 | }
61 |
62 | const widgets = { update, drawDecoMark, drawSubs, deco, subs, mark, heights, lines }
63 |
64 | return widgets
65 | }
66 |
67 | export type Widget = ReturnType
68 |
69 | export function Widget(rect = Rect(0, 0, 1, 1)) {
70 | using $ = Sigui()
71 | const bounds = Bounds()
72 | function draw(c: CanvasRenderingContext2D) { }
73 | return $({ rect, bounds, draw })
74 | }
75 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/error-sub.ts:
--------------------------------------------------------------------------------
1 | import { Widget } from 'editor'
2 | import { Sigui } from 'sigui'
3 | import { drawText } from 'utils'
4 |
5 | export function ErrorSubWidget() {
6 | using $ = Sigui()
7 |
8 | const info = $({
9 | color: '#f00',
10 | error: null as null | Error
11 | })
12 |
13 | const widget = Widget()
14 |
15 | widget.draw = c => {
16 | const { color, error } = info
17 | if (!error) return
18 | const { rect } = widget
19 | const { x, y, w, h } = rect
20 | c.beginPath()
21 | c.moveTo(x, y - 2)
22 | for (let sx = x; sx < x + w + 3; sx += 3) {
23 | c.lineTo(sx, y - 2 + (sx % 2 ? 0 : 2))
24 | }
25 | c.strokeStyle = color
26 | c.stroke()
27 | drawText(c, { x, y: y + 1 }, error.message, color, .025, color)
28 | }
29 |
30 | return { info, widget }
31 | }
32 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/hover-mark.ts:
--------------------------------------------------------------------------------
1 | import { hexToInt, Widget } from 'editor'
2 | import type { Shapes } from 'gfx'
3 | import { Sigui } from 'sigui'
4 |
5 | export type HoverMarkWidget = ReturnType
6 |
7 | export function HoverMarkWidget(shapes: Shapes) {
8 | using $ = Sigui()
9 |
10 | const info = $({
11 | color: '#fff',
12 | })
13 |
14 | const widget = Widget()
15 | const box = shapes.Box(widget.rect)
16 | box.view.color = hexToInt(info.color)
17 | box.view.alpha = .2
18 |
19 | function dispose() {
20 | box.remove()
21 | }
22 |
23 | return { info, widget, box, dispose }
24 | }
25 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error-sub.ts'
2 | export * from './hover-mark.ts'
3 | export * from './list-mark.ts'
4 | export * from './rms-deco.ts'
5 | export * from './wave-canvas-deco.ts'
6 | export * from './wave-gl-deco.ts'
7 | export * from './wave-svg-deco.tsx'
8 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/list-mark.ts:
--------------------------------------------------------------------------------
1 | import { hexToInt, Widget, type Pane } from 'editor'
2 | import { Sigui } from 'sigui'
3 | import { assign } from 'utils'
4 | import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts'
5 | import type { SoundValue } from '~/src/as/dsp/shared.ts'
6 | import type { Token } from '~/src/lang/tokenize.ts'
7 | import { modWrap } from '~/src/util/mod-wrap.ts'
8 |
9 | export type ListMarkWidget = ReturnType
10 |
11 | export function ListMarkWidget(pane: Pane) {
12 | using $ = Sigui()
13 |
14 | const info = $({
15 | color: '#fff',
16 | list: [] as Token.Bounds[],
17 | indexValue$: -1,
18 | value: -1,
19 | })
20 |
21 | const widget = Widget()
22 | const box = pane.draw.shapes.Box(widget.rect)
23 | box.view.color = hexToInt(info.color)
24 | box.view.alpha = .2
25 |
26 | function update(values: SoundValue[], audios: Float32Array[], scalars: Float32Array) {
27 | const value = values[info.indexValue$]
28 | if (value.kind === SoundValueKind.Scalar) {
29 | info.value = scalars[value.ptr]
30 | }
31 | else if (value.kind === SoundValueKind.Audio) {
32 | info.value = audios[value.ptr][0]
33 | }
34 | assign(widget.bounds, info.list[modWrap(info.value, info.list.length) >> 0])
35 | pane.draw.updateMarkRect(widget)
36 | }
37 |
38 | function dispose() {
39 | box.remove()
40 | }
41 |
42 | return { info, widget, box, update, dispose }
43 | }
44 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/rms-deco.ts:
--------------------------------------------------------------------------------
1 | import { hexToInt, Widget } from 'editor'
2 | import { Rect, type Shapes } from 'gfx'
3 | import { wasm } from 'rms'
4 | import { Sigui } from 'sigui'
5 | import { getMemoryView } from 'utils'
6 | import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts'
7 | import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts'
8 | import { ShapeOpts } from '~/as/assembly/gfx/sketch-shared.ts'
9 | import type { SoundValue } from '~/src/as/dsp/shared.ts'
10 |
11 | export type RmsDecoWidget = ReturnType
12 |
13 | export function RmsDecoWidget(shapes: Shapes) {
14 | using $ = Sigui()
15 |
16 | const info = $({
17 | rect: Rect(),
18 | index: -1,
19 | value$: -1,
20 | color: '#fff',
21 | peak: 0,
22 | value: 0,
23 | })
24 |
25 | const rmsFloats = getMemoryView(wasm.memory).getF32(+wasm.floats, BUFFER_SIZE)
26 | const widget = Widget()
27 | const box = shapes.Box(info.rect)
28 | box.view.opts |= ShapeOpts.Collapse | ShapeOpts.NoMargin
29 | box.view.color = hexToInt(info.color)
30 |
31 | $.fx(() => {
32 | const { rect } = info
33 | const { pr, x, y, w, h } = widget.rect
34 | $()
35 | rect.x = x + 3
36 | rect.y = y
37 | rect.w = 4
38 | rect.h = h
39 | })
40 |
41 | $.fx(() => {
42 | const { rect, value } = info
43 | const { y, h } = widget.rect
44 | $()
45 | rect.h = value * h
46 | rect.y = y + h - rect.h
47 | })
48 |
49 | function update(values: SoundValue[], audios: Float32Array[], scalars: Float32Array) {
50 | const value = values[info.value$]
51 | if (value.kind === SoundValueKind.Scalar) {
52 | rmsFloats.fill(scalars[value.ptr])
53 | }
54 | else if (value.kind === SoundValueKind.Audio) {
55 | rmsFloats.set(audios[value.ptr])
56 | }
57 | info.value = wasm.run()
58 | }
59 |
60 | function dispose() {
61 | box.remove()
62 | }
63 |
64 | return { info, widget, box, update, dispose }
65 | }
66 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/wave-canvas-deco.ts:
--------------------------------------------------------------------------------
1 | import { Widget } from 'editor'
2 | import { Sigui } from 'sigui'
3 | import { clamp } from 'utils'
4 | import { screen } from '~/src/screen.ts'
5 |
6 | export function WaveCanvasWidget() {
7 | using $ = Sigui()
8 |
9 | const info = $({
10 | floats: new Float32Array(),
11 | color: '#f09',
12 | })
13 |
14 | const widget = Widget()
15 | widget.draw = c => {
16 | const { floats, color } = info
17 | const { rect } = widget
18 | const { pr } = screen
19 |
20 | const coeff = floats.length / rect.w
21 | const startIndex = 0
22 | const scaleY = 1
23 | const width = rect.w
24 | const height = rect.h
25 | const step = .5
26 |
27 | const startX = (startIndex / coeff) | 0
28 |
29 | c.save()
30 |
31 | c.translate(rect.x, rect.y)
32 |
33 | let x = 0, y, h
34 |
35 | h = clamp(-1, 1, floats[(x + startX) * coeff | 0]!) * scaleY * 0.5 + 0.5
36 | y = (height - pr) * h + pr / 2
37 | c.beginPath()
38 | c.moveTo(x, y)
39 |
40 | x += step
41 |
42 | for (; x < width; x += step) {
43 | h = clamp(-1, 1, floats[(x + startX) * coeff | 0]!) * scaleY * 0.5 + 0.5
44 | y = (height - pr) * h + pr / 2
45 | c.lineTo(x, y)
46 | }
47 |
48 | c.lineWidth = 1
49 | c.strokeStyle = color
50 | c.stroke()
51 |
52 | c.restore()
53 | }
54 |
55 | return { info, widget }
56 | }
57 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/wave-gl-deco.ts:
--------------------------------------------------------------------------------
1 | import { Widget } from 'editor'
2 | import { wasm, type Rect, type Shapes } from 'gfx'
3 | import { Sigui } from 'sigui'
4 | import type { SoundValue } from '~/src/as/dsp/shared.ts'
5 | import { hexToInt } from '~/src/ui/editor/util/rgb.ts'
6 | import { Stabilizer } from '~/src/util/stabilizer.ts'
7 |
8 | export type WaveGlDecoWidget = ReturnType
9 |
10 | export function WaveGlDecoWidget(shapes: Shapes, rect?: Rect) {
11 | using $ = Sigui()
12 |
13 | const info = $({
14 | index: -1,
15 | resultValue: null as null | SoundValue,
16 | previewFloats: wasm.alloc(Float32Array, 0),
17 | floats: wasm.alloc(Float32Array, 0),
18 | stabilizer: new Stabilizer(),
19 | stabilizerTemp: wasm.alloc(Float32Array, 0),
20 | color: '#fff',
21 | })
22 |
23 | const widget = Widget(rect)
24 | const wave = shapes.Wave(widget.rect)
25 |
26 | $.fx(() => {
27 | const { floats, color } = info
28 | const { w } = widget.rect
29 | $()
30 | wave.view.floats$ = floats.ptr
31 | wave.view.color = hexToInt(color)
32 | wave.view.coeff = floats.length / w
33 | wave.view.lw = 1
34 | wave.view.len = floats.length
35 | })
36 |
37 | function dispose() {
38 | wave.remove()
39 | }
40 |
41 | return { info, widget, dispose }
42 | }
43 |
--------------------------------------------------------------------------------
/src/ui/editor/widgets/wave-svg-deco.tsx:
--------------------------------------------------------------------------------
1 | import { Widget } from 'editor'
2 | import { Sigui } from 'sigui'
3 | import { clamp } from 'utils'
4 |
5 | export function WaveSvgWidget() {
6 | using $ = Sigui()
7 |
8 | const info = $({
9 | floats: new Float32Array(),
10 | color: '#f09',
11 | })
12 |
13 | const path = as SVGElement
22 |
23 | const widget = Widget()
24 | const { rect } = widget
25 |
26 | const svg = as SVGElement
33 |
34 | widget.draw = c => {
35 | const { floats } = info
36 | const { w, h } = widget.rect
37 |
38 | let d = ''
39 | const coeff = floats.length / w
40 | const startIndex = 0
41 | const scaleY = h
42 | const step = .5
43 |
44 | const startX = (startIndex / coeff) | 0
45 |
46 | let x = 0, y
47 |
48 | y = clamp(-1, 1, floats[(x + startX) * coeff | 0]!) * scaleY * 0.5 + 0.5
49 | d += `M ${x} ${y}`
50 |
51 | x += step
52 |
53 | for (; x < w; x += step) {
54 | y = clamp(-1, 1, floats[(x + startX) * coeff | 0]!) * scaleY * 0.5 + 0.5
55 | d += `L ${x} ${y}`
56 | }
57 |
58 | path.setAttribute('d', d)
59 | }
60 |
61 | return { info, widget, svg }
62 | }
63 |
--------------------------------------------------------------------------------
/src/ui/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button.tsx'
2 | export * from './Canvas.tsx'
3 | export * from './DropDown.tsx'
4 | export * from './Editor.tsx'
5 | export * from './Fieldset.tsx'
6 | export * from './Heading.tsx'
7 | export * from './Input.tsx'
8 | export * from './Label.tsx'
9 | export * from './Layout.tsx'
10 | export * from './Link.tsx'
11 |
12 |
--------------------------------------------------------------------------------
/src/util/copy-ring-into.ts:
--------------------------------------------------------------------------------
1 | import { memoize } from 'utils'
2 |
3 | export const getRingOffsets = memoize(function getRingOffsets(ringPos: number, length: number, max: number) {
4 | const a = (max - ringPos) * 128
5 | const b = length
6 | const c = 0
7 | const d = (ringPos + 1) * 128
8 |
9 | const e = 0
10 | const f = a
11 | const g = d
12 | const h = length
13 |
14 | return [a, b, c, d, e, f, g, h]
15 | })
16 |
17 | export function copyRingInto(target: Float32Array, source: Float32Array, ringPos: number, length: number, max: number) {
18 | const [a, b, c, d, e, f, g, h] = getRingOffsets(ringPos, length, max)
19 | target.set(
20 | source.subarray(c, d),
21 | a
22 | )
23 | target.set(
24 | source.subarray(g, h),
25 | e
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/util/mod-wrap.ts:
--------------------------------------------------------------------------------
1 | export function modWrap(x: number, N: number): number {
2 | return (x % N + N) % N
3 | }
4 |
--------------------------------------------------------------------------------
/src/util/parse-form.test.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { parseForm } from './parse-form.ts'
3 |
4 | describe('parseForm', () => {
5 | it('with form element', () => {
6 | const schema = z.object({
7 | foo: z.string(),
8 | bar: z.string(),
9 | })
10 |
11 | const form = as HTMLFormElement
15 |
16 | const obj = parseForm(form, schema)
17 |
18 | expect(obj).toEqual({
19 | foo: 'foo',
20 | bar: 'bar',
21 | })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/src/util/parse-form.ts:
--------------------------------------------------------------------------------
1 | import type { z } from 'zod'
2 |
3 | export function parseForm(el: HTMLFormElement, schema: T) {
4 | const form = new FormData(el)
5 | const data = Object.fromEntries(form.entries())
6 | return schema.parse(data) as z.infer
7 | }
8 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | body {
7 | @apply bg-neutral-900 text-neutral-400 font-sans font-normal;
8 | }
9 | h1 {
10 | @apply text-2xl;
11 | }
12 | h2 {
13 | @apply text-xl;
14 | }
15 | h3 {
16 | @apply text-lg font-bold;
17 | }
18 | legend {
19 | @apply text-xl;
20 | }
21 | input {
22 | @apply min-h-8 pl-2 pr-2 bg-neutral-700;
23 | }
24 | input {
25 | @apply border-2 border-neutral-500 border-t-neutral-800 border-l-neutral-800
26 | hover:border-neutral-400 hover:border-t-neutral-600
27 | hover:border-l-neutral-600 placeholder-neutral-500;
28 | }
29 | a {
30 | @apply text-orange-600 hover:text-neutral-100 hover:underline;
31 | }
32 | table {
33 | @apply border-collapse text-xs bg-neutral-900;
34 | }
35 | tr {
36 | @apply even:bg-neutral-800;
37 | }
38 | td, th {
39 | @apply p-1.5 border border-orange-900;
40 | }
41 | }
42 |
43 | input::placeholder {
44 | font-style: italic;
45 | }
46 | input:-webkit-autofill,
47 | input:-webkit-autofill:hover,
48 | input:-webkit-autofill:focus,
49 | input:-webkit-autofill:active {
50 | -webkit-box-shadow: 0 0 0 30px #666 inset !important;
51 | -webkit-text-fill-color: #ccc !important;
52 | }
53 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import imageRendering from 'tailwindcss-image-rendering'
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | content: [
6 | './index.html',
7 | './src/**/*.{js,ts,jsx,tsx}',
8 | './admin/index.html',
9 | './admin/**/*.{js,ts,jsx,tsx}',
10 | ],
11 | theme: {
12 | fontFamily: {
13 | sans: ['"Fustat"', 'sans-serif'],
14 | mono: ['"IBM Plex Mono"', 'monospace'],
15 | },
16 | extend: {},
17 | },
18 | plugins: [
19 | imageRendering(),
20 | ],
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "admin",
4 | "generated/typescript",
5 | "lib",
6 | "scripts",
7 | "src",
8 | "vendor"
9 | ],
10 | "compilerOptions": {
11 | "rootDir": ".",
12 | "baseUrl": ".",
13 | "lib": [
14 | "ESNext",
15 | "DOM",
16 | "DOM.Iterable"
17 | ],
18 | "noEmit": true,
19 | "module": "ESNext",
20 | "target": "ESNext",
21 | "moduleResolution": "node",
22 | "resolveJsonModule": true,
23 | "allowSyntheticDefaultImports": true,
24 | "allowImportingTsExtensions": true,
25 | "jsx": "react-jsx",
26 | "jsxImportSource": "sigui",
27 | "useDefineForClassFields": true,
28 | "exactOptionalPropertyTypes": true,
29 | "experimentalDecorators": true,
30 | "emitDecoratorMetadata": true,
31 | "esModuleInterop": true,
32 | "noImplicitAny": true,
33 | "noErrorTruncation": true,
34 | "noImplicitThis": true,
35 | // "noUncheckedIndexedAccess": true,
36 | // "isolatedModules": true,
37 | // "inlineSources": true,
38 | // "declaration": true,
39 | // "noUnusedLocals": true,
40 | "skipDefaultLibCheck": true,
41 | "skipLibCheck": true,
42 | "sourceMap": true,
43 | "allowJs": true,
44 | "strict": true,
45 | "paths": {
46 | "ui": [
47 | "./src/ui/index.ts"
48 | ],
49 | "lang": [
50 | "./src/lang/index.ts"
51 | ],
52 | "editor": [
53 | "./src/ui/editor/index.ts"
54 | ],
55 | "dsp": [
56 | "src/as/dsp/index.ts"
57 | ],
58 | "rms": [
59 | "src/as/dsp/rms.ts"
60 | ],
61 | "gfx": [
62 | "./src/as/gfx/index.ts"
63 | ],
64 | "~/*": [
65 | "./*"
66 | ]
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/vendor/as-transform-unroll.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | import { TransformVisitor, utils } from 'visitor-as'
3 | const { not, isStdlib } = utils
4 | class UnrollTransform extends TransformVisitor {
5 | visitBlockStatement(node) {
6 | if (node.statements.length >= 1) {
7 | if (node.statements[0]?.expression?.expression?.text === 'unroll') {
8 | const args = node.statements[0].expression.args
9 | const body = args[1].declaration.body
10 | const times = args[0].value.low
11 | const res = body
12 | res.range = node.range
13 | body.statements = Array.from({ length: times }, () =>
14 | body.statements
15 | ).flat()
16 | return super.visitBlockStatement(res)
17 | }
18 | }
19 | return super.visitBlockStatement(node)
20 | }
21 | afterParse(parser) {
22 | const sources = parser.sources.filter(not(isStdlib))
23 | this.visit(sources)
24 | }
25 | }
26 | export default UnrollTransform
27 |
--------------------------------------------------------------------------------
/vendor/util.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | function replaceNonAlphaNumeric(x, replaceValue) {
3 | return x.replaceAll(/[^a-z0-9]/ig, replaceValue);
4 | }
5 | export function cleanupExportName(x) {
6 | x = replaceNonAlphaNumeric(x.split("assembly/").slice(1).join("_"), "_");
7 | const parts = x.split("_");
8 | const [pre, name, fn, ...methodParts] = parts;
9 | if (!fn) {
10 | return x;
11 | } else if (fn.toLowerCase() !== name) {
12 | return `${pre === "gen" ? `${pre}_` : ""}${name}_${[fn, ...methodParts].join("_")}`;
13 | } else {
14 | return `${pre === "gen" ? `${pre}_` : ""}${name}_${methodParts.join("_")}`;
15 | }
16 | }
17 | export const capitalize = (s) => s[0].toUpperCase() + s.slice(1);
18 | export const extendsRegExp = /extends\s([^\s]+)/;
19 | export function sortCompareKeys([a], [b]) {
20 | return a < b ? -1 : a > b ? 1 : 0;
21 | }
22 | export function sortObjectInPlace(data) {
23 | const sorted = Object.fromEntries(
24 | Object.entries(data).sort(sortCompareKeys)
25 | );
26 | for (const key in data) {
27 | delete data[key];
28 | }
29 | Object.assign(data, sorted);
30 | return data;
31 | }
32 |
--------------------------------------------------------------------------------
/vendor/vite-plugin-bundle-url.ts:
--------------------------------------------------------------------------------
1 | import { build, createLogger, type Plugin, type UserConfig } from 'vite'
2 |
3 | export function ViteBundleUrl({ plugins }: { plugins: (Plugin | Plugin[])[] }): Plugin {
4 | let viteConfig: UserConfig
5 |
6 | return {
7 | name: 'vite-plugin-bundle-url',
8 | apply: 'build',
9 | enforce: 'post',
10 |
11 | config(config) {
12 | viteConfig = config
13 | },
14 |
15 | async transform(_code, id) {
16 | if (!id.endsWith('.ts?url')) return
17 |
18 | const quietLogger = createLogger()
19 | quietLogger.info = () => undefined
20 |
21 | const output = await build({
22 | ...viteConfig,
23 | configFile: false,
24 | clearScreen: false,
25 | customLogger: quietLogger,
26 | plugins: [],
27 | build: {
28 | ...viteConfig?.build,
29 | lib: {
30 | entry: id.replace('?url', ''),
31 | name: '_',
32 | formats: ['iife'],
33 | },
34 | rollupOptions: {
35 | plugins
36 | },
37 | write: false,
38 | },
39 | })
40 |
41 | if (!Array.isArray(output)) return
42 |
43 | const iife = output[0].output[0].code
44 | const encoded = Buffer.from(iife, 'utf8').toString('base64')
45 | const transformed = `export default "data:text/javascript;base64,${encoded}";`
46 |
47 | console.log(
48 | `[bundle-url] ${id} (${transformed.length} bytes)`,
49 | )
50 |
51 | return transformed
52 | },
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/vendor/vite-plugin-cors-coop-coep.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from 'vite'
2 |
3 | export function ViteCorsCoopCoep(): Plugin {
4 | return {
5 | name: 'cors-coop-coep',
6 | configureServer(server) {
7 | server.middlewares.use((req, res, next) => {
8 | res.setHeader('access-control-allow-origin', req.headers.origin ?? '*')
9 | res.setHeader('cross-origin-opener-policy', 'same-origin')
10 | res.setHeader('cross-origin-embedder-policy', 'require-corp')
11 | next()
12 | })
13 | },
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/vendor/vite-plugin-hex-loader.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 | import type { Plugin } from 'vite'
3 |
4 | export function ViteHexLoader(): Plugin {
5 | return {
6 | name: 'hex-loader',
7 | transform(code, id) {
8 | const [path, query] = id.split('?')
9 | if (query != 'raw-hex')
10 | return null
11 |
12 | const data = fs.readFileSync(path)
13 | const hex = data.toString('hex')
14 |
15 | return `export default '${hex}';`
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/vendor/vite-plugin-open-in-editor.ts:
--------------------------------------------------------------------------------
1 | import os from 'node:os'
2 | import path from 'node:path'
3 | import { Plugin } from 'vite'
4 |
5 | // @ts-ignore
6 | import openInEditor from 'open-in-editor'
7 |
8 | const editor: { open(filename: string): Promise } = openInEditor.configure({
9 | editor: 'code',
10 | dotfiles: 'allow',
11 | })
12 |
13 | export const ViteOpenInEditor = (): Plugin => ({
14 | name: 'open-in-editor',
15 | configureServer(server) {
16 | server.middlewares.use(async (req, res, next) => {
17 | if (req.method !== 'POST') return next()
18 |
19 | const fsPath = req.url!.slice(1).replace('@fs', '')
20 | const homedir = os.homedir()
21 |
22 | console.log('[open-in-editor]', fsPath)
23 |
24 | let filename: string
25 | if (fsPath.startsWith(homedir)) {
26 | filename = fsPath
27 | }
28 | else {
29 | filename = path.join(process.cwd(), fsPath)
30 | }
31 | try {
32 | await editor.open(filename)
33 | }
34 | catch (error) {
35 | res.statusCode = 500
36 | res.end((error as Error).message)
37 | return
38 | }
39 |
40 | res.statusCode = 200
41 | res.setHeader('content-type', 'text/html')
42 | res.end('')
43 | })
44 | },
45 | })
46 |
--------------------------------------------------------------------------------
/vendor/vite-plugin-print-address.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from 'vite'
2 | import os from 'node:os'
3 | // @ts-ignore
4 | import qrcode from 'qrcode-terminal'
5 |
6 | export function getNetworkAddress(options: { port: string }) {
7 | for (const addresses of Object.values(os.networkInterfaces())) {
8 | for (const address of addresses!) {
9 | const { address: host, family, internal } = address
10 | if (!internal && family === 'IPv4') {
11 | return `https://${host}:${options.port}`
12 | }
13 | }
14 | }
15 | return '-'
16 | }
17 |
18 | export const VitePrintAddress = (): Plugin => ({
19 | name: 'print-address',
20 | configureServer(server) {
21 | let its = 0
22 | let timeout: any
23 | const printUrls = (origin: string) => {
24 | clearTimeout(timeout)
25 | const url = new URL(origin)
26 | const { port } = url
27 | timeout = setTimeout(() => {
28 | const network = getNetworkAddress({ port })
29 | its++ % 5 === 0 && qrcode.generate(network, { small: true })
30 | console.log(url.href)
31 | console.log(network)
32 | }, 500)
33 | }
34 | server.middlewares.use(async (req, res, next) => {
35 | if (req.headers.origin) printUrls(req.headers.origin)
36 | next()
37 | })
38 | },
39 | })
40 |
--------------------------------------------------------------------------------