├── .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 | 2 | 3 | 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
24 |
25 | 33 | 34 | 42 | 43 | 51 | 52 |
53 | 54 |
55 | 56 | {() => info.error} 57 |
58 |
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 |
42 | 43 | 44 | 47 | 48 |
49 | 50 | 51 | 52 | {() => info.error} 53 |
: 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 |
17 | {() => state.currentChannel?.users.map(user => 18 |
19 | {nick !== user.nick ? 20 | onUserClick(user.nick)} 23 | > 24 | {user.nick} 25 | 26 | : {user.nick} 27 | } 28 |
29 | )} 30 |
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 |
{ 30 | ev.preventDefault() 31 | const { nick } = parseForm(ev.target as HTMLFormElement, formSchema) 32 | oauth.registerOAuth(id, nick) 33 | .then(() => go('/oauth/complete')) 34 | .catch(error => info.error = error.message) 35 | }}> 36 | info.nick} spellcheck="false" required autocomplete="nickname" /> 37 | 38 |
39 | {() => info.error} 40 |
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 |
30 | 31 | 32 |
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
3 | {legend} 4 |
5 | {children} 6 |
7 |
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 = rect.x} 28 | y={() => rect.y} 29 | width={() => rect.width} 30 | height={() => rect.height} 31 | viewBox={() => `-1 -${rect.h / 2} ${rect.w} ${rect.h + 1}`} 32 | >{path} 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 =
12 | 13 | 14 |
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 | --------------------------------------------------------------------------------