From fe16dfb76e08567d5e9b81ec213e9a632603f28a Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 15 Oct 2024 03:57:52 +0300 Subject: [PATCH 01/33] feat(api): use crypto uuid --- api/auth/actions.ts | 8 ++++---- api/oauth/routes/common.ts | 3 +-- api/oauth/routes/github.ts | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/api/auth/actions.ts b/api/auth/actions.ts index c74ff90..e7a62c5 100644 --- a/api/auth/actions.ts +++ b/api/auth/actions.ts @@ -1,6 +1,6 @@ // deno-lint-ignore-file require-await import { hash } from 'jsr:@denorg/scrypt@4.4.4' -import { createCookie, randomHash, timeout } from 'utils' +import { createCookie, timeout } from 'utils' import { ADMINS } from '~/api/admin/actions.ts' import { UserLogin, UserRegister, UserSession } from "~/api/auth/types.ts" import { kv } from '~/api/core/app.ts' @@ -60,7 +60,7 @@ export async function getUser(nickOrEmail: string) { export async function loginUser(ctx: Context, nick: string) { ctx.log('Login:', nick) - const sessionId = randomHash() + const sessionId = crypto.randomUUID() const sessionKey = ['session', sessionId] const now = new Date() @@ -90,7 +90,7 @@ export async function loginUser(ctx: Context, nick: string) { } async function generateEmailVerificationToken(email: string) { - const token = randomHash() + const token = crypto.randomUUID() const now = new Date() const expires = new Date(now) const expireAfterHours = 3 * 24 // 3 days @@ -264,7 +264,7 @@ export async function forgotPassword(ctx: Context, email: string) { return } - const token = randomHash() + const token = crypto.randomUUID() const now = new Date() const expires = new Date(now) const expireAfterMinutes = 15 diff --git a/api/oauth/routes/common.ts b/api/oauth/routes/common.ts index 476dafa..6fd0dc2 100644 --- a/api/oauth/routes/common.ts +++ b/api/oauth/routes/common.ts @@ -1,4 +1,3 @@ -import { randomHash } from 'utils' import { z } from 'zod' import { kv } from '~/api/core/app.ts' import { Router } from '~/api/core/router.ts' @@ -30,7 +29,7 @@ export function mount(app: Router) { provider, }) - const oauthStateId = randomHash() + const oauthStateId = crypto.randomUUID() await kv.set(['oauthState', oauthStateId], state, { expireIn: 30 * 60 * 1000 }) diff --git a/api/oauth/routes/github.ts b/api/oauth/routes/github.ts index 22e1863..9c6d39b 100644 --- a/api/oauth/routes/github.ts +++ b/api/oauth/routes/github.ts @@ -1,4 +1,3 @@ -import { randomHash } from 'utils' import { z } from 'zod' import { getUserByEmail, loginUser } from '~/api/auth/actions.ts' import { kv } from '~/api/core/app.ts' @@ -104,7 +103,7 @@ export function mount(app: Router) { } // create oauth session - const id = randomHash() + const id = crypto.randomUUID() const now = new Date() const expires = new Date(now) expires.setMinutes(expires.getMinutes() + 30) From 9469695bb2da5e58591ed5d20bf65a66207a8f8b Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 15 Oct 2024 05:13:54 +0300 Subject: [PATCH 02/33] wip assemblyscript --- src/as/pkg/worker.ts | 1 + src/as/pkg/worklet.ts | 2 +- src/pages/App.tsx | 12 ++++++++++-- src/pages/AssemblyScript.tsx | 31 ++++++++++++++++++++++++------- src/pages/EditorDemo.tsx | 4 ++-- src/state.ts | 20 ++++++++++++++++++++ src/ui/Editor.tsx | 6 ++++-- src/ui/Input.tsx | 10 ++++++++-- src/ui/editor/draw.ts | 1 + src/ui/editor/view.tsx | 4 ++-- 10 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/as/pkg/worker.ts b/src/as/pkg/worker.ts index 9bfe2c4..8f5ecaa 100644 --- a/src/as/pkg/worker.ts +++ b/src/as/pkg/worker.ts @@ -16,6 +16,7 @@ const worker = { } }, async multiply(a: number, b: number) { + console.log('multiply', a, b) return pkg.multiply(a, b) } } diff --git a/src/as/pkg/worklet.ts b/src/as/pkg/worklet.ts index a28047d..9017e07 100644 --- a/src/as/pkg/worklet.ts +++ b/src/as/pkg/worklet.ts @@ -1,6 +1,6 @@ import { getMemoryView, wasmSourceMap } from 'utils' import { BUFFER_SIZE } from '~/as/assembly/pkg/constants.ts' -import type { __AdaptedExports as WasmExports } from '~/as/build/pkg-nort' +import type { __AdaptedExports as WasmExports } from '~/as/build/pkg-nort.d.ts' import hex from '~/as/build/pkg-nort.wasm?raw-hex' import { Out, PlayerMode } from '~/src/as/pkg/shared.ts' diff --git a/src/pages/App.tsx b/src/pages/App.tsx index a5d0635..b6fe7dc 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -18,7 +18,7 @@ import { UiShowcase } from '~/src/pages/UiShowcase.tsx' import { WebGLDemo } from '~/src/pages/WebGLDemo.tsx' import { WebSockets } from '~/src/pages/WebSockets.tsx' import { whoami } from '~/src/rpc/auth.ts' -import { state } from '~/src/state.ts' +import { state, triggers } from '~/src/state.ts' import { go, Link } from '~/src/ui/Link.tsx' export function App() { @@ -29,7 +29,7 @@ export function App() { const info = $({ bg: 'transparent', canvasWidth: state.$.containerWidth, - canvasHeight: 800, + canvasHeight: state.$.containerHeight, }) const router = CachingRouter({ @@ -78,6 +78,14 @@ export function App() { }), ]) + $.fx(() => { + const { user } = state + $() + $.flush() + triggers.resize++ + requestAnimationFrame(() => triggers.resize++) + }) + return
info.bg = '#433'} diff --git a/src/pages/AssemblyScript.tsx b/src/pages/AssemblyScript.tsx index bf9d40d..a78170c 100644 --- a/src/pages/AssemblyScript.tsx +++ b/src/pages/AssemblyScript.tsx @@ -2,7 +2,7 @@ import { Sigui } from 'sigui' import { Player } from '~/src/as/pkg/player.ts' import { PkgService } from '~/src/as/pkg/service.ts' import pkg from '~/src/as/pkg/wasm.ts' -import { Button } from '~/src/ui/index.ts' +import { Button, Input } from '~/src/ui/index.ts' let audioContext: AudioContext @@ -10,7 +10,9 @@ export function AssemblyScript() { using $ = Sigui() const info = $({ - fromWorker: null as null | number + fromWorker: null as null | number, + n1: 2, + n2: 3, }) audioContext ??= new AudioContext() @@ -22,10 +24,11 @@ export function AssemblyScript() { $.fx(() => () => pkgService.terminate()) $.fx(() => { + const { n1, n2 } = info const { pkg } = $.of(pkgService.info) - $() - pkgService.service.multiply(2, 3) - .then(result => info.fromWorker = result) + $().then(async () => { + info.fromWorker = await pkgService.service.multiply(n1, n2) + }) }) return
@@ -33,9 +36,23 @@ export function AssemblyScript() {
Direct: {pkg.multiply(2, 3)}
- Worker: {() => info.fromWorker} +
+ Worker: { + info.n1 = +(ev.target as HTMLInputElement).value + }} + class="w-8" + value={() => info.n1} + /> + { + info.n2 = +(ev.target as HTMLInputElement).value + }} + class="w-8" + value={() => info.n2} + /> = {() => info.fromWorker} +

- + Worklet:
} diff --git a/src/pages/EditorDemo.tsx b/src/pages/EditorDemo.tsx index 7242f2b..6b61b90 100644 --- a/src/pages/EditorDemo.tsx +++ b/src/pages/EditorDemo.tsx @@ -314,8 +314,8 @@ export function EditorDemo({ width, height }: { const paneSvg = pane.dims.info.rect.w} + height={() => pane.dims.info.rect.h} viewBox={() => `${-pane.dims.info.scrollX} ${-pane.dims.info.scrollY} ${pane.dims.info.rect.w} ${pane.dims.info.rect.h}`} /> as SVGSVGElement diff --git a/src/state.ts b/src/state.ts index 36a0b12..9e5fa60 100644 --- a/src/state.ts +++ b/src/state.ts @@ -9,10 +9,16 @@ import { env } from '~/src/env.ts' import { screen } from '~/src/screen.ts' import { link } from '~/src/ui/Link.tsx' +export const triggers = $({ + resize: 0, +}) + class State { // container container: HTMLElement | null = null + get containerWidth(): number { + const { resize } = triggers const { width } = screen const { container } = this if (!container) return width @@ -23,6 +29,20 @@ class State { - parseFloat(style.paddingRight) } + get containerHeight(): number { + const { resize } = triggers + const { height } = screen + const { container } = this + if (!container) return height + let h = 0 + const tagNames = ['header', 'article'] + tagNames.forEach(tagName => { + const el = container.getElementsByTagName(tagName)[0] as HTMLElement + h += el.getBoundingClientRect().height + }) + return height - h + } + // url url: typeof link.$.url get pathname(): string { diff --git a/src/ui/Editor.tsx b/src/ui/Editor.tsx index 966499e..50e11b3 100644 --- a/src/ui/Editor.tsx +++ b/src/ui/Editor.tsx @@ -1,6 +1,8 @@ -import { Input, Misc, Pane, Rect, View, type InputHandlers, type InputMouse, type Linecol, type WordWrapProcessor } from 'editor' +import { Input, Misc, Pane, Rect, View, type InputHandlers, type WordWrapProcessor } from 'editor' import { Sigui, type $, type Signal } from 'sigui' import type { Source, Token } from '~/src/lang/tokenize.ts' +import { screen } from '~/src/screen.ts' +import { state } from '~/src/state.ts' export function Editor({ code, width, height, colorize, tokenize, wordWrapProcessor, inputHandlers }: { code: Signal @@ -18,7 +20,7 @@ export function Editor({ code, width, height, colorize, tokenize, wordWrapProces // initial pane const pane = createPane({ - rect: $(Rect(), { x: 20, y: 20, w: 193, h: 200 }), + rect: $(Rect(), { x: 0, y: 0, w: width, h: height }), code, }) diff --git a/src/ui/Input.tsx b/src/ui/Input.tsx index 94ddcdb..62c5e24 100644 --- a/src/ui/Input.tsx +++ b/src/ui/Input.tsx @@ -1,6 +1,12 @@ -export function Input(props: Record) { +import { Sigui, type Signal } from 'sigui' +import { cn } from '~/lib/cn.ts' + +export function Input(props: Record) { return } diff --git a/src/ui/editor/draw.ts b/src/ui/editor/draw.ts index ecd8769..917a6fa 100644 --- a/src/ui/editor/draw.ts +++ b/src/ui/editor/draw.ts @@ -328,6 +328,7 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize // trigger draw $.fx(() => { + const { width, height } = view.info const { c, pr, rect: { x, y, w, h }, showScrollbars } = $.of(info) const { isHovering, diff --git a/src/ui/editor/view.tsx b/src/ui/editor/view.tsx index f5e6a78..43b2966 100644 --- a/src/ui/editor/view.tsx +++ b/src/ui/editor/view.tsx @@ -19,11 +19,11 @@ export function View({ width, height }: { svgs: new Set(), }) - const canvas = as HTMLCanvasElement + const canvas = as HTMLCanvasElement const glCanvas = as HTMLCanvasElement - const svg = + const svg = info.width} height={() => info.height} class="absolute top-0 left-0" > {() => [...info.svgs]} as SVGSVGElement From f38e3bf54d244763819f72d7d7cabdba99abd25f Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 15 Oct 2024 06:15:31 +0300 Subject: [PATCH 03/33] vite public dir --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index 52d8902..aca76e8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -131,6 +131,7 @@ export default ({ mode }) => { return defineConfig({ clearScreen: false, + publicDir: './public', server: { watch: { // KEEP: this option fixes hmr for an unknown reason From a25d821a4eea3805f92410163ddd964b31c57b3a Mon Sep 17 00:00:00 2001 From: stagas Date: Wed, 16 Oct 2024 08:25:56 +0300 Subject: [PATCH 04/33] wip dsp --- as/assembly/dsp/constants.ts | 8 + as/assembly/dsp/core/antialias-wavetable.ts | 155 +++++ as/assembly/dsp/core/clock.ts | 54 ++ as/assembly/dsp/core/constants-internal.ts | 3 + as/assembly/dsp/core/constants.ts | 4 + as/assembly/dsp/core/engine.ts | 36 ++ as/assembly/dsp/core/fft.ts | 141 +++++ as/assembly/dsp/core/wave.ts | 44 ++ as/assembly/dsp/core/wavetable.ts | 147 +++++ as/assembly/dsp/gen/adsr.ts | 90 +++ as/assembly/dsp/gen/aosc.ts | 26 + as/assembly/dsp/gen/atan.ts | 34 ++ as/assembly/dsp/gen/bap.ts | 10 + as/assembly/dsp/gen/bbp.ts | 10 + as/assembly/dsp/gen/bhp.ts | 10 + as/assembly/dsp/gen/bhs.ts | 11 + as/assembly/dsp/gen/biquad.ts | 270 +++++++++ as/assembly/dsp/gen/blp.ts | 10 + as/assembly/dsp/gen/bls.ts | 11 + as/assembly/dsp/gen/bno.ts | 10 + as/assembly/dsp/gen/bpk.ts | 11 + as/assembly/dsp/gen/clamp.ts | 40 ++ as/assembly/dsp/gen/clip.ts | 38 ++ as/assembly/dsp/gen/comp.ts | 103 ++++ as/assembly/dsp/gen/daverb.ts | 364 ++++++++++++ as/assembly/dsp/gen/dcc.ts | 46 ++ as/assembly/dsp/gen/dclip.ts | 31 + as/assembly/dsp/gen/dclipexp.ts | 33 ++ as/assembly/dsp/gen/dcliplin.ts | 39 ++ as/assembly/dsp/gen/delay.ts | 72 +++ as/assembly/dsp/gen/diode.ts | 162 +++++ as/assembly/dsp/gen/exp.ts | 7 + as/assembly/dsp/gen/freesound.ts | 10 + as/assembly/dsp/gen/gen.ts | 10 + as/assembly/dsp/gen/gendy.ts | 28 + as/assembly/dsp/gen/grain.ts | 28 + as/assembly/dsp/gen/inc.ts | 45 ++ as/assembly/dsp/gen/lp.ts | 46 ++ as/assembly/dsp/gen/mhp.ts | 35 ++ as/assembly/dsp/gen/mlp.ts | 35 ++ as/assembly/dsp/gen/moog.ts | 96 +++ as/assembly/dsp/gen/noi.ts | 7 + as/assembly/dsp/gen/nrate.ts | 20 + as/assembly/dsp/gen/osc.ts | 65 +++ as/assembly/dsp/gen/ramp.ts | 7 + as/assembly/dsp/gen/rate.ts | 17 + as/assembly/dsp/gen/sap.ts | 35 ++ as/assembly/dsp/gen/saw.ts | 7 + as/assembly/dsp/gen/say.ts | 10 + as/assembly/dsp/gen/sbp.ts | 35 ++ as/assembly/dsp/gen/shp.ts | 35 ++ as/assembly/dsp/gen/sin.ts | 7 + as/assembly/dsp/gen/slp.ts | 35 ++ as/assembly/dsp/gen/smp.ts | 135 +++++ as/assembly/dsp/gen/sno.ts | 35 ++ as/assembly/dsp/gen/spk.ts | 35 ++ as/assembly/dsp/gen/sqr.ts | 7 + as/assembly/dsp/gen/svf.ts | 89 +++ as/assembly/dsp/gen/tanh.ts | 32 + as/assembly/dsp/gen/tanha.ts | 63 ++ as/assembly/dsp/gen/tap.ts | 58 ++ as/assembly/dsp/gen/tri.ts | 7 + as/assembly/dsp/gen/zero.ts | 19 + as/assembly/dsp/graph/copy.ts | 75 +++ as/assembly/dsp/graph/dc-bias-old.ts | 42 ++ as/assembly/dsp/graph/dc-bias.ts | 43 ++ as/assembly/dsp/graph/fade.ts | 107 ++++ as/assembly/dsp/graph/fill.ts | 15 + as/assembly/dsp/graph/join.ts | 72 +++ as/assembly/dsp/graph/math.ts | 559 ++++++++++++++++++ as/assembly/dsp/graph/rms.ts | 92 +++ as/assembly/dsp/index.ts | 90 +++ as/assembly/dsp/vm/bin-op.ts | 35 ++ as/assembly/dsp/vm/dsp-shared.ts | 27 + as/assembly/dsp/vm/dsp.ts | 264 +++++++++ as/assembly/dsp/vm/sound.ts | 270 +++++++++ as/assembly/dsp/vm/template.ts | 18 + as/assembly/env.ts | 21 - as/assembly/gfx/draw.ts | 3 +- as/assembly/pkg/math.ts | 5 - as/assembly/util.ts | 243 ++++++++ as/tsconfig.json | 1 + asconfig-dsp.json | 35 ++ generated/assembly/dsp-factory.ts | 108 ++++ generated/assembly/dsp-offsets.ts | 110 ++++ generated/assembly/dsp-op.ts | 20 + generated/assembly/dsp-runner.ts | 136 +++++ generated/assembly/tsconfig.json | 6 + generated/typescript/dsp-gens.ts | 396 +++++++++++++ generated/typescript/dsp-vm.ts | 120 ++++ package.json | 2 + scripts/generate-dsp-vm.ts | 136 +++++ scripts/update-dsp-factory.ts | 35 ++ scripts/update-gens-offsets.ts | 21 + scripts/util.ts | 22 + src/as/dsp/constants.ts | 1 + src/as/dsp/dsp-service.ts | 36 ++ src/as/dsp/dsp-shared.ts | 21 + src/as/dsp/dsp-worker.ts | 158 +++++ src/as/dsp/dsp.ts | 617 ++++++++++++++++++++ src/as/dsp/index.ts | 3 + src/as/dsp/notes-shared.ts | 17 + src/as/dsp/params-shared.ts | 17 + src/as/dsp/util.ts | 32 + src/as/dsp/value.ts | 118 ++++ src/as/dsp/wasm.ts | 24 + src/as/gfx/wasm.ts | 1 - src/as/init-wasm.ts | 2 +- src/as/pkg/wasm.ts | 1 - src/comp/DspEditor.tsx | 243 ++++++++ src/lang/interpreter.ts | 403 +++++++++++++ src/lang/util.ts | 63 ++ src/pages/App.tsx | 2 + src/pages/DspDemo.tsx | 136 +++++ src/pages/Home.tsx | 1 + src/ui/Editor.tsx | 4 +- src/ui/editor/view.tsx | 6 +- src/ui/editor/widgets/wave-gl.ts | 2 + tsconfig.json | 5 + vendor/as-transform-update-dsp-gens.js | 95 +++ vendor/util.js | 31 + vite.config.ts | 21 + 122 files changed, 8278 insertions(+), 34 deletions(-) create mode 100644 as/assembly/dsp/constants.ts create mode 100644 as/assembly/dsp/core/antialias-wavetable.ts create mode 100644 as/assembly/dsp/core/clock.ts create mode 100644 as/assembly/dsp/core/constants-internal.ts create mode 100644 as/assembly/dsp/core/constants.ts create mode 100644 as/assembly/dsp/core/engine.ts create mode 100644 as/assembly/dsp/core/fft.ts create mode 100644 as/assembly/dsp/core/wave.ts create mode 100644 as/assembly/dsp/core/wavetable.ts create mode 100644 as/assembly/dsp/gen/adsr.ts create mode 100644 as/assembly/dsp/gen/aosc.ts create mode 100644 as/assembly/dsp/gen/atan.ts create mode 100644 as/assembly/dsp/gen/bap.ts create mode 100644 as/assembly/dsp/gen/bbp.ts create mode 100644 as/assembly/dsp/gen/bhp.ts create mode 100644 as/assembly/dsp/gen/bhs.ts create mode 100644 as/assembly/dsp/gen/biquad.ts create mode 100644 as/assembly/dsp/gen/blp.ts create mode 100644 as/assembly/dsp/gen/bls.ts create mode 100644 as/assembly/dsp/gen/bno.ts create mode 100644 as/assembly/dsp/gen/bpk.ts create mode 100644 as/assembly/dsp/gen/clamp.ts create mode 100644 as/assembly/dsp/gen/clip.ts create mode 100644 as/assembly/dsp/gen/comp.ts create mode 100644 as/assembly/dsp/gen/daverb.ts create mode 100644 as/assembly/dsp/gen/dcc.ts create mode 100644 as/assembly/dsp/gen/dclip.ts create mode 100644 as/assembly/dsp/gen/dclipexp.ts create mode 100644 as/assembly/dsp/gen/dcliplin.ts create mode 100644 as/assembly/dsp/gen/delay.ts create mode 100644 as/assembly/dsp/gen/diode.ts create mode 100644 as/assembly/dsp/gen/exp.ts create mode 100644 as/assembly/dsp/gen/freesound.ts create mode 100644 as/assembly/dsp/gen/gen.ts create mode 100644 as/assembly/dsp/gen/gendy.ts create mode 100644 as/assembly/dsp/gen/grain.ts create mode 100644 as/assembly/dsp/gen/inc.ts create mode 100644 as/assembly/dsp/gen/lp.ts create mode 100644 as/assembly/dsp/gen/mhp.ts create mode 100644 as/assembly/dsp/gen/mlp.ts create mode 100644 as/assembly/dsp/gen/moog.ts create mode 100644 as/assembly/dsp/gen/noi.ts create mode 100644 as/assembly/dsp/gen/nrate.ts create mode 100644 as/assembly/dsp/gen/osc.ts create mode 100644 as/assembly/dsp/gen/ramp.ts create mode 100644 as/assembly/dsp/gen/rate.ts create mode 100644 as/assembly/dsp/gen/sap.ts create mode 100644 as/assembly/dsp/gen/saw.ts create mode 100644 as/assembly/dsp/gen/say.ts create mode 100644 as/assembly/dsp/gen/sbp.ts create mode 100644 as/assembly/dsp/gen/shp.ts create mode 100644 as/assembly/dsp/gen/sin.ts create mode 100644 as/assembly/dsp/gen/slp.ts create mode 100644 as/assembly/dsp/gen/smp.ts create mode 100644 as/assembly/dsp/gen/sno.ts create mode 100644 as/assembly/dsp/gen/spk.ts create mode 100644 as/assembly/dsp/gen/sqr.ts create mode 100644 as/assembly/dsp/gen/svf.ts create mode 100644 as/assembly/dsp/gen/tanh.ts create mode 100644 as/assembly/dsp/gen/tanha.ts create mode 100644 as/assembly/dsp/gen/tap.ts create mode 100644 as/assembly/dsp/gen/tri.ts create mode 100644 as/assembly/dsp/gen/zero.ts create mode 100644 as/assembly/dsp/graph/copy.ts create mode 100644 as/assembly/dsp/graph/dc-bias-old.ts create mode 100644 as/assembly/dsp/graph/dc-bias.ts create mode 100644 as/assembly/dsp/graph/fade.ts create mode 100644 as/assembly/dsp/graph/fill.ts create mode 100644 as/assembly/dsp/graph/join.ts create mode 100644 as/assembly/dsp/graph/math.ts create mode 100644 as/assembly/dsp/graph/rms.ts create mode 100644 as/assembly/dsp/index.ts create mode 100644 as/assembly/dsp/vm/bin-op.ts create mode 100644 as/assembly/dsp/vm/dsp-shared.ts create mode 100644 as/assembly/dsp/vm/dsp.ts create mode 100644 as/assembly/dsp/vm/sound.ts create mode 100644 as/assembly/dsp/vm/template.ts delete mode 100644 as/assembly/env.ts create mode 100644 as/assembly/util.ts create mode 100644 asconfig-dsp.json create mode 100644 generated/assembly/dsp-factory.ts create mode 100644 generated/assembly/dsp-offsets.ts create mode 100644 generated/assembly/dsp-op.ts create mode 100644 generated/assembly/dsp-runner.ts create mode 100644 generated/assembly/tsconfig.json create mode 100644 generated/typescript/dsp-gens.ts create mode 100644 generated/typescript/dsp-vm.ts create mode 100644 scripts/generate-dsp-vm.ts create mode 100644 scripts/update-dsp-factory.ts create mode 100644 scripts/update-gens-offsets.ts create mode 100644 scripts/util.ts create mode 100644 src/as/dsp/constants.ts create mode 100644 src/as/dsp/dsp-service.ts create mode 100644 src/as/dsp/dsp-shared.ts create mode 100644 src/as/dsp/dsp-worker.ts create mode 100644 src/as/dsp/dsp.ts create mode 100644 src/as/dsp/index.ts create mode 100644 src/as/dsp/notes-shared.ts create mode 100644 src/as/dsp/params-shared.ts create mode 100644 src/as/dsp/util.ts create mode 100644 src/as/dsp/value.ts create mode 100644 src/as/dsp/wasm.ts create mode 100644 src/comp/DspEditor.tsx create mode 100644 src/lang/interpreter.ts create mode 100644 src/lang/util.ts create mode 100644 src/pages/DspDemo.tsx create mode 100644 vendor/as-transform-update-dsp-gens.js create mode 100644 vendor/util.js diff --git a/as/assembly/dsp/constants.ts b/as/assembly/dsp/constants.ts new file mode 100644 index 0000000..2ea495b --- /dev/null +++ b/as/assembly/dsp/constants.ts @@ -0,0 +1,8 @@ +export const MAX_SCALARS = 4096 +export const MAX_LITERALS = 4096 +export const MAX_FLOATS = 4096 +export const MAX_LISTS = 4096 +export const MAX_TRACKS = 16 +export const MAX_RMSS = 1024 +export const MAX_SOUNDS = 16 +export const BUFFER_SIZE = 8192 diff --git a/as/assembly/dsp/core/antialias-wavetable.ts b/as/assembly/dsp/core/antialias-wavetable.ts new file mode 100644 index 0000000..41de7ba --- /dev/null +++ b/as/assembly/dsp/core/antialias-wavetable.ts @@ -0,0 +1,155 @@ +import { nextPowerOfTwo } from '../../util' +import { ANTIALIAS_WAVETABLE_OVERSAMPLING, WAVETABLE_SIZE } from './constants' +import { fft } from './fft' + +class Real { + @inline static saw(real: StaticArray, i: u32, j: u32): void { + const temp: f32 = -1.0 / f32(i) + real[i] = temp + real[j] = -temp + } + + @inline static ramp(real: StaticArray, i: u32, j: u32): void { + const temp: f32 = -1.0 / f32(i) + real[i] = -temp + real[j] = temp + } + + @inline static sqr(real: StaticArray, i: u32, j: u32): void { + const temp: f32 = i & 0x01 ? 1.0 / f32(i) : 0.0 + real[i] = -temp + real[j] = temp + } + + static sign: f32 = 1.0 + @inline static tri(real: StaticArray, i: u32, j: u32): void { + const temp: f32 = i & 0x01 ? 1.0 / f32(i * i) * (this.sign = -this.sign) : 0.0 + real[i] = temp + real[j] = -temp + } +} + +export class AntialiasWavetable { + real: StaticArray + imag: StaticArray + freqs: StaticArray + topFreq: f64 + maxHarms: u32 + numOfTables: u32 + tableLength: u32 + tableMask: u32 + tableIndex: u32 = 0 + stepShift: i32 = 0 + sampleRate: u32 + + saw: StaticArray> + ramp: StaticArray> + sqr: StaticArray> + tri: StaticArray> + + constructor(sampleRate: u32) { + let topFreq: f64 = 10 + let maxHarms: u32 = u32(f64(sampleRate) / (3.0 * topFreq) + 0.5) + const tableLength: u32 = nextPowerOfTwo(maxHarms) * 2 * ANTIALIAS_WAVETABLE_OVERSAMPLING + const tableMask: u32 = (tableLength - 1) << 2 + const numOfTables: u32 = u32(Math.log2(f64(maxHarms)) + 1) + + // logi(tableLength) + const saw = new StaticArray>(numOfTables) + const ramp = new StaticArray>(numOfTables) + const sqr = new StaticArray>(numOfTables) + const tri = new StaticArray>(numOfTables) + for (let i: u32 = 0; i < numOfTables; i++) { + saw[i] = new StaticArray(tableLength) + ramp[i] = new StaticArray(tableLength) + sqr[i] = new StaticArray(tableLength) + tri[i] = new StaticArray(tableLength) + } + + const freqs = new StaticArray(numOfTables) + const real = new StaticArray(tableLength) + const imag = new StaticArray(tableLength) + + this.real = real + this.imag = imag + this.freqs = freqs + + this.saw = saw + this.ramp = ramp + this.sqr = sqr + this.tri = tri + + this.sampleRate = sampleRate + this.topFreq = topFreq + this.maxHarms = maxHarms + this.numOfTables = numOfTables + this.tableLength = tableLength + this.tableMask = tableMask + this.stepShift = i32(Math.log2(f64(WAVETABLE_SIZE))) - i32(Math.log2(f64(this.tableLength))) + + this.makeTables(this.saw, Real.saw) + this.makeTables(this.ramp, Real.ramp) + this.makeTables(this.sqr, Real.sqr) + this.makeTables(this.tri, Real.tri) + } + + getTableIndex(hz: f32): u32 { + let tableIndex: u32 = 0 + while ( + hz >= this.freqs[tableIndex] + && tableIndex < this.numOfTables - 1 + ) { + tableIndex = tableIndex + 1 + } + return tableIndex + } + + makeTables(target: StaticArray>, fn: (real: StaticArray, i: u32, j: u32) => void): void { + let topFreq: f64 = this.topFreq + let i: u32 = 0 + for (let harms: u32 = this.maxHarms; harms >= 1; harms >>= 1) { + this.defineWaveform(harms, fn) + this.makeWavetable(target[i]) + this.freqs[i] = f32(topFreq) + topFreq = topFreq * 2 + i = i + 1 + } + } + + defineWaveform(harms: u32, fn: (real: StaticArray, i: u32, j: u32) => void): void { + if (harms > (this.tableLength >> 1)) { + harms = (this.tableLength >> 1) + } + + this.imag.fill(0) + this.real.fill(0) + + Real.sign = 1.0 + for (let i: u32 = 1, j: u32 = this.tableLength - 1; i <= harms; i++, j--) { + fn(this.real, i, j) + } + } + + writeSaw(i: u32, j: u32): void { + const temp: f32 = -1.0 / f32(i) + this.real[i] = temp + this.real[j] = -temp + } + + makeWavetable(wave: StaticArray): void { + fft(this.tableLength, this.real, this.imag) + + // calc normal + let scale: f32 + let max: f32 = 0.0 + for (let i: u32 = 0; i < this.tableLength; i++) { + let temp: f32 = Mathf.abs(this.imag[i]) + if (max < temp) max = temp + } + scale = 1.0 / max * 0.999 + + for (let idx: u32 = 0; idx < this.tableLength; idx++) { + wave[idx] = this.imag[idx] * scale + } + } +} diff --git a/as/assembly/dsp/core/clock.ts b/as/assembly/dsp/core/clock.ts new file mode 100644 index 0000000..4577625 --- /dev/null +++ b/as/assembly/dsp/core/clock.ts @@ -0,0 +1,54 @@ +@unmanaged +export class Clock { + time: f64 = 0 + timeStep: f64 = 0 + prevTime: f64 = -1 + startTime: f64 = 0 + endTime: f64 = 1 + bpm: f64 = 60 + coeff: f64 = 0 + barTime: f64 = 0 + barTimeStep: f64 = 0 + loopStart: f64 = -Infinity + loopEnd: f64 = +Infinity + sampleRate: u32 = 44100 + jumpBar: i32 = -1 + ringPos: u32 = 0 + nextRingPos: u32 = 0 + + reset(): void { + const c: Clock = this + c.ringPos = 0 + c.nextRingPos = 0 + c.prevTime = -1 + c.time = 0 + c.barTime = c.startTime + } + update(): void { + const c: Clock = this + + c.coeff = c.bpm / 60 / 4 + c.timeStep = 1.0 / c.sampleRate + c.barTimeStep = c.timeStep * c.coeff + + let bt: f64 + + // advance barTime + bt = c.barTime + ( + c.prevTime >= 0 + ? (c.time - c.prevTime) * c.coeff + : 0 + ) + c.prevTime = c.time + + // wrap barTime on clock.endTime + const startTime = Math.max(c.loopStart, c.startTime) + const endTime = Math.min(c.loopEnd, c.endTime) + + if (bt >= endTime) { + bt = startTime + (bt % 1.0) + } + + c.barTime = bt + } +} diff --git a/as/assembly/dsp/core/constants-internal.ts b/as/assembly/dsp/core/constants-internal.ts new file mode 100644 index 0000000..762e351 --- /dev/null +++ b/as/assembly/dsp/core/constants-internal.ts @@ -0,0 +1,3 @@ +// @ts-ignore +@inline +export const k2PI: f32 = 6.28318530718 diff --git a/as/assembly/dsp/core/constants.ts b/as/assembly/dsp/core/constants.ts new file mode 100644 index 0000000..6dc507b --- /dev/null +++ b/as/assembly/dsp/core/constants.ts @@ -0,0 +1,4 @@ +export const WAVETABLE_SIZE: u32 = 1 << 13 +export const DELAY_MAX_SIZE: u32 = 1 << 16 +export const SAMPLE_MAX_SIZE: u32 = 1 << 16 +export const ANTIALIAS_WAVETABLE_OVERSAMPLING: u32 = 1 diff --git a/as/assembly/dsp/core/engine.ts b/as/assembly/dsp/core/engine.ts new file mode 100644 index 0000000..1cc7c8d --- /dev/null +++ b/as/assembly/dsp/core/engine.ts @@ -0,0 +1,36 @@ +import { rateToPhaseStep } from '../../util' +import { Clock } from './clock' +import { WAVETABLE_SIZE } from './constants' +import { Wavetable } from './wavetable' + +export class Core { + wavetable: Wavetable + constructor(public sampleRate: u32) { + this.wavetable = new Wavetable(sampleRate, WAVETABLE_SIZE) + } +} + +export class Engine { + wavetable: Wavetable + clock: Clock + + rateSamples: u32 + rateSamplesRecip: f64 + rateStep: u32 + samplesPerMs: f64 + + constructor(public sampleRate: u32, public core: Core) { + const clock = new Clock() + + this.wavetable = core.wavetable + this.clock = clock + this.clock.sampleRate = sampleRate + clock.update() + clock.reset() + + this.rateSamples = sampleRate + this.rateSamplesRecip = (1.0 / f64(sampleRate)) + this.rateStep = rateToPhaseStep(sampleRate) + this.samplesPerMs = f64(sampleRate) / 1000 + } +} diff --git a/as/assembly/dsp/core/fft.ts b/as/assembly/dsp/core/fft.ts new file mode 100644 index 0000000..9c24844 --- /dev/null +++ b/as/assembly/dsp/core/fft.ts @@ -0,0 +1,141 @@ +export function fft(N: i32, ar: StaticArray, ai: StaticArray): void { + let i: i32, j: i32, k: i32, L: i32; + let M: i32, TEMP: i32, LE: i32, LE1: i32, ip: i32; + let NV2: i32, NM1: i32; + let t: f32; + let Ur: f32, Ui: f32, Wr: f32, Wi: f32, Tr: f32, Ti: f32; + let Ur_old: f32; + + NV2 = N >> 1; + NM1 = N - 1; + TEMP = N; + M = 0; + while (TEMP >>= 1) ++M; + + j = 1; + for (i = 1; i <= NM1; i++) { + if (i < j) { + t = ar[j - 1]; + ar[j - 1] = ar[i - 1]; + ar[i - 1] = t; + t = ai[j - 1]; + ai[j - 1] = ai[i - 1]; + ai[i - 1] = t; + } + + k = NV2; + while (k < j) { + j -= k; + k /= 2; + } + + j += k; + } + + LE = 1; + for (L = 1; L <= M; L++) { + LE1 = LE; + LE *= 2; + Ur = 1.0; + Ui = 0.0; + Wr = Mathf.cos(Mathf.PI / f32(LE1)); + Wi = -Mathf.sin(Mathf.PI / f32(LE1)); + for (j = 1; j <= LE1; j++) { + for (i = j; i <= N; i += LE) { + ip = i + LE1; + Tr = ar[ip - 1] * Ur - ai[ip - 1] * Ui; + Ti = ar[ip - 1] * Ui + ai[ip - 1] * Ur; + ar[ip - 1] = ar[i - 1] - Tr; + ai[ip - 1] = ai[i - 1] - Ti; + ar[i - 1] = ar[i - 1] + Tr; + ai[i - 1] = ai[i - 1] + Ti; + } + Ur_old = Ur; + Ur = Ur_old * Wr - Ui * Wi; + Ui = Ur_old * Wi + Ui * Wr; + } + } +} + +// export function computeInverseFFT(input: StaticArray): void { +// const N: i32 = input.length / 2; +// // const output: Float32Array = new Float32Array(N * 2); // Output array will contain real and imaginary parts + +// // Conjugate the input +// for (let i: i32 = 0; i < input.length; i += 2) { +// input[i + 1] = -input[i + 1]; +// } + +// // Compute forward FFT +// computeForwardFFT(input); + +// // Conjugate the output and scale +// for (let i: i32 = 0; i < input.length; i += 2) { +// input[i] = input[i] / N; +// input[i + 1] = -input[i + 1] / N; +// } +// } + +// export function computeForwardFFT(input: StaticArray): void { +// const N: i32 = input.length / 2; + +// // Bit-reversal permutation +// let j: i32 = 0; +// for (let i: i32 = 0; i < N - 1; i++) { +// if (i < j) { +// const tempR: f32 = input[i * 2]; +// const tempI: f32 = input[i * 2 + 1]; +// input[i * 2] = input[j * 2]; +// input[i * 2 + 1] = input[j * 2 + 1]; +// input[j * 2] = tempR; +// input[j * 2 + 1] = tempI; +// } +// let k: i32 = N >> 1; +// while (j >= k) { +// j -= k; +// k >>= 1; +// } +// j += k; +// } + +// // Compute FFT +// let step: i32 = 1; +// while (step < N) { +// const angleStep: f32 = Mathf.PI / step; +// let k: i32 = 0; +// while (k < N) { +// for (let l: i32 = 0; l < step; l++) { +// const angle: f32 = l * angleStep; +// const WkR: f32 = Mathf.cos(angle); +// const WkI: f32 = Mathf.sin(angle); +// const i: i32 = k + l; +// const j: i32 = i + step; + +// const inputR: f32 = input[j * 2]; +// const inputI: f32 = input[j * 2 + 1]; + +// const twiddleR: f32 = WkR * inputR - WkI * inputI; +// const twiddleI: f32 = WkR * inputI + WkI * inputR; + +// const outputR: f32 = input[i * 2]; +// const outputI: f32 = input[i * 2 + 1]; + +// input[i * 2] = outputR + twiddleR; +// input[i * 2 + 1] = outputI + twiddleI; +// input[j * 2] = outputR - twiddleR; +// input[j * 2 + 1] = outputI - twiddleI; +// } +// k += step * 2; +// } +// step <<= 1; +// } +// } + +// export function reconstructWave(input: StaticArray): void { +// const N: i32 = input.length / 2; + +// // Extract the real part and normalize +// for (let i: i32 = 0; i < N; i++) { +// input[i] = input[i * 2] / N; +// } +// } diff --git a/as/assembly/dsp/core/wave.ts b/as/assembly/dsp/core/wave.ts new file mode 100644 index 0000000..c74e814 --- /dev/null +++ b/as/assembly/dsp/core/wave.ts @@ -0,0 +1,44 @@ +import { WAVETABLE_SIZE } from './constants' +import { rnd } from '../../util' + +// @ts-ignore +@inline const HALF_PI: f64 = Math.PI / 2.0 + +export class Wave { + @inline static sine(phase: f64): f64 { + return Math.sin(phase) + } + + @inline static saw(phase: f64): f64 { + return 1.0 - (((phase + Math.PI) / Math.PI) % 2.0) + } + + @inline static ramp(phase: f64): f64 { + return (((phase + Math.PI) / Math.PI) % 2.0) - 1.0 + } + + @inline static tri(phase: f64): f64 { + return 1.0 - Math.abs(1.0 - (((phase + HALF_PI) / Math.PI) % 2.0)) * 2.0 + } + + @inline static sqr(phase: f64): f64 { + return Wave.ramp(phase) < 0 ? -1 : 1 + } + + @inline static noise(phase: f64): f64 { + return rnd() * 2.0 - 1.0 + } +} + +const numHarmonics: u32 = 16 +export class Blit { + @inline static saw(i: u32): f64 { + let value: f64 = 0.0 + for (let h: u32 = 1; h <= numHarmonics; h++) { + const harmonicPhase: f64 = f64(i * h) / f64(WAVETABLE_SIZE) + const harmonicValue: f64 = Math.sin(harmonicPhase) / f64(h); + value += harmonicValue; + } + return value + } +} diff --git a/as/assembly/dsp/core/wavetable.ts b/as/assembly/dsp/core/wavetable.ts new file mode 100644 index 0000000..4f340e1 --- /dev/null +++ b/as/assembly/dsp/core/wavetable.ts @@ -0,0 +1,147 @@ +import { phaseFrac, phaseToRadians, rateToPhaseStep } from '../../util' +import { AntialiasWavetable } from './antialias-wavetable' +import { Wave } from './wave' + +function exp(phase: f64): f64 { + return Math.exp(-phase) +} + +// @ts-ignore +@inline +export function readAtPhase(mask: u32, table: u32, phase: u32, offset: u32): f32 { + const + current = phase + offset, + pos = current >> 14, + masked = pos & mask, + index = table + masked, + a: f32 = load(index), + b: f32 = load(index, 4), + d: f32 = b - a, + frac: f32 = phaseFrac(current), + sample: f32 = a + frac * d + return sample +} + +export class Wavetable { + length: u32 + mask: u32 + phases: StaticArray + + sine: StaticArray + cos: StaticArray + exp: StaticArray + // saw: StaticArray + // ramp: StaticArray + // tri: StaticArray + // sqr: StaticArray + noise: StaticArray + + antialias: AntialiasWavetable + + constructor(public sampleRate: u32, public size: u32) { + // length is overshoot by 1 so that we can interpolate + // the values at ( index, index+1 ) without an extra & mask operation + const length = size + 1 + this.length = length + this.mask = (size - 1) << 2 + this.phases = new StaticArray(length) + + this.sine = new StaticArray(length) + this.cos = new StaticArray(length) + this.exp = new StaticArray(length) + // this.saw = new StaticArray(length) + // this.ramp = new StaticArray(length) + // this.tri = new StaticArray(length) + // this.sqr = new StaticArray(length) + this.noise = new StaticArray(length) + + this.antialias = new AntialiasWavetable(sampleRate) + + const step: u32 = rateToPhaseStep(this.size) + for (let i: u32 = 0, phase: u32 = 0; i < this.length; i++) { + this.phases[i] = phase + phase += step + } + + this.fill(this.sine, Math.sin, phaseToRadians) + this.fill(this.cos, Math.cos, phaseToRadians) + this.fill(this.exp, exp, phaseToRadians) + // this.fill(this.saw, Wave.saw, phaseToRadians) + // this.fill(this.ramp, Wave.ramp, phaseToRadians) + // this.fill(this.tri, Wave.tri, phaseToRadians) + // this.fill(this.sqr, Wave.sqr, phaseToRadians) + this.fill(this.noise, Wave.noise, phaseToRadians) + } + + create(fn: (phase: f64) => f64, phaser: (phase: u32) => f64): StaticArray { + const table = new StaticArray(this.length) + this.fill(table, fn, phaser) + return table + } + + fill(table: StaticArray, fn: (phase: f64) => f64, phaser: (phase: u32) => f64): void { + for (let i: u32 = 0, phase: u32 = 0; i < this.length; i++) { + phase = this.phases[i] + table[i] = fn(phaser(phase)) + } + } + + fillByIndex(table: StaticArray, fn: (phase: u32) => f64): void { + for (let i: u32 = 0; i < this.length; i++) { + table[i] = fn(i) + } + } + + readAt(wavetable: StaticArray, phase: u32): f32 { + const + mask: u32 = this.mask, + table: u32 = changetype(wavetable), + pos = phase >> 14, + masked = pos & mask, + index = table + masked, + a: f32 = load(index), + b: f32 = load(index, 4), + d: f32 = b - a, + frac: f32 = phaseFrac(phase), + sample: f32 = a + frac * d + + return sample + } + + read( + table: u32, + mask: u32, + phase: u32, + offset: u32, + step: u32, + begin: u32, + end: u32, + targetPtr: usize + ): u32 { + let target: u32 = targetPtr + (begin << 2) + let sv: v128 = f32x4(0, 0, 0, 0) + + // multiply length by 4 because f32=4 + end = target + ((end - begin) << 2) + + while (target < end) { + unroll(4, (): void => { + sv = f32x4.replace_lane(sv, 0, readAtPhase(mask, table, phase, offset)) + phase += step + sv = f32x4.replace_lane(sv, 1, readAtPhase(mask, table, phase, offset)) + phase += step + sv = f32x4.replace_lane(sv, 2, readAtPhase(mask, table, phase, offset)) + phase += step + sv = f32x4.replace_lane(sv, 3, readAtPhase(mask, table, phase, offset)) + store(target, sv) + + // advance pointer 4x4 because of simd x4 + f32 len 4 + target += 16 + + phase += step + }) + } + + return phase + } +} diff --git a/as/assembly/dsp/gen/adsr.ts b/as/assembly/dsp/gen/adsr.ts new file mode 100644 index 0000000..8b62f3a --- /dev/null +++ b/as/assembly/dsp/gen/adsr.ts @@ -0,0 +1,90 @@ +import { clamp } from '../../util' +import { Gen } from './gen' + +enum AdsrState { + Release, + Attack, + Decay, +} + +export class Adsr extends Gen { + attack: f32 = 1 // ms + decay: f32 = 200 + sustain: f32 = 0.1 + release: f32 = 500 + + /** Trigger */ + on: f32 = -1.0 + _lastOn: i32 = -1 + off: f32 = -1.0 + _lastOff: i32 = -1 + + _state: AdsrState = AdsrState.Release + + _step: f32 = 0 + _pos: i32 = 0 + _value: f32 = 0 + _decayAt: i32 = 0 + _bottom: f32 = 0 + + _update(): void { + // On + if (this._lastOn !== i32(this.on)) { + this._lastOn = i32(this.on) + + // (any) -> Attack + this._state = AdsrState.Attack + this._step = f32(f64(1.0 / (f64(this.attack) * this._engine.samplesPerMs))) + this._pos = 0 + this._decayAt = i32(f64(this.attack) * this._engine.samplesPerMs) + this._bottom = 0 + } + + // Off + if (this._lastOff !== i32(this.off)) { + this._lastOff = i32(this.off) + + // (any) -> Release + this._state = AdsrState.Release + this._step = -f32(f64(this._value / (f64(this.release) * this._engine.samplesPerMs))) + this._pos = 0 + this._bottom = 0 + } + + if (this._state === AdsrState.Attack) { + // Attack -> Decay + if (this._pos >= this._decayAt) { + this._state = AdsrState.Decay + this._step = -f32(f64((1.0 - this.sustain) / (f64(this.decay) * this._engine.samplesPerMs))) + this._pos = 0 + this._bottom = this.sustain + } + } + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + out += offset + + const step: f32 = this._step + const bottom: f32 = this._bottom + + let value: f32 = this._value + + for (; i < end; i += 16) { + unroll(16, () => { + value = clamp(bottom, 1, 0, (value + step)) + f32.store(out, value) + out += 4 + }) + } + + this._value = value + this._pos = this._pos + i32(length) + } +} diff --git a/as/assembly/dsp/gen/aosc.ts b/as/assembly/dsp/gen/aosc.ts new file mode 100644 index 0000000..81520c7 --- /dev/null +++ b/as/assembly/dsp/gen/aosc.ts @@ -0,0 +1,26 @@ +import { Osc } from './osc' + +export class Aosc extends Osc { + get _tables(): StaticArray> { + return this._engine.wavetable.antialias.saw + } + + get _table(): StaticArray { + return this._tables[this._tableIndex] + } + + get _mask(): u32 { + return this._engine.wavetable.antialias.tableMask + } + + _tableIndex: u32 = 0 + + _update(): void { + super._update() + this._tableIndex = this._engine.wavetable.antialias.getTableIndex(this.hz) + const stepShift: i32 = this._engine.wavetable.antialias.stepShift + this._step = stepShift < 0 + ? this._step << (-stepShift) + : this._step >> stepShift + } +} diff --git a/as/assembly/dsp/gen/atan.ts b/as/assembly/dsp/gen/atan.ts new file mode 100644 index 0000000..781ca7e --- /dev/null +++ b/as/assembly/dsp/gen/atan.ts @@ -0,0 +1,34 @@ +import { Gen } from './gen' + +export class Atan extends Gen { + gain: f32 = 1.0; + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + const gain: f32 = this.gain + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + sample = Mathf.atan(sample * gain) + + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/bap.ts b/as/assembly/dsp/gen/bap.ts new file mode 100644 index 0000000..27d4f51 --- /dev/null +++ b/as/assembly/dsp/gen/bap.ts @@ -0,0 +1,10 @@ +import { Biquad } from './biquad' + +export class Bap extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._allpass(this.cut, this.q) + } +} diff --git a/as/assembly/dsp/gen/bbp.ts b/as/assembly/dsp/gen/bbp.ts new file mode 100644 index 0000000..c238ca5 --- /dev/null +++ b/as/assembly/dsp/gen/bbp.ts @@ -0,0 +1,10 @@ +import { Biquad } from './biquad' + +export class Bbp extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._bandpass(this.cut, this.q) + } +} diff --git a/as/assembly/dsp/gen/bhp.ts b/as/assembly/dsp/gen/bhp.ts new file mode 100644 index 0000000..13a2b90 --- /dev/null +++ b/as/assembly/dsp/gen/bhp.ts @@ -0,0 +1,10 @@ +import { Biquad } from './biquad' + +export class Bhp extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._highpass(this.cut, this.q) + } +} diff --git a/as/assembly/dsp/gen/bhs.ts b/as/assembly/dsp/gen/bhs.ts new file mode 100644 index 0000000..344ebe6 --- /dev/null +++ b/as/assembly/dsp/gen/bhs.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Bhs extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + amt: f32 = 1 + + _update(): void { + this._highshelf(this.cut, this.q, this.amt) + } +} diff --git a/as/assembly/dsp/gen/biquad.ts b/as/assembly/dsp/gen/biquad.ts new file mode 100644 index 0000000..5f09302 --- /dev/null +++ b/as/assembly/dsp/gen/biquad.ts @@ -0,0 +1,270 @@ +import { paramClamp } from '../../util' +import { Gen } from './gen' + +// @ts-ignore +@inline const PI2 = Math.PI * 2.0 + +export class Biquad extends Gen { + in: u32 = 0 + + _x1: f64 = 0 + _x2: f64 = 0 + _y1: f64 = 0 + _y2: f64 = 0 + + _a0: f64 = 1 + _a1: f64 = 0 + _a2: f64 = 0 + _b0: f64 = 0 + _b1: f64 = 0 + _b2: f64 = 0 + + _params_freq: f32[] = [50, 22040, 4000] + _params_Q: f32[] = [0.01, 40, 1.0] + _params_gain: f32[] = [-10, 10, 0] + + @inline _clear(): void { + this._x1 = 0 + this._x2 = 0 + this._y1 = 0 + this._y2 = 0 + + this._a0 = 1 + this._a1 = 0 + this._a2 = 0 + this._b0 = 0 + this._b1 = 0 + this._b2 = 0 + } + + @inline _db(gain: f64): f64 { + return Math.pow(10.0, gain / 20.0) + } + + @inline _omega(freq: f64): f64 { + return (PI2 * freq) / f64(this._engine.sampleRate) + } + + @inline _alpha(sin0: f64, Q: f64): f64 { + return sin0 / (2.0 * Q) + } + + @inline _shelf(sin0: f64, A: f64, Q: f64): f64 { + return ( + 2.0 * + Math.sqrt(A) * + ((sin0 / 2) * Math.sqrt((A + 1 / A) * (1 / Q - 1) + 2)) + ) + } + + @inline _validate(freq: f32, Q: f32): boolean { + if (freq <= 0) return false + if (freq !== freq) return false + if (Q <= 0) return false + if (Q !== Q) return false + return true + } + + @inline _lowpass(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + const alpha: f64 = this._alpha(sin0, f64(Q)) + + this._b0 = (1.0 - cos0) / 2.0 + this._b1 = 1.0 - cos0 + this._b2 = (1.0 - cos0) / 2.0 + this._a0 = 1.0 + alpha + this._a1 = -2.0 * cos0 + this._a2 = 1.0 - alpha + this._integrate() + } + + @inline _highpass(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + const alpha: f64 = this._alpha(sin0, f64(Q)) + + this._b0 = (1.0 + cos0) / 2.0 + this._b1 = -(1.0 + cos0) + this._b2 = (1.0 + cos0) / 2.0 + this._a0 = 1.0 + alpha + this._a1 = -2.0 * cos0 + this._a2 = 1.0 - alpha + this._integrate() + } + + @inline _bandpass(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + const alpha: f64 = this._alpha(sin0, f64(Q)) + + this._b0 = alpha + this._b1 = 0.0 + this._b2 = -alpha + this._a0 = 1.0 + alpha + this._a1 = -2.0 * cos0 + this._a2 = 1.0 - alpha + this._integrate() + } + + @inline _notch(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + const alpha: f64 = this._alpha(sin0, f64(Q)) + + this._b0 = 1.0 + this._b1 = -2.0 * cos0 + this._b2 = 1.0 + this._a0 = 1.0 + alpha + this._a1 = -2.0 * cos0 + this._a2 = 1.0 - alpha + this._integrate() + } + + @inline _allpass(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + const alpha: f64 = this._alpha(sin0, f64(Q)) + + this._b0 = 1.0 - alpha + this._b1 = -2.0 * cos0 + this._b2 = 1.0 + alpha + this._a0 = 1.0 + alpha + this._a1 = -2.0 * cos0 + this._a2 = 1.0 - alpha + this._integrate() + } + + @inline _peak(freq: f32, Q: f32, gain: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + gain = paramClamp(this._params_gain, gain) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + const alpha: f64 = this._alpha(sin0, f64(Q)) + + const A: f64 = this._db(f64(gain)) + + this._b0 = 1.0 + alpha * A + this._b1 = -2.0 * cos0 + this._b2 = 1.0 - alpha * A + this._a0 = 1.0 + alpha / A + this._a1 = -2.0 * cos0 + this._a2 = 1.0 - alpha / A + this._integrate() + } + + @inline _lowshelf(freq: f32, Q: f32, gain: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + gain = paramClamp(this._params_gain, gain) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + + const A: f64 = this._db(f64(gain)) + const S: f64 = this._shelf(sin0, A, f64(Q)) + + this._b0 = A * (A + 1.0 - (A - 1.0) * cos0 + S) + this._b1 = 2.0 * A * (A - 1.0 - (A + 1.0) * cos0) + this._b2 = A * (A + 1.0 - (A - 1.0) * cos0 - S) + this._a0 = A + 1.0 + (A - 1.0) * cos0 + S + this._a1 = -2.0 * (A - 1.0 + (A + 1.0) * cos0) + this._a2 = A + 1.0 + (A - 1.0) * cos0 - S + this._integrate() + } + + @inline _highshelf(freq: f32, Q: f32, gain: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + gain = paramClamp(this._params_gain, gain) + const omega: f64 = this._omega(f64(freq)) + const sin0: f64 = Math.sin(omega) + const cos0: f64 = Math.cos(omega) + + const A: f64 = this._db(f64(gain)) + const S: f64 = this._shelf(sin0, A, f64(Q)) + + this._b0 = A * (A + 1.0 + (A - 1.0) * cos0 + S) + this._b1 = -2.0 * A * (A - 1.0 + (A + 1.0) * cos0) + this._b2 = A * (A + 1.0 + (A - 1.0) * cos0 - S) + this._a0 = A + 1.0 - (A - 1.0) * cos0 + S + this._a1 = 2.0 * (A - 1.0 - (A + 1.0) * cos0) + this._a2 = A + 1.0 - (A - 1.0) * cos0 - S + this._integrate() + } + + @inline _integrate(): void { + const g: f64 = 1.0 / this._a0 + + this._b0 *= g + this._b1 *= g + this._b2 *= g + this._a1 *= g + this._a2 *= g + } + + @inline _process(x0: f32): f32 { + const y0: f64 = + this._b0 * f64(x0) + + this._b1 * this._x1 + + this._b2 * this._x2 - + this._a1 * this._y1 - + this._a2 * this._y2 + + this._x2 = this._x1 + this._y2 = this._y1 + this._x1 = f64(x0) + this._y1 = y0 + + return f32(y0) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + sample = this._process(sample) + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/blp.ts b/as/assembly/dsp/gen/blp.ts new file mode 100644 index 0000000..fd83eea --- /dev/null +++ b/as/assembly/dsp/gen/blp.ts @@ -0,0 +1,10 @@ +import { Biquad } from './biquad' + +export class Blp extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._lowpass(this.cut, this.q) + } +} diff --git a/as/assembly/dsp/gen/bls.ts b/as/assembly/dsp/gen/bls.ts new file mode 100644 index 0000000..a5ad099 --- /dev/null +++ b/as/assembly/dsp/gen/bls.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Bls extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + amt: f32 = 1 + + _update(): void { + this._lowshelf(this.cut, this.q, this.amt) + } +} diff --git a/as/assembly/dsp/gen/bno.ts b/as/assembly/dsp/gen/bno.ts new file mode 100644 index 0000000..f5df701 --- /dev/null +++ b/as/assembly/dsp/gen/bno.ts @@ -0,0 +1,10 @@ +import { Biquad } from './biquad' + +export class Bno extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._notch(this.cut, this.q) + } +} diff --git a/as/assembly/dsp/gen/bpk.ts b/as/assembly/dsp/gen/bpk.ts new file mode 100644 index 0000000..5b44060 --- /dev/null +++ b/as/assembly/dsp/gen/bpk.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Bpk extends Biquad { + cut: f32 = 500 + q: f32 = 0.5 + amt: f32 = 1 + + _update(): void { + this._peak(this.cut, this.q, this.amt) + } +} diff --git a/as/assembly/dsp/gen/clamp.ts b/as/assembly/dsp/gen/clamp.ts new file mode 100644 index 0000000..3ed554f --- /dev/null +++ b/as/assembly/dsp/gen/clamp.ts @@ -0,0 +1,40 @@ +import { Gen } from './gen' + +export class Clamp extends Gen { + min: f32 = -0.5; + max: f32 = 0.5; + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + const min: f32 = this.min + const max: f32 = this.max + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + if (sample > max) { + sample = max + } + else if (sample < min) { + sample = min + } + + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/clip.ts b/as/assembly/dsp/gen/clip.ts new file mode 100644 index 0000000..d84cd4c --- /dev/null +++ b/as/assembly/dsp/gen/clip.ts @@ -0,0 +1,38 @@ +import { Gen } from './gen' + +export class Clip extends Gen { + threshold: f32 = 1.0; + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + const threshold: f32 = this.threshold + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + if (sample > threshold) { + sample = threshold + } + else if (sample < -threshold) { + sample = -threshold + } + + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/comp.ts b/as/assembly/dsp/gen/comp.ts new file mode 100644 index 0000000..e2696e6 --- /dev/null +++ b/as/assembly/dsp/gen/comp.ts @@ -0,0 +1,103 @@ +import { Gen } from './gen' + +export class Comp extends Gen { + threshold: f32 = 0.7 + ratio: f32 = 1 / 3 + attack: f32 = 0.01 + release: f32 = 0.01; + + in: u32 = 0; + sidechain: u32 = 0; + + _prevLevel: f32 = 0; + _gainReduction: f32 = 1; + + // attackRecip: f32 = 0 + // releaseRecip: f32 = 0 + + _update(): void { + // this.releaseRecip = f32(1.0 / (Mathf.max(0.1, this.release) * 0.001 * this.engine.ratesSamples[Rate.Audio])) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let sideSample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + let diff: f32 + let targetReduction: f32 + let gainReduction: f32 = this._gainReduction + + const threshold: f32 = this.threshold + + const ratio: f32 = this.ratio + const attack: f32 = this.attack + const release: f32 = this.release + + let side: u32 = this.sidechain + + if (side) { + side += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + sideSample = f32.load(side) + + diff = Mathf.max(0.0, Mathf.abs(sideSample) - threshold) * ratio + + targetReduction = diff + if (targetReduction > gainReduction) { + gainReduction = gainReduction + (targetReduction - gainReduction) * attack + } + else { + gainReduction = gainReduction + (targetReduction - gainReduction) * release + } + + sample = sample * (1.0 - gainReduction) + + f32.store(out, sample) + + inp += 4 + out += 4 + side += 4 + }) + } + } + else { + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + diff = Mathf.max(0.0, Mathf.abs(sample) - threshold) * ratio + + targetReduction = diff + if (targetReduction > gainReduction) { + gainReduction = gainReduction + (targetReduction - gainReduction) * attack + } + else { + gainReduction = gainReduction + (targetReduction - gainReduction) * release + } + + sample = sample * (1.0 - gainReduction) + + f32.store(out, sample) + + inp += 4 + out += 4 + }) + } + } + + this._gainReduction = gainReduction + } +} diff --git a/as/assembly/dsp/gen/daverb.ts b/as/assembly/dsp/gen/daverb.ts new file mode 100644 index 0000000..1be79fe --- /dev/null +++ b/as/assembly/dsp/gen/daverb.ts @@ -0,0 +1,364 @@ +import { cubic } from '../../util' +import { DELAY_MAX_SIZE } from '../core/constants' +import { Gen } from './gen' + +// nearestPowerOfTwo.ts + +// Function to find the nearest power of two +export function nearestPowerOfTwo(n: u32): u32 { + // If n is already a power of two, return it + if ((n & (n - 1)) === 0) { + return n + } + + let power: u32 = 0 + let result: u32 = 1 + + while (result < n) { + result <<= 1 // Multiply result by 2 (left shift) + power++ + } + + // Check the nearest powers of two on both sides + const lowerPower = result - n + const upperPower = (result << 1) - n + + // Return the nearest power of two + if (lowerPower < upperPower) { + return result + } else { + return result << 1 // Multiply result by 2 (left shift) + } +} + +const ld0: u32 = nearestPowerOfTwo((48000.0 * 0.004771345)) +const ld1: u32 = nearestPowerOfTwo((48000.0 * 0.003595309)) +const ld2: u32 = nearestPowerOfTwo((48000.0 * 0.012734787)) +const ld3: u32 = nearestPowerOfTwo((48000.0 * 0.009307483)) +const ld4: u32 = nearestPowerOfTwo((48000.0 * 0.022579886)) +const ld5: u32 = nearestPowerOfTwo((48000.0 * 0.149625349)) +const ld6: u32 = nearestPowerOfTwo((48000.0 * 0.060481839)) +const ld7: u32 = nearestPowerOfTwo((48000.0 * 0.1249958)) +const ld8: u32 = nearestPowerOfTwo((48000.0 * 0.030509727)) +const ld9: u32 = nearestPowerOfTwo((48000.0 * 0.141695508)) +const ld10: u32 = nearestPowerOfTwo((48000.0 * 0.089244313)) +const ld11: u32 = nearestPowerOfTwo((48000.0 * 0.106280031)) + +const md0: u32 = ld0 - 1 +const md1: u32 = ld1 - 1 +const md2: u32 = ld2 - 1 +const md3: u32 = ld3 - 1 +const md4: u32 = ld4 - 1 +const md5: u32 = ld5 - 1 +const md6: u32 = ld6 - 1 +const md7: u32 = ld7 - 1 +const md8: u32 = ld8 - 1 +const md9: u32 = ld9 - 1 +const md10: u32 = ld10 - 1 +const md11: u32 = ld11 - 1 + +const lo0: u32 = u32(0.008937872 * 48000) +const lo1: u32 = u32(0.099929438 * 48000) +const lo2: u32 = u32(0.064278754 * 48000) +const lo3: u32 = u32(0.067067639 * 48000) +const lo4: u32 = u32(0.066866033 * 48000) +const lo5: u32 = u32(0.006283391 * 48000) +const lo6: u32 = u32(0.035818689 * 48000) + +const ro0: u32 = u32(0.011861161 * 48000) +const ro1: u32 = u32(0.121870905 * 48000) +const ro2: u32 = u32(0.041262054 * 48000) +const ro3: u32 = u32(0.08981553 * 48000) +const ro4: u32 = u32(0.070931756 * 48000) +const ro5: u32 = u32(0.011256342 * 48000) +const ro6: u32 = u32(0.004065724 * 48000) + +export class Daverb extends Gen { + in: u32 = 0 + + pd: f32 = 0.03 + bw: f32 = 0.1 + fi: f32 = 0.5 + si: f32 = 0.5 + dc: f32 = 0.5 + ft: f32 = 0.5 + st: f32 = 0.5 + dp: f32 = 0.5 + ex: f32 = 0.5 + ed: f32 = 0.5 + + _params_pd: f32[] = [0, 1, 0.03] + _params_bw: f32[] = [0, 1, 0.1] + _params_fi: f32[] = [0, 1, 0.5] + _params_si: f32[] = [0, 1, 0.5] + _params_dc: f32[] = [0, 1, 0.5] + _params_ft: f32[] = [0, 0.999999, 0.5] + _params_st: f32[] = [0, 0.999999, 0.5] + _params_dp: f32[] = [0, 1, 0.5] + _params_ex: f32[] = [0, 2, 0.5] + _params_ed: f32[] = [0, 2, 0.5] + + _dpn: f32 = 0 + _exn: f32 = 0 + _edn: f32 = 0 + _pdn: f32 = 0 + + _predelay: StaticArray = new StaticArray(DELAY_MAX_SIZE) + _d0: StaticArray = new StaticArray(ld0) + _d1: StaticArray = new StaticArray(ld1) + _d2: StaticArray = new StaticArray(ld2) + _d3: StaticArray = new StaticArray(ld3) + _d4: StaticArray = new StaticArray(ld4) + _d5: StaticArray = new StaticArray(ld5) + _d6: StaticArray = new StaticArray(ld6) + _d7: StaticArray = new StaticArray(ld7) + _d8: StaticArray = new StaticArray(ld8) + _d9: StaticArray = new StaticArray(ld9) + _d10: StaticArray = new StaticArray(ld10) + _d11: StaticArray = new StaticArray(ld11) + + _index: u32 = 0 + _mask: u32 = DELAY_MAX_SIZE - 1 + + _lp1: f32 = 0 + _lp2: f32 = 0 + _lp3: f32 = 0 + _exc_phase: f32 = 0 + + _update(): void { + const arf: f32 = f32(this._engine.sampleRate) + this._dpn = 1.0 - this.dp + this._exn = this.ex / arf + this._edn = this.ed * arf / 1000.0 + this._pdn = this.pd * arf + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + const mask: u32 = this._mask + let p: i32 = this._index + let pm: i32 = p - 1 + + let lp1: f32 = this._lp1 + let lp2: f32 = this._lp2 + let lp3: f32 = this._lp3 + + let split: f32 = 0 + + let exc_phase: f32 = this._exc_phase + let exn: f32 = this._exn + let edn: f32 = this._edn + let exc: f32 = 0 + let exc2: f32 = 0 + + let lo: f32 = 0 + let ro: f32 = 0 + + let d4p: f32 = 0 + let d8p: f32 = 0 + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + // predelay + this._predelay[p & mask] = sample * 0.5 + + lp1 += this.bw * (cubic(this._predelay, p - this._pdn, mask) - lp1) + + // pre-tank + this._d0[p & md0] = lp1 - this.fi * this._d0[pm & md0] + this._d1[p & md1] = this.fi * (this._d0[p & md0] - this._d1[pm & md1]) + this._d0[pm & md0] + this._d2[p & md2] = this.fi * this._d1[p & md1] + this._d1[pm & md1] - this.si * this._d2[pm & md2] + this._d3[p & md3] = this.si * (this._d2[p & md2] - this._d3[pm & md3]) + this._d2[pm & md2] + + split = this.si * this._d3[p & md3] + this._d3[pm & md3] + + // excursions + exc = edn * (1 + Mathf.cos(exc_phase * 6.2800)) + exc2 = edn * (1 + Mathf.sin(exc_phase * 6.2847)) + + // left loop + d4p = cubic(this._d4, p - exc, md4) + this._d4[p & md4] = split + this.dc * this._d11[pm & md11] + this.ft * d4p // tank diffuse 1 + this._d5[p & md5] = d4p - this.ft * this._d4[p & md4] // long delay 1 + + lp2 += this._dpn * (this._d5[pm & md5] - lp2) // damp 1 + + this._d6[p & md6] = this.dc * lp2 - this.st * this._d6[pm & md6] // tank diffuse 2 + this._d7[p & md7] = this._d6[pm & md6] + this.st * this._d6[p & md6] // long delay 2 + + // right loop + d8p = cubic(this._d8, p - exc2, md8) + this._d8[p & md8] = split + this.dc * this._d7[pm & md7] + this.ft * d8p // tank diffuse 3 + this._d9[p & md9] = d8p - this.ft * this._d8[p & md8] // long delay 3 + + lp3 += this._dpn * this._d9[pm & md9] - lp3 // damp 2 + + this._d10[p & md10] = this.dc * lp3 - this.st * this._d10[pm & md10] + this._d11[p & md11] = this._d10[pm & md10] + this.st * this._d10[p & md10] + + exc_phase += exn + + lo = this._d9[(p - lo0) & md9] + + this._d9[(p - lo1) & md9] + - this._d10[(p - lo2) & md10] + + this._d11[(p - lo3) & md11] + - this._d5[(p - lo4) & md5] + - this._d6[(p - lo5) & md6] + - this._d7[(p - lo6) & md7] + + + ro = this._d5[(p - ro0) & md5] + + this._d5[(p - ro1) & md5] + - this._d6[(p - ro2) & md6] + + this._d7[(p - ro3) & md7] + - this._d9[(p - ro4) & md9] + - this._d10[(p - ro5) & md10] + - this._d11[(p - ro6) & md11] + + + sample = (lo + ro) * 0.5 + + f32.store(out, sample) + + inp += 4 + out += 4 + p++ + pm++ + }) + } + + this._index = p & mask + this._exc_phase = exc_phase % Mathf.PI + this._lp1 = lp1 + this._lp2 = lp2 + this._lp3 = lp3 + } + + _audio_stereo(begin: u32, end: u32, out_0: usize, out_1: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out_0 += offset + out_1 += offset + + const mask: u32 = this._mask + let p: i32 = this._index + let pm: i32 = p - 1 + + let lp1: f32 = this._lp1 + let lp2: f32 = this._lp2 + let lp3: f32 = this._lp3 + + let split: f32 = 0 + + let exc_phase: f32 = this._exc_phase + let exn: f32 = this._exn + let edn: f32 = this._edn + let exc: f32 = 0 + let exc2: f32 = 0 + + let lo: f32 = 0 + let ro: f32 = 0 + + let d4p: f32 = 0 + let d8p: f32 = 0 + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + // predelay + this._predelay[p & mask] = sample * 0.5 + + lp1 += this.bw * (cubic(this._predelay, p - this._pdn, mask) - lp1) + + // pre-tank + this._d0[p & md0] = lp1 - this.fi * this._d0[pm & md0] + this._d1[p & md1] = this.fi * (this._d0[p & md0] - this._d1[pm & md1]) + this._d0[pm & md0] + this._d2[p & md2] = this.fi * this._d1[p & md1] + this._d1[pm & md1] - this.si * this._d2[pm & md2] + this._d3[p & md3] = this.si * (this._d2[p & md2] - this._d3[pm & md3]) + this._d2[pm & md2] + + split = this.si * this._d3[p & md3] + this._d3[pm & md3] + + // excursions + exc = edn * (1 + Mathf.cos(exc_phase * 6.2800)) + exc2 = edn * (1 + Mathf.sin(exc_phase * 6.2847)) + + // left loop + d4p = cubic(this._d4, p - exc, md4) + this._d4[p & md4] = split + this.dc * this._d11[pm & md11] + this.ft * d4p // tank diffuse 1 + this._d5[p & md5] = d4p - this.ft * this._d4[p & md4] // long delay 1 + + lp2 += this._dpn * (this._d5[pm & md5] - lp2) // damp 1 + + this._d6[p & md6] = this.dc * lp2 - this.st * this._d6[pm & md6] // tank diffuse 2 + this._d7[p & md7] = this._d6[pm & md6] + this.st * this._d6[p & md6] // long delay 2 + + // right loop + d8p = cubic(this._d8, p - exc2, md8) + this._d8[p & md8] = split + this.dc * this._d7[pm & md7] + this.ft * d8p // tank diffuse 3 + this._d9[p & md9] = d8p - this.ft * this._d8[p & md8] // long delay 3 + + lp3 += this._dpn * this._d9[pm & md9] - lp3 // damp 2 + + this._d10[p & md10] = this.dc * lp3 - this.st * this._d10[pm & md10] + this._d11[p & md11] = this._d10[pm & md10] + this.st * this._d10[p & md10] + + exc_phase += exn + + lo = this._d9[(p - lo0) & md9] + + this._d9[(p - lo1) & md9] + - this._d10[(p - lo2) & md10] + + this._d11[(p - lo3) & md11] + - this._d5[(p - lo4) & md5] + - this._d6[(p - lo5) & md6] + - this._d7[(p - lo6) & md7] + + + ro = this._d5[(p - ro0) & md5] + + this._d5[(p - ro1) & md5] + - this._d6[(p - ro2) & md6] + + this._d7[(p - ro3) & md7] + - this._d9[(p - ro4) & md9] + - this._d10[(p - ro5) & md10] + - this._d11[(p - ro6) & md11] + + + f32.store(out_0, lo) + f32.store(out_1, ro) + + inp += 4 + out_0 += 4 + out_1 += 4 + p++ + pm++ + }) + } + + this._index = p & mask + this._exc_phase = exc_phase % Mathf.PI + this._lp1 = lp1 + this._lp2 = lp2 + this._lp3 = lp3 + } +} diff --git a/as/assembly/dsp/gen/dcc.ts b/as/assembly/dsp/gen/dcc.ts new file mode 100644 index 0000000..30035f8 --- /dev/null +++ b/as/assembly/dsp/gen/dcc.ts @@ -0,0 +1,46 @@ +import { Gen } from './gen' + +export class Dcc extends Gen { + ceil: f32 = 0.2; + in: u32 = 0 + + sample: f32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + const ceil: f32 = this.ceil + let prev: f32 = this.sample + let sample: f32 = 0 + let next: f32 = 0 + let diff: f32 = 0 + let abs: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + diff = sample - prev + abs = Mathf.abs(diff) + if (abs > ceil) { + next = prev + diff * (abs - ceil) + } + else { + next = sample + } + prev = next + f32.store(out, next) + inp += 4 + out += 4 + }) + } + + this.sample = prev + } +} diff --git a/as/assembly/dsp/gen/dclip.ts b/as/assembly/dsp/gen/dclip.ts new file mode 100644 index 0000000..fd335f3 --- /dev/null +++ b/as/assembly/dsp/gen/dclip.ts @@ -0,0 +1,31 @@ +import { Gen } from './gen' + +export class Dclip extends Gen { + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + sample = sample > 0 ? sample : 0 + + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/dclipexp.ts b/as/assembly/dsp/gen/dclipexp.ts new file mode 100644 index 0000000..0cede66 --- /dev/null +++ b/as/assembly/dsp/gen/dclipexp.ts @@ -0,0 +1,33 @@ +import { Gen } from './gen' + +export class Dclipexp extends Gen { + factor: f32 = 1.0; + + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + const factor: f32 = this.factor + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + sample = f32(Math.exp(f64(sample) / f64(factor)) - 1.0) + + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/dcliplin.ts b/as/assembly/dsp/gen/dcliplin.ts new file mode 100644 index 0000000..5a80fc5 --- /dev/null +++ b/as/assembly/dsp/gen/dcliplin.ts @@ -0,0 +1,39 @@ +import { Gen } from './gen' + +export class Dcliplin extends Gen { + threshold: f32 = 0.5; + factor: f32 = 0.5; + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + const threshold: f32 = this.threshold + const factor: f32 = this.factor + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + if (sample > threshold) { + sample = threshold + (sample - threshold) * factor + } else if (sample < -threshold) { + sample = -threshold + (sample + threshold) * factor + } + + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/delay.ts b/as/assembly/dsp/gen/delay.ts new file mode 100644 index 0000000..a7febee --- /dev/null +++ b/as/assembly/dsp/gen/delay.ts @@ -0,0 +1,72 @@ +import { cubic } from '../../util' +import { DELAY_MAX_SIZE } from '../core/constants' +import { Gen } from './gen' + +export class Delay extends Gen { + ms: f32 = 200; + fb: f32 = 0.5; + + in: u32 = 0; + + _floats: StaticArray = new StaticArray(DELAY_MAX_SIZE) + _mask: u32 = DELAY_MAX_SIZE - 1 + _index: u32 = 0 + _stepf: f32 = 0 + _targetf: f32 = 0 + + _update(): void { + this._targetf = Mathf.min(DELAY_MAX_SIZE - 1, (this.ms * 0.001) * this._engine.rateSamples) + if (this._stepf === 0) this._stepf = this._targetf + } + + _reset(): void { + this._floats.fill(0, 0, DELAY_MAX_SIZE) + // this._stepf = 0 + // this._index = 0 + // this._targetf = 0 + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + const mask: u32 = this._mask + let index: u32 = this._index + const fb: f32 = this.fb + let delay: f32 = 0 + let stepf: f32 = this._stepf + const targetf: f32 = this._targetf + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + // logi((index - step) & mask) + delay = cubic(this._floats, (index - stepf), mask) + // delay = this.floats[(index - step) & mask] + + this._floats[index] = sample + delay * fb + + f32.store(out, delay) + + inp += 4 + out += 4 + + index = (index + 1) & mask + stepf += (targetf - stepf) * 0.0008 + }) + } + + this._index = index + this._stepf = stepf + } +} diff --git a/as/assembly/dsp/gen/diode.ts b/as/assembly/dsp/gen/diode.ts new file mode 100644 index 0000000..8607fa8 --- /dev/null +++ b/as/assembly/dsp/gen/diode.ts @@ -0,0 +1,162 @@ +import { Gen } from './gen' + +function soft(x: f32, amount: f32 = 1): f32 { + return x / (1 / amount + Mathf.abs(x)) +} + +function clamp(min: f32, max: f32, value: f32): f32 { + if (value < min) value = min + if (value > max) value = max + return value +} + +// @ts-ignore +@inline const PI2 = Mathf.PI * 2.0 + +export class Diode extends Gen { + cut: f32 = 500; + hpf: f32 = 1000; + sat: f32 = 1.0; + q: f32 = 0.0; + + in: u32 = 0 + + _z0: f32 = 0 + _z1: f32 = 0 + _z2: f32 = 0 + _z3: f32 = 0 + _z4: f32 = 0 + + _A: f32 = 0 + _a: f32 = 0 + _a2: f32 = 0 + _b: f32 = 0 + _b2: f32 = 0 + _c: f32 = 0 + _g: f32 = 0 + _g0: f32 = 0 + _ah: f32 = 0 + _bh: f32 = 0 + _ainv: f32 = 0 + _k: f32 = 0 + + _update(): void { + // fc: normalized cutoff frequency in the range [0..1] => 0 HZ .. Nyquist + const nyq: f32 = f32(this._engine.sampleRate) / 2.0 + + // logf(this.hpf / nyq) + // logf(this.cut / nyq) + // hpf + const K: f32 = clamp(0, 1, (this.hpf / nyq)) * Mathf.PI + this._ah = (K - 2.0) / (K + 2.0) + const bh: f32 = 2.0 / (K + 2.0) + this._bh = bh + + // q: resonance in the range [0..1] + this._k = 20.0 * this.q + this._A = 1.0 + 0.5 * this._k // resonance gain compensation + + let a: f32 = Mathf.PI * clamp(0, 1, (this.cut / nyq)) // PI is Nyquist frequency + a = 2.0 * Mathf.tan(0.5 * a) // dewarping, not required with 2x oversampling + this._ainv = 1.0 / a + const a2: f32 = a * a + const b: f32 = 2.0 * a + 1.0 + const b2: f32 = b * b + const c: f32 = 1.0 / (2.0 * a2 * a2 - 4 * a2 * b2 + b2 * b2) + const g0: f32 = 2.0 * a2 * a2 * c + this._g = g0 * bh + + this._a = a + this._a2 = a2 + this._b = b + this._b2 = b2 + this._c = c + this._g0 = g0 + } + + _audio(begin: u32, end: u32, out: usize): void { + const A = this._A + const a = this._a + const a2 = this._a2 + const b = this._b + const b2 = this._b2 + const c = this._c + const g = this._g + const g0 = this._g0 + const ah = this._ah + const bh = this._bh + const ainv = this._ainv + const k = this._k + const sat = this.sat + + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + let z0 = this._z0 + let z1 = this._z1 + let z2 = this._z2 + let z3 = this._z3 + let z4 = this._z4 + + let s0: f32 + let s: f32 + let y0: f32 + let y1: f32 + let y2: f32 + let y3: f32 + let y4: f32 + let y5: f32 + + for (; i < end; i += 1) { + // unroll(16, () => { + sample = f32.load(inp) + + // current state + s0 = (a2 * a * z0 + a2 * b * z1 + z2 * (b2 - 2.0 * a2) * a + z3 * (b2 - 3.0 * a2) * b) * c + s = bh * s0 - z4 + + // solve feedback loop (linear) + y5 = (g * sample + s) / (1.0 + g * k) + + // input clipping + y0 = soft(sample - k * y5, sat) + y5 = g * y0 + s + + // compute integrator outputs + y4 = g0 * y0 + s0 + y3 = (b * y4 - z3) * ainv + y2 = (b * y3 - a * y4 - z2) * ainv + y1 = (b * y2 - a * y3 - z1) * ainv + + // update filter state + z0 += 4.0 * a * (y0 - y1 + y2) + z1 += 2.0 * a * (y1 - 2.0 * y2 + y3) + z2 += 2.0 * a * (y2 - 2.0 * y3 + y4) + z3 += 2.0 * a * (y3 - 2.0 * y4) + z4 = bh * y4 + ah * y5 + + sample = A * y4 + + f32.store(out, sample) + inp += 4 + out += 4 + // }) + } + + // update filter state + this._z0 = z0 + this._z1 = z1 + this._z2 = z2 + this._z3 = z3 + this._z4 = z4 + } +} diff --git a/as/assembly/dsp/gen/exp.ts b/as/assembly/dsp/gen/exp.ts new file mode 100644 index 0000000..a5f3aaa --- /dev/null +++ b/as/assembly/dsp/gen/exp.ts @@ -0,0 +1,7 @@ +import { Osc } from './osc' + +export class Exp extends Osc { + get _table(): StaticArray { + return this._engine.wavetable.exp + } +} diff --git a/as/assembly/dsp/gen/freesound.ts b/as/assembly/dsp/gen/freesound.ts new file mode 100644 index 0000000..bb4a41b --- /dev/null +++ b/as/assembly/dsp/gen/freesound.ts @@ -0,0 +1,10 @@ +import { Smp } from './smp' + +export class Freesound extends Smp { + id: i32 = 0 + + _update(): void { + this._floats = !this.id ? null : changetype>(this.id) + super._update() + } +} diff --git a/as/assembly/dsp/gen/gen.ts b/as/assembly/dsp/gen/gen.ts new file mode 100644 index 0000000..bd87ff7 --- /dev/null +++ b/as/assembly/dsp/gen/gen.ts @@ -0,0 +1,10 @@ +import { Engine } from '../core/engine' + +export abstract class Gen { + gain: f32 = 1 + constructor(public _engine: Engine) { } + _update(): void { } + _reset(): void { } + _audio(begin: u32, end: u32, out: usize): void { } + _audio_stereo(begin: u32, end: u32, out_0: usize, out_1: usize): void { } +} diff --git a/as/assembly/dsp/gen/gendy.ts b/as/assembly/dsp/gen/gendy.ts new file mode 100644 index 0000000..9d04bc7 --- /dev/null +++ b/as/assembly/dsp/gen/gendy.ts @@ -0,0 +1,28 @@ +import { rnd } from '../../util' +import { Gen } from './gen' + +export class Gendy extends Gen { + step: f32 = 0.00001 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + out += offset + + let value: f32 = 0.0 + + for (; i < end; i += 16) { + unroll(16, () => { + + value += (f32(rnd()) * this.step * (f32(rnd()) > 0.1 ? 1.0 : -1.0)) //(f32(rnd()) < this.amt ? f32(rnd()) : 0.0) + f32.store(out, value) + + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/grain.ts b/as/assembly/dsp/gen/grain.ts new file mode 100644 index 0000000..f275322 --- /dev/null +++ b/as/assembly/dsp/gen/grain.ts @@ -0,0 +1,28 @@ +import { Gen } from './gen' +import { rnd } from '../../util' + +export class Grain extends Gen { + amt: f32 = 1.0; + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + out += offset + + let value: f32 = 0 + + for (; i < end; i += 16) { + unroll(16, () => { + + value = (f32(rnd()) < this.amt ? f32(rnd()) : 0.0) + f32.store(out, value) + + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/inc.ts b/as/assembly/dsp/gen/inc.ts new file mode 100644 index 0000000..81964f6 --- /dev/null +++ b/as/assembly/dsp/gen/inc.ts @@ -0,0 +1,45 @@ +import { Gen } from './gen' + +export class Inc extends Gen { + amt: f32 = 1.0; + + /** Trigger phase sync when set to 0. */ + trig: f32 = -1.0 + _lastTrig: i32 = -1 + + _value: f32 = 0.0 + + _reset(): void { + this.trig = -1.0 + this._lastTrig = -1 + } + + _update(): void { + if (this._lastTrig !== i32(this.trig)) { + this._value = 0.0 + } + + this._lastTrig = i32(this.trig) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + let i: u32 = begin + end = i + length + + const offset = begin << 2 + out += offset + const amt: f32 = this.amt * 0.001 + let value: f32 = this._value + + for (; i < end; i += 16) { + unroll(16, () => { + f32.store(out, value) + value += amt + out += 4 + }) + } + + this._value = value + } +} diff --git a/as/assembly/dsp/gen/lp.ts b/as/assembly/dsp/gen/lp.ts new file mode 100644 index 0000000..ec82d9b --- /dev/null +++ b/as/assembly/dsp/gen/lp.ts @@ -0,0 +1,46 @@ +import { Gen } from './gen' + +export class Lp extends Gen { + cut: f32 = 500; + in: u32 = 0 + + _alpha: f32 = 0 + _sample: f32 = 0 + + _update(): void { + const omega: f32 = 1.0 / (2.0 * Mathf.PI * this.cut) + const dt: f32 = 1.0 / f32(this._engine.sampleRate) + this._alpha = dt / (omega + dt) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + const alpha: f32 = this._alpha + + let sample: f32 = 0 + let prev: f32 = this._sample + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + sample = alpha * sample + (1.0 - alpha) * prev + // Store the current sample's value for use in the next iteration + prev = sample + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + + this._sample = prev + } +} diff --git a/as/assembly/dsp/gen/mhp.ts b/as/assembly/dsp/gen/mhp.ts new file mode 100644 index 0000000..d0ee8d6 --- /dev/null +++ b/as/assembly/dsp/gen/mhp.ts @@ -0,0 +1,35 @@ +import { Moog } from './moog' + +export class Mhp extends Moog { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._highpass() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/mlp.ts b/as/assembly/dsp/gen/mlp.ts new file mode 100644 index 0000000..2b5a6c2 --- /dev/null +++ b/as/assembly/dsp/gen/mlp.ts @@ -0,0 +1,35 @@ +import { Moog } from './moog' + +export class Mlp extends Moog { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._lowpass() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/moog.ts b/as/assembly/dsp/gen/moog.ts new file mode 100644 index 0000000..67af059 --- /dev/null +++ b/as/assembly/dsp/gen/moog.ts @@ -0,0 +1,96 @@ +import { paramClamp } from '../../util' +import { Gen } from './gen' + +// TODO: f64 + +function tanha(x: f32): f32 { + return x / (1.0 + x * x / (3.0 + x * x / 5.0)) +} + +// https://github.com/mixxxdj/mixxx/blob/main/src/engine/filters/enginefiltermoogladder4.h +export class Moog extends Gen { + in: u32 = 0 + + _m_azt1: f32 = 0 + _m_azt2: f32 = 0 + _m_azt3: f32 = 0 + _m_azt4: f32 = 0 + _m_az5: f32 = 0 + _m_amf: f32 = 0 + + _v2: f32 = 0 + _x1: f32 = 0 + _az3: f32 = 0 + _az4: f32 = 0 + _amf: f32 = 0 + + _kVt: f32 = 1.2 + + _m_kacr: f32 = 0 + _m_k2vg: f32 = 0 + _m_postGain: f32 = 0 + + _params_freq: f32[] = [50, 22040, 4000] + _params_Q: f32[] = [0.01, 0.985, 0.5] + + @inline _validate(freq: f32, Q: f32): boolean { + if (freq <= 0) return false + if (freq !== freq) return false + if (Q <= 0) return false + if (Q !== Q) return false + return true + } + + @inline _updateCoeffs(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + + this._v2 = 2.0 + this._kVt + const kfc: f32 = freq / f32(this._engine.sampleRate) + const kf: f32 = kfc + + const kfcr: f32 = 1.8730 * (kfc * kfc * kfc) + 0.4955 * (kfc * kfc) - + 0.6490 * kfc + 0.9988 + + const x: f32 = -2.0 * Mathf.PI * kfcr * kf + const exp_out: f32 = Mathf.exp(x) + const m_k2vgNew: f32 = this._v2 * (1.0 - exp_out) + const m_kacrNew: f32 = Q * (-3.9364 * (kfc * kfc) + 1.8409 * kfc + 0.9968) + const m_postGainNew: f32 = 1.0001784074555027 + (0.9331585678097162 * Q) + + this._m_postGain = m_postGainNew + this._m_kacr = m_kacrNew + this._m_k2vg = m_k2vgNew + } + + @inline _process(x0: f32): void { + this._x1 = x0 - this._m_amf * this._m_kacr; + const az1: f32 = this._m_azt1 + this._m_k2vg * tanha(this._x1 / this._v2); + const at1: f32 = this._m_k2vg * tanha(az1 / this._v2); + this._m_azt1 = az1 - at1 + + const az2 = this._m_azt2 + at1 + const at2 = this._m_k2vg * tanha(az2 / this._v2) + this._m_azt2 = az2 - at2 + + this._az3 = this._m_azt3 + at2; + const at3 = this._m_k2vg * tanha(this._az3 / this._v2); + this._m_azt3 = this._az3 - at3 + + this._az4 = this._m_azt4 + at3; + const at4 = this._m_k2vg * tanha(this._az4 / this._v2); + this._m_azt4 = this._az4 - at4; + + // this is for oversampling but we're not doing it here yet, see link + this._m_amf = this._az4; + } + + @inline _lowpass(): f32 { + return this._m_amf * this._m_postGain + } + + @inline _highpass(): f32 { + return (this._x1 - 3.0 * this._az3 + 2 * this._az4) * this._m_postGain + } +} diff --git a/as/assembly/dsp/gen/noi.ts b/as/assembly/dsp/gen/noi.ts new file mode 100644 index 0000000..f45ccea --- /dev/null +++ b/as/assembly/dsp/gen/noi.ts @@ -0,0 +1,7 @@ +import { Osc } from './osc' + +export class Noi extends Osc { + get _table(): StaticArray { + return this._engine.wavetable.noise + } +} diff --git a/as/assembly/dsp/gen/nrate.ts b/as/assembly/dsp/gen/nrate.ts new file mode 100644 index 0000000..b2e4c37 --- /dev/null +++ b/as/assembly/dsp/gen/nrate.ts @@ -0,0 +1,20 @@ +import { rateToPhaseStep } from '../../util' +import { Engine } from '../core/engine' +import { Gen } from './gen' + +export class Nrate extends Gen { + normal: f32 = 1.0 + _reset(): void { + this.normal = 1.0 + this._update() + } + _update(): void { + let samples: u32 = u32(f32(this._engine.sampleRate) * this.normal) + if (samples < 1) samples = 1 + + this._engine.rateSamples = samples + this._engine.rateSamplesRecip = (1.0 / f64(this._engine.rateSamples)) + this._engine.rateStep = rateToPhaseStep(samples) + this._engine.samplesPerMs = f64(this._engine.rateSamples) / 1000 + } +} diff --git a/as/assembly/dsp/gen/osc.ts b/as/assembly/dsp/gen/osc.ts new file mode 100644 index 0000000..de9ecd8 --- /dev/null +++ b/as/assembly/dsp/gen/osc.ts @@ -0,0 +1,65 @@ +import { Gen } from './gen' + +export abstract class Osc extends Gen { + /** Frequency. */ + hz: f32 = 440 + /** Trigger phase sync when set to 0. */ + trig: f32 = -1.0 + /** Phase offset. */ + offset: f32 = 0 + + _phase: u32 = 0 + _step: u32 = 0 + _sample: f32 = 0 + _lastTrig: i32 = -1 + _offsetU32: u32 = 0 + _initial: boolean = true + + get _table(): StaticArray { + return this._engine.wavetable.sine + } + + get _mask(): u32 { + return this._engine.wavetable.mask + } + + _reset(): void { + this.hz = 0 + this.trig = -1.0 + this.offset = 0 + this._lastTrig = -1 + this._phase = 0 + } + + _update(): void { + // TODO: the / 8 needs to be determined and not hard coded + this._step = u32(this.hz * this._engine.rateStep / 8) + this._offsetU32 = u32(this.offset * 0xFFFFFFFF) + this.offset = 0 + + if (this._lastTrig !== i32(this.trig)) { + this._phase = 0 + } + + // TODO: implement Sync (zero crossing reset phase) + + this._lastTrig = i32(this.trig) + } + + _next(): void { + this._phase += this._step + } + + _audio(begin: u32, end: u32, targetPtr: usize): void { + this._phase = this._engine.wavetable.read( + changetype(this._table), + this._mask, + this._phase, + this._offsetU32, + this._step, + begin, + end, + targetPtr + ) + } +} diff --git a/as/assembly/dsp/gen/ramp.ts b/as/assembly/dsp/gen/ramp.ts new file mode 100644 index 0000000..95a6aa3 --- /dev/null +++ b/as/assembly/dsp/gen/ramp.ts @@ -0,0 +1,7 @@ +import { Aosc } from './aosc' + +export class Ramp extends Aosc { + get _tables(): StaticArray> { + return this._engine.wavetable.antialias.ramp + } +} diff --git a/as/assembly/dsp/gen/rate.ts b/as/assembly/dsp/gen/rate.ts new file mode 100644 index 0000000..fc06c35 --- /dev/null +++ b/as/assembly/dsp/gen/rate.ts @@ -0,0 +1,17 @@ +import { rateToPhaseStep } from '../../util' +import { Engine } from '../core/engine' +import { Gen } from './gen' + +export class Rate extends Gen { + samples: f32 + constructor(public _engine: Engine) { + super(_engine) + this.samples = f32(_engine.sampleRate) + } + _update(): void { + this._engine.rateSamples = u32(this.samples) + this._engine.rateSamplesRecip = (1.0 / f64(this._engine.rateSamples)) + this._engine.rateStep = rateToPhaseStep(u32(this.samples)) + this._engine.samplesPerMs = f64(this._engine.rateSamples) / 1000 + } +} diff --git a/as/assembly/dsp/gen/sap.ts b/as/assembly/dsp/gen/sap.ts new file mode 100644 index 0000000..a5d6925 --- /dev/null +++ b/as/assembly/dsp/gen/sap.ts @@ -0,0 +1,35 @@ +import { Svf } from './svf' + +export class Sap extends Svf { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._allpass() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/saw.ts b/as/assembly/dsp/gen/saw.ts new file mode 100644 index 0000000..6ec9419 --- /dev/null +++ b/as/assembly/dsp/gen/saw.ts @@ -0,0 +1,7 @@ +import { Aosc } from './aosc' + +export class Saw extends Aosc { + get _tables(): StaticArray> { + return this._engine.wavetable.antialias.saw + } +} diff --git a/as/assembly/dsp/gen/say.ts b/as/assembly/dsp/gen/say.ts new file mode 100644 index 0000000..a64385a --- /dev/null +++ b/as/assembly/dsp/gen/say.ts @@ -0,0 +1,10 @@ +import { Smp } from './smp' + +export class Say extends Smp { + text: i32 = 0 + + _update(): void { + this._floats = !this.text ? null : changetype>(this.text) + super._update() + } +} diff --git a/as/assembly/dsp/gen/sbp.ts b/as/assembly/dsp/gen/sbp.ts new file mode 100644 index 0000000..0d8a860 --- /dev/null +++ b/as/assembly/dsp/gen/sbp.ts @@ -0,0 +1,35 @@ +import { Svf } from './svf' + +export class Sbp extends Svf { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._bandpass() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/shp.ts b/as/assembly/dsp/gen/shp.ts new file mode 100644 index 0000000..3a91dea --- /dev/null +++ b/as/assembly/dsp/gen/shp.ts @@ -0,0 +1,35 @@ +import { Svf } from './svf' + +export class Shp extends Svf { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._highpass() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/sin.ts b/as/assembly/dsp/gen/sin.ts new file mode 100644 index 0000000..58199d0 --- /dev/null +++ b/as/assembly/dsp/gen/sin.ts @@ -0,0 +1,7 @@ +import { Osc } from './osc' + +export class Sin extends Osc { + get _table(): StaticArray { + return this._engine.wavetable.sine + } +} diff --git a/as/assembly/dsp/gen/slp.ts b/as/assembly/dsp/gen/slp.ts new file mode 100644 index 0000000..e6887e0 --- /dev/null +++ b/as/assembly/dsp/gen/slp.ts @@ -0,0 +1,35 @@ +import { Svf } from './svf' + +export class Slp extends Svf { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._lowpass() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/smp.ts b/as/assembly/dsp/gen/smp.ts new file mode 100644 index 0000000..0e6fb17 --- /dev/null +++ b/as/assembly/dsp/gen/smp.ts @@ -0,0 +1,135 @@ +import { clamp, clamp64, cubicMod } from '../../util' +import { Gen } from './gen' + +export class Smp extends Gen { + offset: f32 = 0 + length: f32 = 1 + + trig: f32 = 0 + _lastTrig: i32 = -1 + + _floats: StaticArray | null = null + _floatsSampleRate: f64 = 48000 + + _index: f64 = 0 + _step: f64 = 0 + + _offsetCurrent: f64 = -1 + _offsetTarget: f64 = 0 + + _initial: boolean = true + + _reset(): void { + this._initial = true + this.offset = 0 + this.length = 1 + } + + _update(): void { + this._offsetTarget = f64(this.offset) + + if (this._initial) { + this._offsetCurrent = this._offsetTarget + } + + if (this._initial || this._lastTrig !== i32(this.trig)) { + this._initial = false + this._index = 0 + } + + this._lastTrig = i32(this.trig) + } + + _audio(begin: u32, end: u32, out: usize): void { + const floats: StaticArray | null = this._floats + if (!floats) return + + const length: u32 = u32(Math.floor(f64(clamp(0, 1, 1, this.length) ) * f64(floats.length))) + let offsetCurrent: f64 = f64(clamp64(0, 1, 0, this._offsetCurrent)) + const offsetTarget: f64 = f64(clamp64(0, 1, 0, this._offsetTarget)) + + const step: f64 = this._floatsSampleRate / this._engine.rateSamples + let index: f64 = this._index + let sample: f32 + let i: u32 = begin + + out += begin << 2 + + for (; i < end; i += 16) { + unroll(16, () => { + sample = cubicMod(floats, index + offsetCurrent * f64(floats.length), length) + f32.store(out, sample) + out += 4 + index += step + offsetCurrent += (offsetTarget - offsetCurrent) * 0.0008 + }) + } + + this._index = index % f64(length) + this._offsetCurrent = offsetCurrent + } +} +// import { DELAY_MAX_SIZE, SAMPLE_MAX_SIZE } from '../core/constants' +// import { logd, logf, logi } from '../../env' +// import { cubic, cubicFrac, phaseFrac } from '../../util' +// import { Gen } from './gen' + +// export class Sample extends Gen { +// offset: f32 = 0; +// trig: f32 = 0 + +// _floats: StaticArray = new StaticArray(SAMPLE_MAX_SIZE) +// _mask: u32 = SAMPLE_MAX_SIZE - 1 +// _phase: u32 = 0 +// _step: u32 = 0 +// _offsetCurrent: f64 = -1 +// _offsetTarget: f64 = 0 + +// _lastTrig: f32 = -1 + +// _update(): void { +// this._offsetTarget = this.offset * (this._engine.rateStep >> 2) + this._mask +// if (this._offsetCurrent === -1) this._offsetCurrent = this._offsetTarget +// this.offset = 0 + +// if (this._lastTrig !== 0 && this.trig === 0) { +// this._phase = 0 +// } + +// this._lastTrig = this.trig +// } + +// _audio(begin: u32, end: u32, out: usize): void { +// const mask: u32 = this._mask +// const step: u32 = this._engine.rateStep >> 2 + +// let offsetCurrent: f64 = this._offsetCurrent +// const offsetTarget: f64 = this._offsetTarget + +// let phase: u32 = this._phase +// let index: u32 +// let sample: f32 +// let frac: f32 +// let offset: u32 + +// let i: u32 = begin + +// out += begin << 2 + +// for (; i < end; i += 16) { +// unroll(16, () => { +// offset = phase >> 14 +// index = (offset + u32(offsetCurrent)) & mask +// frac = phaseFrac(phase) +// sample = cubicFrac(this._floats, index, frac, mask) +// f32.store(out, sample) +// out += 4 +// phase += step +// offsetCurrent += (offsetTarget - offsetCurrent) * 0.0008 +// }) +// } + +// this._phase = phase +// this._offsetCurrent = offsetCurrent +// } +// } diff --git a/as/assembly/dsp/gen/sno.ts b/as/assembly/dsp/gen/sno.ts new file mode 100644 index 0000000..b15d5ad --- /dev/null +++ b/as/assembly/dsp/gen/sno.ts @@ -0,0 +1,35 @@ +import { Svf } from './svf' + +export class Sno extends Svf { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._notch() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/spk.ts b/as/assembly/dsp/gen/spk.ts new file mode 100644 index 0000000..b7565c9 --- /dev/null +++ b/as/assembly/dsp/gen/spk.ts @@ -0,0 +1,35 @@ +import { Svf } from './svf' + +export class Spk extends Svf { + cut: f32 = 500 + q: f32 = 0.5 + + _update(): void { + this._updateCoeffs(this.cut, this.q) + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + this._process(sample) + sample = this._peak() + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/sqr.ts b/as/assembly/dsp/gen/sqr.ts new file mode 100644 index 0000000..fa6d0f9 --- /dev/null +++ b/as/assembly/dsp/gen/sqr.ts @@ -0,0 +1,7 @@ +import { Aosc } from './aosc' + +export class Sqr extends Aosc { + get _tables(): StaticArray> { + return this._engine.wavetable.antialias.sqr + } +} diff --git a/as/assembly/dsp/gen/svf.ts b/as/assembly/dsp/gen/svf.ts new file mode 100644 index 0000000..20fa872 --- /dev/null +++ b/as/assembly/dsp/gen/svf.ts @@ -0,0 +1,89 @@ +import { paramClamp } from '../../util' +import { Gen } from './gen' + +export class Svf extends Gen { + in: u32 = 0 + + _c1: f64 = 0 + _c2: f64 = 0 + + _a1: f64 = 1 + _a2: f64 = 0 + _a3: f64 = 0 + + _v0: f64 = 0 + _v1: f64 = 0 + _v2: f64 = 0 + _v3: f64 = 0 + + _k: f64 = 0 + + _params_freq: f32[] = [50, 22040, 4000] + _params_Q: f32[] = [0.01, 0.985, 1.0] + + _reset(): void { + this._clear() + } + + @inline _clear(): void { + this._a1 = 0 + this._a2 = 0 + this._a3 = 0 + + this._v1 = 0 + this._v2 = 0 + this._v3 = 0 + } + + @inline _validate(freq: f32, Q: f32): boolean { + if (freq <= 0) return false + if (freq !== freq) return false + if (Q <= 0) return false + if (Q !== Q) return false + return true + } + + @inline _updateCoeffs(freq: f32, Q: f32): void { + if (!this._validate(freq, Q)) return + freq = paramClamp(this._params_freq, freq) + Q = paramClamp(this._params_Q, Q) + const g: f64 = Math.tan(Math.PI * freq / f64(this._engine.sampleRate)) + this._k = 2.0 - 2.0 * Q + this._a1 = 1.0 / (1.0 + g * (g + this._k)) + this._a2 = g * this._a1 + this._a3 = g * this._a2 + } + + @inline _process(v0: f32): void { + this._v0 = f64(v0) + this._v3 = v0 - this._c2 + this._v1 = this._a1 * this._c1 + this._a2 * this._v3 + this._v2 = this._c2 + this._a2 * this._c1 + this._a3 * this._v3 + this._c1 = 2.0 * this._v1 - this._c1 + this._c2 = 2.0 * this._v2 - this._c2 + } + + @inline _lowpass(): f32 { + return f32(this._v2) + } + + @inline _bandpass(): f32 { + return f32(this._v1) + } + + @inline _highpass(): f32 { + return f32(this._v0 - this._k * this._v1 - this._v2) + } + + @inline _notch(): f32 { + return f32(this._v0 - this._k * this._v1) + } + + @inline _peak(): f32 { + return f32(this._v0 - this._k * this._v1 - 2.0 * this._v2) + } + + @inline _allpass(): f32 { + return f32(this._v0 - 2.0 * this._k * this._v1) + } +} diff --git a/as/assembly/dsp/gen/tanh.ts b/as/assembly/dsp/gen/tanh.ts new file mode 100644 index 0000000..4e1cefc --- /dev/null +++ b/as/assembly/dsp/gen/tanh.ts @@ -0,0 +1,32 @@ +import { Gen } from './gen' + +export class Tanh extends Gen { + gain: f32 = 1.0; + in: u32 = 0 + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + const gain: f32 = this.gain + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + sample = Mathf.tanh(sample * gain) + f32.store(out, sample) + inp += 4 + out += 4 + }) + } + } +} diff --git a/as/assembly/dsp/gen/tanha.ts b/as/assembly/dsp/gen/tanha.ts new file mode 100644 index 0000000..b743df1 --- /dev/null +++ b/as/assembly/dsp/gen/tanha.ts @@ -0,0 +1,63 @@ +import { Gen } from './gen' + +export class Tanha extends Gen { + gain: f32 = 1 + _gainv: v128 = f32x4.splat(1.0); + + in: u32 = 0 + + _update(): void { + this._gainv = f32x4.splat(this.gain) + } + + _audio(begin: u32, end: u32, out: usize): void { + const gainv: v128 = this._gainv + + let in0: u32 = this.in + + let x: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + let x2: v128 + + for (; i < end; i += 64) { + unroll(16, () => { + x = v128.load(in0) + + x = f32x4.mul(x, gainv) + + // Calculate x * x + x2 = f32x4.mul(x, x) + + // Calculate 5.0 + x * x + resv = f32x4.add(f32x4.splat(5.0), x2) + + // Calculate x * x / (5.0 + x * x) + resv = f32x4.div(x2, resv) + + // Calculate 3.0 + x * x / (5.0 + x * x) + resv = f32x4.add(f32x4.splat(3.0), resv) + + // Calculate x / (1.0 + x * x / (3.0 + x * x / 5.0)) + resv = f32x4.div(x, f32x4.add(f32x4.splat(1.0), f32x4.div(x2, resv))) + + // Calculate min(x / (1.0 + x * x / (3.0 + x * x / 5.0)), 1.0) + resv = f32x4.min(resv, f32x4.splat(1.0)) + + // Calculate max(-1.0, min(x / (1.0 + x * x / (3.0 + x * x / 5.0)), 1.0)) + resv = f32x4.max(resv, f32x4.splat(-1.0)) + + v128.store(out, resv) + + in0 += 16 + out += 16 + }) + } + } +} diff --git a/as/assembly/dsp/gen/tap.ts b/as/assembly/dsp/gen/tap.ts new file mode 100644 index 0000000..98bbe29 --- /dev/null +++ b/as/assembly/dsp/gen/tap.ts @@ -0,0 +1,58 @@ +import { cubic } from '../../util' +import { DELAY_MAX_SIZE } from '../core/constants' +import { Gen } from './gen' + +export class Tap extends Gen { + ms: f32 = 200; + in: u32 = 0; + + _floats: StaticArray = new StaticArray(DELAY_MAX_SIZE) + _mask: u32 = DELAY_MAX_SIZE - 1 + _index: u32 = 0 + _stepf: f32 = 0 + _targetf: f32 = 0 + + _update(): void { + this._targetf = Mathf.min(DELAY_MAX_SIZE - 1, (this.ms * 0.001) * this._engine.rateStep) + if (this._stepf === 0) this._stepf = this._targetf + } + + _audio(begin: u32, end: u32, out: usize): void { + const length: u32 = end - begin + + let sample: f32 = 0 + let inp: u32 = this.in + + let i: u32 = begin + end = i + length + + const offset = begin << 2 + inp += offset + out += offset + + const mask: u32 = this._mask + let index: u32 = this._index + let delay: f32 = 0 + let stepf: f32 = this._stepf + const targetf: f32 = this._targetf + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(inp) + + delay = cubic(this._floats, (index - stepf), mask) + this._floats[index] = sample + f32.store(out, delay) + + inp += 4 + out += 4 + + index = (index + 1) & mask + stepf += (targetf - stepf) * 0.0008 + }) + } + + this._index = index + this._stepf = stepf + } +} diff --git a/as/assembly/dsp/gen/tri.ts b/as/assembly/dsp/gen/tri.ts new file mode 100644 index 0000000..b6af520 --- /dev/null +++ b/as/assembly/dsp/gen/tri.ts @@ -0,0 +1,7 @@ +import { Aosc } from './aosc' + +export class Tri extends Aosc { + get _tables(): StaticArray> { + return this._engine.wavetable.antialias.tri + } +} diff --git a/as/assembly/dsp/gen/zero.ts b/as/assembly/dsp/gen/zero.ts new file mode 100644 index 0000000..67e7e23 --- /dev/null +++ b/as/assembly/dsp/gen/zero.ts @@ -0,0 +1,19 @@ +import { Gen } from './gen' + +export class Zero extends Gen { + _audio(begin: u32, end: u32, out: usize): void { + const zerov: v128 = f32x4.splat(0) + + let i: u32 = begin + + const offset = begin << 2 + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + v128.store(out, zerov) + out += 16 + }) + } + } +} diff --git a/as/assembly/dsp/graph/copy.ts b/as/assembly/dsp/graph/copy.ts new file mode 100644 index 0000000..8808fb3 --- /dev/null +++ b/as/assembly/dsp/graph/copy.ts @@ -0,0 +1,75 @@ +export function copyMem( + inp: usize, + out: usize, + size: usize +): void { + memory.copy(out, inp, size) + + // let inpv: v128 + + // let i: u32 = 0 + // length = length << 2 + + // for (; i < length; i += 64) { + // unroll(16, () => { + // inpv = v128.load(inp) + // v128.store(out, inpv) + // inp += 16 + // out += 16 + // }) + // } +} + +export function copyInto( + begin: u32, + end: u32, + inp: usize, + out: usize, +): void { + const size: usize = (end - begin) << 2 + const offset: usize = begin << 2 + memory.copy( + out + offset, + inp + offset, + size + ) +} + +export function copyAt( + begin: u32, + end: u32, + inp: usize, + out: usize, +): void { + const size: usize = (end - begin) << 2 + const offset: usize = begin << 2 + memory.copy( + out, + inp + offset, + size + ) +} + +// export function copyInto( +// begin: u32, +// end: u32, +// inp: usize, +// out: usize, +// ): void { +// let inpv: v128 + +// let i: u32 = begin + +// const offset = begin << 2 +// inp += offset +// out += offset + +// for (; i < end; i += 64) { +// unroll(16, () => { +// inpv = v128.load(inp) +// v128.store(out, inpv) +// inp += 16 +// out += 16 +// }) +// } +// } diff --git a/as/assembly/dsp/graph/dc-bias-old.ts b/as/assembly/dsp/graph/dc-bias-old.ts new file mode 100644 index 0000000..b8510ce --- /dev/null +++ b/as/assembly/dsp/graph/dc-bias-old.ts @@ -0,0 +1,42 @@ +import { logf } from '../../env' + +export function dcBias( + begin: u32, + end: u32, + block: usize, +): void { + const ptr: usize = block + const length: u32 = end - begin + let i: u32 = begin + let resv: v128 = f32x4.splat(0) + + end = i + (length >> 2) // divide length by 4 because we process 4 elements at a time + + for (; i < end; i += 32) { + unroll(32, () => { + resv = f32x4.add(resv, v128.load(block)) + block += 16 + }) + } + + const sum: f32 = + f32x4.extract_lane(resv, 0) + + f32x4.extract_lane(resv, 1) + + f32x4.extract_lane(resv, 2) + + f32x4.extract_lane(resv, 3) + + const mean: f32 = sum / f32(length) + logf(mean) + const meanv: v128 = f32x4.splat(mean) + + block = ptr + i = begin + for (; i < end; i += 32) { + unroll(32, () => { + resv = v128.load(block) + resv = f32x4.sub(resv, meanv) + v128.store(block, resv) + block += 16 + }) + } +} diff --git a/as/assembly/dsp/graph/dc-bias.ts b/as/assembly/dsp/graph/dc-bias.ts new file mode 100644 index 0000000..4fc4240 --- /dev/null +++ b/as/assembly/dsp/graph/dc-bias.ts @@ -0,0 +1,43 @@ +import { logf } from '../../env' + +export function dcBias( + begin: u32, + end: u32, + block: usize, +): void { + const length: u32 = end - begin + let sample: f32 = 0 + let prev: f32 = 0 + let diff: f32 = 0 + let abs: f32 = 0 + const threshold: f32 = 0.6 + let alpha: f32 = 0 + + let i: u32 = begin + end = i + (length << 2) + + const offset = begin << 2 + block += offset + + for (; i < end; i += 16) { + unroll(16, () => { + sample = f32.load(block) + // logf(sample) + diff = sample - prev + abs = Mathf.abs(diff) + if (abs > threshold) { + alpha = (threshold - abs) / threshold + if (alpha > 1) alpha = 1 + else if (alpha < 0) alpha = 0 + prev = sample + sample = prev + alpha * diff + // logf(sample) + f32.store(block, sample) + } + else { + prev = sample + } + block += 4 + }) + } +} diff --git a/as/assembly/dsp/graph/fade.ts b/as/assembly/dsp/graph/fade.ts new file mode 100644 index 0000000..327677c --- /dev/null +++ b/as/assembly/dsp/graph/fade.ts @@ -0,0 +1,107 @@ +export function fadeIn( + total: u32, + begin: u32, + end: u32, + block: usize, +): void { + let gainv: v128 = f32x4.splat(0) + let frame: u32 = 0 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + block += offset + + for (; i < end; i += 64) { + unroll(16, () => { + gainv = f32x4.splat(Mathf.min(1.0, f32(frame) / f32(total))) + resv = v128.load(block) + resv = f32x4.mul(resv, gainv) + v128.store(block, resv) + block += 16 + frame += 4 + }) + } +} + +export function fadeIn16( + total: u32, + begin: u32, + end: u32, + block: usize, +): void { + let gainv: v128 = f32x4.splat(0) + let frame: u32 = 0 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + block += offset + + for (; i < end; i += 16) { + unroll(4, () => { + gainv = f32x4.splat(Mathf.min(1.0, f32(frame) / f32(total))) + resv = v128.load(block) + resv = f32x4.mul(resv, gainv) + v128.store(block, resv) + block += 16 + frame += 4 + }) + } +} + +export function fadeOut( + total: u32, + begin: u32, + end: u32, + block: usize, +): void { + let gainv: v128 = f32x4.splat(0) + let frame: u32 = 0 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + block += offset + + for (; i < end; i += 64) { + unroll(16, () => { + gainv = f32x4.splat(Mathf.max(0, 1.0 - (f32(frame) / f32(total)))) + resv = v128.load(block) + resv = f32x4.mul(resv, gainv) + v128.store(block, resv) + block += 16 + frame += 4 + }) + } +} + +export function fadeOut16( + total: u32, + begin: u32, + end: u32, + block: usize, +): void { + let gainv: v128 = f32x4.splat(0) + let frame: u32 = 0 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + block += offset + + for (; i < end; i += 16) { + unroll(4, () => { + gainv = f32x4.splat(Mathf.max(0, 1.0 - (f32(frame) / f32(total)))) + resv = v128.load(block) + resv = f32x4.mul(resv, gainv) + v128.store(block, resv) + block += 16 + frame += 4 + }) + } +} diff --git a/as/assembly/dsp/graph/fill.ts b/as/assembly/dsp/graph/fill.ts new file mode 100644 index 0000000..e02a773 --- /dev/null +++ b/as/assembly/dsp/graph/fill.ts @@ -0,0 +1,15 @@ +export function fill(value: f32, begin: u32, end: u32, out: usize): void { + const v: v128 = f32x4.splat(value) + + let i: u32 = begin + + const offset = begin << 2 + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + v128.store(out, v) + out += 16 + }) + } +} diff --git a/as/assembly/dsp/graph/join.ts b/as/assembly/dsp/graph/join.ts new file mode 100644 index 0000000..517e251 --- /dev/null +++ b/as/assembly/dsp/graph/join.ts @@ -0,0 +1,72 @@ +import { logi } from '../../env' + +export function join21g( + begin: u32, + end: u32, + in0: usize, + in1: usize, + out: usize, + gain0: f32, + gain1: f32, +): void { + const gain0v: v128 = f32x4.splat(gain0) + const gain1v: v128 = f32x4.splat(gain1) + + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.add( + f32x4.mul(in0v, gain0v), + f32x4.mul(in1v, gain1v) + ) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +// TODO: consolidate with math.add_audio_audio +export function join21( + begin: u32, + end: u32, + in0: usize, + in1: usize, + out: usize, +): void { + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.add(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} diff --git a/as/assembly/dsp/graph/math.ts b/as/assembly/dsp/graph/math.ts new file mode 100644 index 0000000..4025920 --- /dev/null +++ b/as/assembly/dsp/graph/math.ts @@ -0,0 +1,559 @@ +export function pow_scalar_scalar( + n1: f32, + n2: f32, +): f32 { + return Mathf.pow(n1, n2) +} + +export function pow_audio_scalar( + in0: usize, + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + let in0f: f32 + let res: f32 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + in0f = f32.load(in0) + res = Mathf.pow(in0f, scalar) + f32.store(out, res) + in0 += 4 + out += 4 + }) + } +} + +export function pow_audio_audio( + in0: usize, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + let in0f: f32 + let in1f: f32 + let res: f32 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + in0f = f32.load(in0) + in1f = f32.load(in1) + res = Mathf.pow(in0f, in1f) + f32.store(out, res) + in0 += 4 + in1 += 4 + out += 4 + }) + } +} + +export function pow_scalar_audio( + scalar: f32, + in0: usize, + begin: u32, + end: u32, + out: usize, +): void { + let in0f: f32 + let res: f32 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 16) { + unroll(16, () => { + in0f = f32.load(in0) + res = Mathf.pow(scalar, in0f) + f32.store(out, res) + in0 += 4 + out += 4 + }) + } +} + +export function mul_audio_scalar( + in0: usize, + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.mul(in0v, scalarv) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function div_audio_scalar( + in0: usize, + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.div(in0v, scalarv) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function div_scalar_audio( + scalar: f32, + in0: usize, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.div(scalarv, in0v) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function add_audio_scalar( + in0: usize, + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.add(in0v, scalarv) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function sub_audio_scalar( + in0: usize, + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.sub(in0v, scalarv) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function sub_scalar_audio( + scalar: f32, + in0: usize, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.sub(scalarv, in0v) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function add_audio_audio( + in0: usize, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.add(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +export function addmul_audio_audio_scalar( + in0: usize, + in1: usize, + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.add(in0v, in1v) + resv = f32x4.mul(resv, scalarv) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +export function mul_audio_scalar_add_audio( + in0: usize, + scalar: f32, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in0v = f32x4.mul(in0v, scalarv) + in1v = v128.load(in1) + resv = f32x4.add(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +export function mul_audio_scalar_add_audio16( + in0: usize, + scalar: f32, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 16) { + unroll(4, () => { + in0v = v128.load(in0) + in0v = f32x4.mul(in0v, scalarv) + in1v = v128.load(in1) + resv = f32x4.add(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +// @ts-ignore +@inline +export function mul_audio_scalar_add_audio1( + in0: usize, + scalar: f32, + in1: usize, + pos: u32, + out: usize, +): void { + const offset = pos << 2 + const in0v = f32.load(in0 + offset) * scalar + const in1v = f32.load(in1 + offset) + const resv = in0v + in1v + f32.store(out + offset, resv) +} + +export function sub_audio_audio( + in0: usize, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.sub(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +export function mul_audio_audio( + in0: usize, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.mul(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +export function div_audio_audio( + in0: usize, + in1: usize, + begin: u32, + end: u32, + out: usize, +): void { + let in0v: v128 + let in1v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + in1 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + in1v = v128.load(in1) + resv = f32x4.div(in0v, in1v) + v128.store(out, resv) + in0 += 16 + in1 += 16 + out += 16 + }) + } +} + +export function not_audio( + in0: usize, + begin: u32, + end: u32, + out: usize, +): void { + const minus1v: v128 = f32x4.splat(-1.0) + + let in0v: v128 + let resv: v128 + + let i: u32 = begin + + const offset = begin << 2 + in0 += offset + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + in0v = v128.load(in0) + resv = f32x4.mul(in0v, minus1v) + v128.store(out, resv) + in0 += 16 + out += 16 + }) + } +} + +export function to_audio_scalar( + scalar: f32, + begin: u32, + end: u32, + out: usize, +): void { + const scalarv: v128 = f32x4.splat(scalar) + + let i: u32 = begin + + const offset = begin << 2 + out += offset + + for (; i < end; i += 64) { + unroll(16, () => { + v128.store(out, scalarv) + out += 16 + }) + } +} diff --git a/as/assembly/dsp/graph/rms.ts b/as/assembly/dsp/graph/rms.ts new file mode 100644 index 0000000..96d225d --- /dev/null +++ b/as/assembly/dsp/graph/rms.ts @@ -0,0 +1,92 @@ +export function rms( + inp: i32, + begin: u32, + end: u32, +): f32 { + let sumv: v128 = f32x4.splat(0) + let sv: v128 = f32x4.splat(0) + const total: f32 = f32(end - begin) + + let i: u32 = begin + + const offset = begin << 2 + inp += offset + + for (; i < end; i += 64) { + unroll(16, () => { + sv = v128.load(inp) + + sumv = f32x4.add( + sumv, + f32x4.mul(sv, sv) + ) + + inp += 16 + }) + } + + const sum: f32 = + f32x4.extract_lane(sumv, 0) + + f32x4.extract_lane(sumv, 1) + + f32x4.extract_lane(sumv, 2) + + f32x4.extract_lane(sumv, 3) + + return Mathf.sqrt(sum / total) +} + +export function rmsTwo( + inp: i32, + begin_0: u32, + end_0: u32, + begin_1: u32, + end_1: u32, +): f32 { + let sumv: v128 = f32x4.splat(0) + let sv: v128 = f32x4.splat(0) + + let total: f32 = f32(end_0 - begin_0) + f32(end_1 - begin_1) + + let i: u32 = begin_0 + let offset = begin_0 << 2 + let pos: i32 = inp + pos += offset + + for (; i < end_0; i += 64) { + unroll(16, () => { + sv = v128.load(pos) + + sumv = f32x4.add( + sumv, + f32x4.mul(sv, sv) + ) + + pos += 16 + }) + } + + i = begin_1 + offset = begin_1 << 2 + pos = inp + inp += offset + + for (; i < end_0; i += 64) { + unroll(16, () => { + sv = v128.load(pos) + + sumv = f32x4.add( + sumv, + f32x4.mul(sv, sv) + ) + + pos += 16 + }) + } + + const sum: f32 = + f32x4.extract_lane(sumv, 0) + + f32x4.extract_lane(sumv, 1) + + f32x4.extract_lane(sumv, 2) + + f32x4.extract_lane(sumv, 3) + + return Mathf.sqrt(sum / total) +} diff --git a/as/assembly/dsp/index.ts b/as/assembly/dsp/index.ts new file mode 100644 index 0000000..9b4f56c --- /dev/null +++ b/as/assembly/dsp/index.ts @@ -0,0 +1,90 @@ +import { Clock } from './core/clock' +import { Core, Engine } from './core/engine' +import { Sound } from './vm/sound' + +export * from '../../../generated/assembly/dsp-factory' +export { run as dspRun } from '../../../generated/assembly/dsp-runner' +export * from '../alloc' + +export function createCore(sampleRate: u32): Core { + return new Core(sampleRate) +} + +export function createEngine(sampleRate: u32, core: Core): Engine { + return new Engine(sampleRate, core) +} + +export function getEngineClock(engine: Engine): usize { + return changetype(engine.clock) +} + +export function resetClock(clock$: usize): void { + const clock = changetype(clock$) + clock.reset() +} + +export function updateClock(clock$: usize): void { + const clock = changetype(clock$) + clock.update() +} + +export function createSound(engine: Engine): Sound { + return new Sound(engine) +} + +export function resetSound(sound: Sound): void { + sound.reset() +} + +export function clearSound(sound: Sound): void { + sound.clear() +} + +export function fillSound( + sound: Sound, + ops$: usize, + notes$: usize, + notesCount: u32, + params$: usize, + paramsCount: u32, + audio_LR$: usize, + begin: u32, + end: u32, + out$: usize +): void { + sound.fill( + ops$, + notes$, + notesCount, + params$, + paramsCount, + audio_LR$, + begin, + end, + out$ + ) +} + +export function getSoundData(sound: Sound): usize { + return changetype(sound.data) +} + +export function getSoundAudio(sound: Sound, index: i32): usize { + return changetype(sound.audios[index]) +} + +export function getSoundLiterals(sound: Sound): usize { + return changetype(sound.literals) +} + +export function setSoundLiterals(sound: Sound, literals$: usize): void { + sound.literals = changetype>(literals$) +} + +export function getSoundScalars(sound: Sound): usize { + return changetype(sound.scalars) +} + +export function getSoundLists(sound: Sound): usize { + return changetype(sound.lists) +} diff --git a/as/assembly/dsp/vm/bin-op.ts b/as/assembly/dsp/vm/bin-op.ts new file mode 100644 index 0000000..b58937e --- /dev/null +++ b/as/assembly/dsp/vm/bin-op.ts @@ -0,0 +1,35 @@ +import { add_audio_audio, add_audio_scalar, div_audio_audio, div_audio_scalar, div_scalar_audio, mul_audio_audio, mul_audio_scalar, pow_audio_audio, pow_audio_scalar, pow_scalar_audio, sub_audio_audio, sub_audio_scalar, sub_scalar_audio } from '../graph/math' +import { DspBinaryOp } from './dsp-shared' + +export type BinOpScalarScalar = (lhs: f32, rhs: f32) => f32 +export type BinOpScalarAudio = (lhs: f32, rhs: i32, begin: i32, end: i32, out: i32) => void +export type BinOpAudioScalar = (lhs: i32, rhs: f32, begin: i32, end: i32, out: i32) => void +export type BinOpAudioAudio = (lhs: i32, rhs: i32, begin: i32, end: i32, out: i32) => void + +export const BinOpScalarScalar: Map = new Map() +BinOpScalarScalar.set(DspBinaryOp.Add, (lhs: f32, rhs: f32): f32 => lhs + rhs) +BinOpScalarScalar.set(DspBinaryOp.Mul, (lhs: f32, rhs: f32): f32 => lhs * rhs) +BinOpScalarScalar.set(DspBinaryOp.Sub, (lhs: f32, rhs: f32): f32 => lhs - rhs) +BinOpScalarScalar.set(DspBinaryOp.Div, (lhs: f32, rhs: f32): f32 => lhs / rhs) +BinOpScalarScalar.set(DspBinaryOp.Pow, (lhs: f32, rhs: f32): f32 => Mathf.pow(lhs, rhs)) + +export const BinOpScalarAudio: Map = new Map() +BinOpScalarAudio.set(DspBinaryOp.Add, (lhs: f32, rhs: i32, begin: i32, end: i32, out: i32): void => add_audio_scalar(rhs, lhs, begin, end, out)) +BinOpScalarAudio.set(DspBinaryOp.Mul, (lhs: f32, rhs: i32, begin: i32, end: i32, out: i32): void => mul_audio_scalar(rhs, lhs, begin, end, out)) +BinOpScalarAudio.set(DspBinaryOp.Sub, (lhs: f32, rhs: i32, begin: i32, end: i32, out: i32): void => sub_scalar_audio(lhs, rhs, begin, end, out)) +BinOpScalarAudio.set(DspBinaryOp.Div, (lhs: f32, rhs: i32, begin: i32, end: i32, out: i32): void => div_scalar_audio(lhs, rhs, begin, end, out)) +BinOpScalarAudio.set(DspBinaryOp.Pow, (lhs: f32, rhs: i32, begin: i32, end: i32, out: i32): void => pow_scalar_audio(lhs, rhs, begin, end, out)) + +export const BinOpAudioScalar: Map = new Map() +BinOpAudioScalar.set(DspBinaryOp.Add, (lhs: i32, rhs: f32, begin: i32, end: i32, out: i32): void => add_audio_scalar(lhs, rhs, begin, end, out)) +BinOpAudioScalar.set(DspBinaryOp.Mul, (lhs: i32, rhs: f32, begin: i32, end: i32, out: i32): void => mul_audio_scalar(lhs, rhs, begin, end, out)) +BinOpAudioScalar.set(DspBinaryOp.Sub, (lhs: i32, rhs: f32, begin: i32, end: i32, out: i32): void => sub_audio_scalar(lhs, rhs, begin, end, out)) +BinOpAudioScalar.set(DspBinaryOp.Div, (lhs: i32, rhs: f32, begin: i32, end: i32, out: i32): void => div_audio_scalar(lhs, rhs, begin, end, out)) +BinOpAudioScalar.set(DspBinaryOp.Pow, (lhs: i32, rhs: f32, begin: i32, end: i32, out: i32): void => pow_audio_scalar(lhs, rhs, begin, end, out)) + +export const BinOpAudioAudio: Map = new Map() +BinOpAudioAudio.set(DspBinaryOp.Add, (lhs: i32, rhs: i32, begin: i32, end: i32, out: i32): void => add_audio_audio(lhs, rhs, begin, end, out)) +BinOpAudioAudio.set(DspBinaryOp.Mul, (lhs: i32, rhs: i32, begin: i32, end: i32, out: i32): void => mul_audio_audio(rhs, lhs, begin, end, out)) +BinOpAudioAudio.set(DspBinaryOp.Sub, (lhs: i32, rhs: i32, begin: i32, end: i32, out: i32): void => sub_audio_audio(lhs, rhs, begin, end, out)) +BinOpAudioAudio.set(DspBinaryOp.Div, (lhs: i32, rhs: i32, begin: i32, end: i32, out: i32): void => div_audio_audio(lhs, rhs, begin, end, out)) +BinOpAudioAudio.set(DspBinaryOp.Pow, (lhs: i32, rhs: i32, begin: i32, end: i32, out: i32): void => pow_audio_audio(lhs, rhs, begin, end, out)) diff --git a/as/assembly/dsp/vm/dsp-shared.ts b/as/assembly/dsp/vm/dsp-shared.ts new file mode 100644 index 0000000..a7fcadc --- /dev/null +++ b/as/assembly/dsp/vm/dsp-shared.ts @@ -0,0 +1,27 @@ +export enum SoundValueKind { + Null, + I32, + Floats, + Literal, + Scalar, + Audio, + Dynamic, +} + +export enum DspBinaryOp { + // math: commutative + Add, + Mul, + + // math: non-commutative + Sub, + Div, + Pow, +} + +export class SoundData { + constructor() { } + begin: u32 = 0 + end: u32 = 0 + pan: f32 = 0 +} diff --git a/as/assembly/dsp/vm/dsp.ts b/as/assembly/dsp/vm/dsp.ts new file mode 100644 index 0000000..3737f89 --- /dev/null +++ b/as/assembly/dsp/vm/dsp.ts @@ -0,0 +1,264 @@ +import { Factory } from '../../../../generated/assembly/dsp-factory' +import { Offsets } from '../../../../generated/assembly/dsp-offsets' +import { modWrap } from '../../util' +import { BUFFER_SIZE } from '../constants' +import { fill } from '../graph/fill' +import { BinOpAudioAudio, BinOpAudioScalar, BinOpScalarAudio, BinOpScalarScalar } from './bin-op' +import { DspBinaryOp, SoundValueKind } from './dsp-shared' +import { Sound, SoundValue } from './sound' + +export { DspBinaryOp } + +export class Dsp { + constructor() { } + + @inline + CreateGen(snd: Sound, kind_index: i32): void { + const gen = Factory[kind_index](snd.engine) + snd.gens.push(gen) + snd.offsets.push(Offsets[kind_index]) + } + @inline + CreateAudios(snd: Sound, count: i32): void { + for (let x = 0; x < count; x++) { + snd.audios.push(new StaticArray(BUFFER_SIZE)) + } + } + @inline + CreateValues(snd: Sound, count: i32): void { + for (let x = 0; x < count; x++) { + snd.values.push(new SoundValue(SoundValueKind.Null, 0)) + } + } + @inline + AudioToScalar(snd: Sound, audio$: i32, scalar$: i32): void { + const yf: f32 = f32.load( + changetype(snd.audios[audio$]) + (snd.begin << 2) + ) + snd.scalars[scalar$] = yf + } + @inline + LiteralToAudio(snd: Sound, literal$: i32, audio$: i32): void { + const xf: f32 = snd.literals[literal$] + fill( + xf, + snd.begin, + snd.end, + i32(changetype(snd.audios[audio$])) + ) + } + @inline + Pick(snd: Sound, list$: i32, list_length: i32, list_index_value$: i32, out_value$: i32): void { + const list_index = snd.values[list_index_value$] + const out_value = snd.values[out_value$] + + let vf: f32 = 0.0 + + switch (list_index.kind) { + case SoundValueKind.Literal: + vf = snd.literals[list_index.ptr] + break + case SoundValueKind.Scalar: + vf = snd.scalars[list_index.ptr] + break + case SoundValueKind.Audio: + vf = f32.load( + changetype(snd.audios[list_index.ptr]) + (snd.begin << 2) + ) + break + } + + const index = i32(modWrap(f64(vf), f64(list_length))) + const x = list$ + index + const value = snd.values[snd.lists[x]] + out_value.kind = value.kind + out_value.ptr = value.ptr + } + @inline + Pan(snd: Sound, value$: i32): void { + const value = snd.values[value$] + switch (value.kind) { + case SoundValueKind.Literal: + snd.pan = snd.literals[value.ptr] + break + case SoundValueKind.Scalar: + snd.pan = snd.scalars[value.ptr] + break + case SoundValueKind.Audio: + snd.pan = f32.load( + changetype(snd.audios[value.ptr]) + (snd.begin << 2) + ) + break + } + } + @inline + SetValue(snd: Sound, value$: i32, kind: i32, ptr: i32): void { + const value = snd.values[value$] + value.kind = kind + value.ptr = ptr + } + @inline + SetValueDynamic(snd: Sound, value$: i32, scalar$: i32, audio$: i32): void { + const value = snd.values[value$] + value.kind = SoundValueKind.Dynamic + value.scalar$ = scalar$ + value.audio$ = audio$ + } + @inline + SetProperty(snd: Sound, gen$: i32, prop$: i32, kind: i32, value$: i32): void { + const gen = snd.gens[gen$] + const offsets = snd.offsets[gen$] + const u = offsets[prop$] + const value = snd.values[value$] + const ptr = changetype(gen) + u + let xf: f32 + let x: i32 + switch (kind) { + case SoundValueKind.Scalar: + switch (value.kind) { + case SoundValueKind.I32: + i32.store(ptr, value.ptr) + break + case SoundValueKind.Literal: + xf = snd.literals[value.ptr] + f32.store(ptr, xf) + break + case SoundValueKind.Scalar: + xf = snd.scalars[value.ptr] + f32.store(ptr, xf) + break + case SoundValueKind.Audio: + xf = f32.load( + changetype(snd.audios[value.ptr]) + (snd.begin << 2) + ) + f32.store(ptr, xf) + break + default: + throw new Error('Invalid binary op.') + } + break + + case SoundValueKind.Audio: + switch (value.kind) { + case SoundValueKind.Audio: + x = i32(changetype(snd.audios[value.ptr])) + i32.store(ptr, x) + break + } + break + + case SoundValueKind.Floats: + x = snd.floats[value.ptr] + i32.store(ptr, x) + break + + default: + throw new Error('Invalid property write.') + } + } + @inline + UpdateGen(snd: Sound, gen$: i32): void { + const gen = snd.gens[gen$] + gen._update() + } + @inline + ProcessAudio(snd: Sound, gen$: i32, audio$: i32): void { + const gen = snd.gens[gen$] + gen._update() + gen._audio(snd.begin, snd.end, + changetype(snd.audios[audio$]) + ) + } + @inline + ProcessAudioStereo(snd: Sound, gen$: i32, audio_0$: i32, audio_1$: i32): void { + const gen = snd.gens[gen$] + gen._update() + gen._audio_stereo(snd.begin, snd.end, + changetype(snd.audios[audio_0$]), + changetype(snd.audios[audio_1$]), + ) + } + @inline + BinaryOp(snd: Sound, op: DspBinaryOp, lhs$: i32, rhs$: i32, out$: i32): void { + const lhs_value = snd.values[lhs$] + const rhs_value = snd.values[rhs$] + const out_value = snd.values[out$] + + let binOpScalarScalar: BinOpScalarScalar + let binOpScalarAudio: BinOpScalarAudio + let binOpAudioScalar: BinOpAudioScalar + let binOpAudioAudio: BinOpAudioAudio + + let xf: f32 + let yf: f32 + let x: i32 + let y: i32 + let z: i32 + + switch (lhs_value.kind) { + case SoundValueKind.Literal: + case SoundValueKind.Scalar: + if (lhs_value.kind === SoundValueKind.Scalar) { + xf = snd.scalars[lhs_value.ptr] + } + else if (lhs_value.kind === SoundValueKind.Literal) { + xf = snd.literals[lhs_value.ptr] + } + else { + throw 'unreachable' + } + switch (rhs_value.kind) { + case SoundValueKind.Literal: + binOpScalarScalar = BinOpScalarScalar.get(op) + yf = snd.literals[rhs_value.ptr] + out_value.kind = SoundValueKind.Scalar + out_value.ptr = out_value.scalar$ + snd.scalars[out_value.ptr] = binOpScalarScalar(xf, yf) + break + case SoundValueKind.Scalar: + binOpScalarScalar = BinOpScalarScalar.get(op) + yf = snd.scalars[rhs_value.ptr] + out_value.kind = SoundValueKind.Scalar + out_value.ptr = out_value.scalar$ + snd.scalars[out_value.ptr] = binOpScalarScalar(xf, yf) + break + case SoundValueKind.Audio: + binOpScalarAudio = BinOpScalarAudio.get(op) + out_value.kind = SoundValueKind.Audio + out_value.ptr = out_value.audio$ + y = i32(changetype(snd.audios[rhs_value.ptr])) + z = i32(changetype(snd.audios[out_value.ptr])) + binOpScalarAudio(xf, y, snd.begin, snd.end, z) + break + } + break + case SoundValueKind.Audio: + x = i32(changetype(snd.audios[lhs_value.ptr])) + z = i32(changetype(snd.audios[out_value.ptr])) + switch (rhs_value.kind) { + case SoundValueKind.Literal: + binOpAudioScalar = BinOpAudioScalar.get(op) + yf = snd.literals[rhs_value.ptr] + out_value.kind = SoundValueKind.Audio + out_value.ptr = out_value.audio$ + binOpAudioScalar(x, yf, snd.begin, snd.end, z) + break + case SoundValueKind.Scalar: + binOpAudioScalar = BinOpAudioScalar.get(op) + yf = snd.scalars[rhs_value.ptr] + out_value.kind = SoundValueKind.Audio + out_value.ptr = out_value.audio$ + binOpAudioScalar(x, yf, snd.begin, snd.end, z) + break + case SoundValueKind.Audio: + binOpAudioAudio = BinOpAudioAudio.get(op) + out_value.kind = SoundValueKind.Audio + out_value.ptr = out_value.audio$ + y = i32(changetype(snd.audios[rhs_value.ptr])) + binOpAudioAudio(x, y, snd.begin, snd.end, z) + break + } + break + } + } +} diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts new file mode 100644 index 0000000..e74dcac --- /dev/null +++ b/as/assembly/dsp/vm/sound.ts @@ -0,0 +1,270 @@ +import { run as dspRun } from '../../../../generated/assembly/dsp-runner' +import { Note, ParamValue } from '../../gfx/sketch-shared' +import { clamp } from '../../util' +import { BUFFER_SIZE, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_SCALARS } from '../constants' +import { Clock } from '../core/clock' +import { Engine } from '../core/engine' +import { Gen } from '../gen/gen' +import { SoundData, SoundValueKind } from './dsp-shared' + +export function ntof(n: f32): f32 { + return 440 * 2 ** ((n - 69) / 12) +} + +export class SoundValue { + constructor( + public kind: SoundValueKind, + public ptr: i32, + ) { } + scalar$: i32 = 0 + audio$: i32 = 0 +} + +export class Sound { + constructor(public engine: Engine) { } + + data: SoundData = new SoundData() + + get begin(): u32 { + return this.data.begin + } + + get end(): u32 { + return this.data.end + } + + set pan(v: f32) { + this.data.pan = v + } + + get pan(): f32 { + return this.data.pan + } + + gens: Gen[] = [] + offsets: usize[][] = [] + + literals: StaticArray = new StaticArray(MAX_LITERALS) + scalars: StaticArray = new StaticArray(MAX_SCALARS) + audios: Array | null> = [] + lists: StaticArray = new StaticArray(MAX_LISTS) + floats: StaticArray = new StaticArray(MAX_FLOATS) + + values: SoundValue[] = [] + + reset(): void { + this.gens.forEach(gen => { + gen._reset() + }) + this.literals.fill(0) + this.scalars.fill(0) + } + + clear(): void { + this.gens = [] + this.offsets = [] + this.values = [] + this.audios = [] + } + + @inline + updateScalars(c: Clock): void { + this.scalars[Globals.t] = f32(c.barTime) + this.scalars[Globals.rt] = f32(c.time) + } + + // TODO: this needs to be updated to handle + // sustained notes which have to keep track + // which nY scalar we're using, e.g + // n0 n1 n2 are pressed together, + // n0 n1 are released, n2 should be filled until it is released + // we need release/note off time. + @inline + updateVoices(notes$: usize, count: i32, start: f32, end: f32): void { + let y = 0 + for (let i = 0; i < count; i++) { + const note = changetype(notes$ + ((i * 4) << 2)) + if (note.time >= start && note.time < end) { + const voice = voices[y++] + this.scalars[voice[Voice.n]] = note.n + this.scalars[voice[Voice.f]] = ntof(note.n) + this.scalars[voice[Voice.t]] = note.time + this.scalars[voice[Voice.v]] = note.vel + if (y === 6) return + } + } + } + + @inline + updateParams(params$: usize, count: i32, start: f32, end: f32): void { + const params = changetype>(params$) + let y = 0 + for (let i = 0; i < count; i++) { + const ptr = unchecked(params[(i * 2)]) + const len = i32(unchecked(params[(i * 2) + 1])) + + let a: ParamValue | null = null + let b: ParamValue | null = null + for (let j = 0; j < len; j++) { + const v = changetype(ptr + ((j * 4) << 2)) + if (!a) a = v + else a = b + b = v + if (v.time >= end) break + } + + if (a) { + const param = Params.p0 + y + y++ + let amt = f32(1.0) + if (b) { + if (a.time === b.time) { + amt = b.amt + } + else { + const diff: f32 = Mathf.max(0, start - a.time) + const width: f32 = b.time - a.time + const alpha = Mathf.min(width, diff) / width + amt = clamp(-1.0, 1.0, 0.0, a.amt + (b.amt - a.amt) * alpha) + // logf2(start, amt) + } + } + this.scalars[param] = amt + // logf(6667) + } + // if (v.time >= start && v.time < end) { + // const param = params[y++] + // this.scalars[param[Param.t]] = v.time + // this.scalars[param[Param.l]] = v.length + // this.scalars[param[Param.s]] = v.slope + // this.scalars[param[Param.a]] = v.amt + // if (y === 6) return + // } + } + } + + fill( + ops$: usize, + notes$: usize, + notesCount: u32, + params$: usize, + paramsCount: u32, + audio_LR$: i32, + begin: u32, + end: u32, + out$: usize + ): void { + const CHUNK_SIZE = 64 + let chunkCount = 0 + + const c = this.engine.clock + this.scalars[Globals.sr] = f32(c.sampleRate) + this.scalars[Globals.co] = f32(c.coeff) + + let timeStart: f32 + let timeEnd: f32 + + this.updateScalars(c) + timeStart = f32(c.barTime / NOTE_SCALE_X) + timeEnd = f32(c.barTime + c.barTimeStep * f64(CHUNK_SIZE)) + this.updateVoices(notes$, notesCount, timeStart, timeEnd) + this.updateParams(params$, paramsCount, timeStart, timeEnd) + + let i = begin + const data = this.data + data.begin = i + data.end = i + dspRun(this, ops$) + + for (let x = i; x < end; x += BUFFER_SIZE) { + const chunkEnd = x + BUFFER_SIZE > end ? end - x : BUFFER_SIZE + + for (let i: u32 = 0; i < chunkEnd; i += CHUNK_SIZE) { + this.updateScalars(c) + timeStart = f32(c.barTime * NOTE_SCALE_X) + timeEnd = f32((c.barTime + c.barTimeStep * f64(CHUNK_SIZE)) * NOTE_SCALE_X) + this.updateVoices(notes$, notesCount, timeStart, timeEnd) + this.updateParams(params$, paramsCount, timeStart, timeEnd) + + data.begin = i + data.end = i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE + dspRun(this, ops$) + + chunkCount++ + + c.time = f64(chunkCount * CHUNK_SIZE) * c.timeStep + c.barTime = f64(chunkCount * CHUNK_SIZE) * c.barTimeStep + } + + const audio = this.audios[audio_LR$] + memory.copy( + out$ + (x << 2), + changetype(audio), + chunkEnd << 2 + ) + } + } +} + +const NOTE_SCALE_X: f64 = 16 + +const enum Globals { + sr, + t, + rt, + co, +} + +const MAX_VOICES = 6 + +const enum Voices { + n0 = 4, + n1 = 8, + n2 = 12, + n3 = 16, + n4 = 20, + n5 = 24, +} + +const enum Voice { + n, + f, + t, + v, +} + +const voices: i32[][] = [ + [Voices.n0, Voices.n0 + 1, Voices.n0 + 2, Voices.n0 + 3], + [Voices.n1, Voices.n1 + 1, Voices.n1 + 2, Voices.n1 + 3], + [Voices.n2, Voices.n2 + 1, Voices.n2 + 2, Voices.n2 + 3], + [Voices.n3, Voices.n3 + 1, Voices.n3 + 2, Voices.n3 + 3], + [Voices.n4, Voices.n4 + 1, Voices.n4 + 2, Voices.n4 + 3], + [Voices.n5, Voices.n5 + 1, Voices.n5 + 2, Voices.n5 + 3], +] + +const MAX_PARAMS = 6 + +const enum Params { + p0 = 28, + p1, + p2, + p3, + p4, + p5, +} + +const enum Param { + t, + l, + s, + a, +} + +const params: i32[][] = [ + [Params.p0, Params.p0 + 1, Params.p0 + 2, Params.p0 + 3], + [Params.p1, Params.p1 + 1, Params.p1 + 2, Params.p1 + 3], + [Params.p2, Params.p2 + 1, Params.p2 + 2, Params.p2 + 3], + [Params.p3, Params.p3 + 1, Params.p3 + 2, Params.p3 + 3], + [Params.p4, Params.p4 + 1, Params.p4 + 2, Params.p4 + 3], + [Params.p5, Params.p5 + 1, Params.p5 + 2, Params.p5 + 3], +] diff --git a/as/assembly/dsp/vm/template.ts b/as/assembly/dsp/vm/template.ts new file mode 100644 index 0000000..ffb118c --- /dev/null +++ b/as/assembly/dsp/vm/template.ts @@ -0,0 +1,18 @@ +class Vm { + // + run(ops: StaticArray): void { + // + let i: i32 = 0 + let op: i32 + while (op = ops[i++]) { + switch (op) { + // + } // end switch + } // end while + } +} + +export function createVm(): Vm { + const vm: Vm = new Vm() + return vm +} diff --git a/as/assembly/env.ts b/as/assembly/env.ts deleted file mode 100644 index 2f829d9..0000000 --- a/as/assembly/env.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-ignore -@external('env', 'log') -export declare function logi(x: i32): void -// @ts-ignore -@external('env', 'log') -export declare function logd(x: f64): void -// @ts-ignore -@external('env', 'log') -export declare function logf(x: f32): void -// @ts-ignore -@external('env', 'log') -export declare function logf2(x: f32, y: f32): void -// @ts-ignore -@external('env', 'log') -export declare function logf3(x: f32, y: f32, z: f32): void -// @ts-ignore -@external('env', 'log') -export declare function logf4(x: f32, y: f32, z: f32, w: f32): void -// @ts-ignore -@external('env', 'log') -export declare function logf6(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32): void diff --git a/as/assembly/gfx/draw.ts b/as/assembly/gfx/draw.ts index e0be920..86a7cfa 100644 --- a/as/assembly/gfx/draw.ts +++ b/as/assembly/gfx/draw.ts @@ -1,6 +1,5 @@ -import { logf, logf2, logf3, logf4, logf6, logi } from '../env' import { Sketch } from './sketch' -import { Box, Line, Matrix, Notes, Shape, ShapeOpts, WAVE_MIPMAPS, Wave, Note, Params, ParamValue } from './sketch-shared' +import { Box, Line, Matrix, Note, Notes, Params, ParamValue, Shape, ShapeOpts, Wave } from './sketch-shared' import { lineIntersectsRect } from './util' const MAX_ZOOM: f32 = 0.5 diff --git a/as/assembly/pkg/math.ts b/as/assembly/pkg/math.ts index 55b9648..50be7d8 100644 --- a/as/assembly/pkg/math.ts +++ b/as/assembly/pkg/math.ts @@ -1,8 +1,3 @@ -import { logf } from '../env' - export function multiply(a: f32, b: f32): f32 { - // uncomment to test devtools console source link - // logf(a) - return a * b } diff --git a/as/assembly/util.ts b/as/assembly/util.ts new file mode 100644 index 0000000..1a58004 --- /dev/null +++ b/as/assembly/util.ts @@ -0,0 +1,243 @@ +export type Floats = StaticArray + +export function clamp255(x: f32): i32 { + if (x > 255) x = 255 + else if (x < 0) x = 0 + return i32(x) +} + +export function rgbToInt(r: f32, g: f32, b: f32): i32 { + return (clamp255(r * 255) << 16) | (clamp255(g * 255) << 8) | clamp255(b * 255) +} + +// @ts-ignore +@inline +export function rateToPhaseStep(rate: u32): u32 { + return 0xFFFFFFFF / rate +} + +// @ts-ignore +@inline +export function phaseToNormal(phase: u32): f64 { + return phase / 0xFFFFFFFF +} + +// // @ts-ignore +// export function rateToPhaseStep(rate: f64): u32 { +// let stepFactor: f64 = 0xFFFFFFFF / (2 * Math.PI); +// return ((1.0 / rate) * stepFactor); +// } + +// @ts-ignore +@inline +export function radiansToPhase(radians: f32): u32 { + return ((radians / (2 * Mathf.PI)) * 0xFFFFFFFF) +} + +// @ts-ignore +@inline +export function phaseToRadians(phase: u32): f64 { + const radiansFactor: f64 = 2 * Mathf.PI / 0xFFFFFFFF + return phase * radiansFactor +} + +// @ts-ignore +@inline +export function degreesToPhase(degrees: f32): u32 { + return ((degrees / 360.0) * 0xFFFFFFFF) +} + +// @ts-ignore +@inline +export function ftoint(x: f32): i32 { + const bits: i32 = reinterpret(x) + const expo: i32 = (bits >> 23) & 0xFF + const mant: i32 = bits & 0x7FFFFF + const bias: i32 = 127 + return expo === 0 + ? mant >> (23 - (bias - 1)) + : (mant | 0x800000) >> (23 - (expo - bias)) +} + +// @ts-ignore +@inline +function phaseToWavetableIndex(phase: u32, wavetableSize: u32): u32 { + const indexFactor = wavetableSize / 0xFFFFFFFF + return (phase * indexFactor) +} + +// @ts-ignore +@inline +export function phaseFrac(phase: u32): f32 { + const u: u32 = 0x3F800000 | ((0x007FFF80 & phase) << 7) + return reinterpret(u) - 1.0 +} + +// @ts-ignore +@inline +export function tofloat(u: u32): f32 { + return (0x3F800000 | (u >> 9)) - 1.0 +} + +// export function copyInto(src: T, dst: T): void { +// const srcPtr = changetype(src) +// const dstPtr = changetype(dst) +// const size: usize = offsetof() +// for (let offset: usize = 0; offset < size; offset += sizeof()) { +// const value = load(srcPtr + offset) +// store(dstPtr + offset, value) +// } +// } + +const f32s: StaticArray[] = [] + +export function allocF32(blockSize: u32): StaticArray { + const block = new StaticArray(blockSize) + f32s.push(block) + return block +} + +const mems: StaticArray[] = [] + +export function cloneI32(src: usize, size: usize): StaticArray { + const mem = new StaticArray(i32(size)) + memory.copy(changetype(mem), src, size) + mems.push(mem) + return mem +} + +export function getObjectSize(): usize { + return offsetof() +} + +export function nextPowerOfTwo(v: u32): u32 { + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v++ + return v +} + +export function clamp64(min: f64, max: f64, def: f64, value: f64): f64 { + if (value - value !== 0) return def + if (value < min) value = min + if (value > max) value = max + return value +} +export function clamp(min: f32, max: f32, def: f32, value: f32): f32 { + if (value - value !== 0) return def + if (value < min) value = min + if (value > max) value = max + return value +} + +export function clampMax(max: f32, s: f32): f32 { + return s > max ? max : s +} + +export function paramClamp(param: f32[], value: f32): f32 { + return clamp(param[0], param[1], param[2], value) +} + +export function cubic(floats: StaticArray, index: f32, mask: u32): f32 { + index += mask + const i: i32 = u32(index) + const fr: f32 = index - f32(i) + + const p: i32 = i32(changetype(floats)) //+ (i << 2) + const xm: f32 = f32.load(p + (((i - 1) & mask) << 2)) + const x0: f32 = f32.load(p + (((i) & mask) << 2)) //floats[(i) & mask] + const x1: f32 = f32.load(p + (((i + 1) & mask) << 2)) //floats[(i + 1) & mask] + const x2: f32 = f32.load(p + (((i + 2) & mask) << 2)) //floats[(i + 2) & mask] + + // const a: f32 = (3.0 * (x0 - x1) - xm + x2) * .5 + // const b: f32 = 2.0 * x1 + xm - (5.0 * x0 + x2) * .5 + // const c: f32 = (x1 - xm) * .5 + + // this has one operation less (a +), are they equal though? + // const c0: f32 = x0 + // const c1: f32 = 0.5 * (x1 - xm) + // const c2: f32 = xm - 2.5 * x0 + 2.0 * x1 - 0.5 * x2 + // const c3: f32 = 0.5 * (x2 - xm) + 1.5 * (x0 - x1) + // return ((c3 * fr + c2) * fr + c1) * fr + c0 + + const a: f32 = (3.0 * (x0 - x1) - xm + x2) * .5 + const b: f32 = 2.0 * x1 + xm - (5.0 * x0 + x2) * .5 + const c: f32 = (x1 - xm) * .5 + + return (((a * fr) + b) + * fr + c) + * fr + x0 +} + +export function cubicFrac(floats: StaticArray, index: u32, fr: f32, mask: u32): f32 { + const i: i32 = u32(index) + + const xm: f32 = floats[(i - 1) & mask] + const x0: f32 = floats[(i) & mask] + const x1: f32 = floats[(i + 1) & mask] + const x2: f32 = floats[(i + 2) & mask] + + const a: f32 = (3.0 * (x0 - x1) - xm + x2) * .5 + const b: f32 = 2.0 * x1 + xm - (5.0 * x0 + x2) * .5 + const c: f32 = (x1 - xm) * .5 + + return (((a * fr) + b) + * fr + c) + * fr + x0 +} + +export function cubicMod(floats: StaticArray, index: f64, length: u32): f32 { + const i: i32 = i32(index) + i32(length) + index += f64(length) + const fr: f64 = index - f64(i) + + // TODO: we could possibly improve this perf by moding the index early + // and branching, but lets keep it simple for now. + const xm = f64(unchecked(floats[(i - 1) % length])) + const x0 = f64(unchecked(floats[(i) % length])) + const x1 = f64(unchecked(floats[(i + 1) % length])) + const x2 = f64(unchecked(floats[(i + 2) % length])) + + const a: f64 = (3.0 * (x0 - x1) - xm + x2) * .5 + const b: f64 = 2.0 * x1 + xm - (5.0 * x0 + x2) * .5 + const c: f64 = (x1 - xm) * .5 + + return f32((((a * fr) + b) + * fr + c) + * fr + x0) +} + +// export function cubicMod(floats: StaticArray, index: f32, frames: u32): f32 { +// const i: i32 = u32(index) + frames +// index += frames +// const fr: f32 = index - f32(i) +// const xm: f32 = floats[(i - 1) % frames] +// const x0: f32 = floats[(i) % frames] +// const x1: f32 = floats[(i + 1) % frames] +// const x2: f32 = floats[(i + 2) % frames] + +// const a: f32 = (3.0 * (x0 - x1) - xm + x2) * .5 +// const b: f32 = 2.0 * x1 + xm - (5.0 * x0 + x2) * .5 +// const c: f32 = (x1 - xm) * .5 + +// return (((a * fr) + b) +// * fr + c) +// * fr + x0 +// } + +let seed: u32 = 0 +export function rnd(amt: f64 = 1): f64 { + seed += 0x6D2B79F5 + let t: u32 = seed + t = (t ^ t >>> 15) * (t | 1) + t ^= t + (t ^ t >>> 7) * (t | 61) + return (f64((t ^ t >>> 14) >>> 0) / 4294967296.0) * amt +} + +export function modWrap(x: f64, N: f64): f64 { + return (x % N + N) % N +} diff --git a/as/tsconfig.json b/as/tsconfig.json index 11d5ca3..b773934 100644 --- a/as/tsconfig.json +++ b/as/tsconfig.json @@ -4,6 +4,7 @@ "./assembly/**/*.ts" ], "compilerOptions": { + "baseUrl": "..", "experimentalDecorators": true } } diff --git a/asconfig-dsp.json b/asconfig-dsp.json new file mode 100644 index 0000000..cd49faa --- /dev/null +++ b/asconfig-dsp.json @@ -0,0 +1,35 @@ +{ + "targets": { + "debug": { + "outFile": "./as/build/dsp.wasm", + "textFile": "./as/build/dsp.wat", + "sourceMap": true, + "debug": true, + "noAssert": true + }, + "release": { + "outFile": "./as/build/dsp.wasm", + "textFile": "./as/build/dsp.wat", + "sourceMap": true, + "debug": false, + "optimizeLevel": 0, + "shrinkLevel": 0, + "converge": false, + "noAssert": true + } + }, + "options": { + "enable": [ + "simd", + "relaxed-simd", + "threads" + ], + "sharedMemory": true, + "importMemory": false, + "initialMemory": 5000, + "maximumMemory": 5000, + "bindings": "raw", + "runtime": "incremental", + "exportRuntime": true + } +} diff --git a/generated/assembly/dsp-factory.ts b/generated/assembly/dsp-factory.ts new file mode 100644 index 0000000..83f9107 --- /dev/null +++ b/generated/assembly/dsp-factory.ts @@ -0,0 +1,108 @@ +import { Engine } from '../../as/assembly/dsp/core/engine' +import { Adsr } from '../../as/assembly/dsp/gen/adsr' +function createAdsr(engine: Engine): Adsr { return new Adsr(engine) } +import { Aosc } from '../../as/assembly/dsp/gen/aosc' +function createAosc(engine: Engine): Aosc { return new Aosc(engine) } +import { Atan } from '../../as/assembly/dsp/gen/atan' +function createAtan(engine: Engine): Atan { return new Atan(engine) } +import { Bap } from '../../as/assembly/dsp/gen/bap' +function createBap(engine: Engine): Bap { return new Bap(engine) } +import { Bbp } from '../../as/assembly/dsp/gen/bbp' +function createBbp(engine: Engine): Bbp { return new Bbp(engine) } +import { Bhp } from '../../as/assembly/dsp/gen/bhp' +function createBhp(engine: Engine): Bhp { return new Bhp(engine) } +import { Bhs } from '../../as/assembly/dsp/gen/bhs' +function createBhs(engine: Engine): Bhs { return new Bhs(engine) } +import { Biquad } from '../../as/assembly/dsp/gen/biquad' +function createBiquad(engine: Engine): Biquad { return new Biquad(engine) } +import { Blp } from '../../as/assembly/dsp/gen/blp' +function createBlp(engine: Engine): Blp { return new Blp(engine) } +import { Bls } from '../../as/assembly/dsp/gen/bls' +function createBls(engine: Engine): Bls { return new Bls(engine) } +import { Bno } from '../../as/assembly/dsp/gen/bno' +function createBno(engine: Engine): Bno { return new Bno(engine) } +import { Bpk } from '../../as/assembly/dsp/gen/bpk' +function createBpk(engine: Engine): Bpk { return new Bpk(engine) } +import { Clamp } from '../../as/assembly/dsp/gen/clamp' +function createClamp(engine: Engine): Clamp { return new Clamp(engine) } +import { Clip } from '../../as/assembly/dsp/gen/clip' +function createClip(engine: Engine): Clip { return new Clip(engine) } +import { Comp } from '../../as/assembly/dsp/gen/comp' +function createComp(engine: Engine): Comp { return new Comp(engine) } +import { Daverb } from '../../as/assembly/dsp/gen/daverb' +function createDaverb(engine: Engine): Daverb { return new Daverb(engine) } +import { Dcc } from '../../as/assembly/dsp/gen/dcc' +function createDcc(engine: Engine): Dcc { return new Dcc(engine) } +import { Dclip } from '../../as/assembly/dsp/gen/dclip' +function createDclip(engine: Engine): Dclip { return new Dclip(engine) } +import { Dclipexp } from '../../as/assembly/dsp/gen/dclipexp' +function createDclipexp(engine: Engine): Dclipexp { return new Dclipexp(engine) } +import { Dcliplin } from '../../as/assembly/dsp/gen/dcliplin' +function createDcliplin(engine: Engine): Dcliplin { return new Dcliplin(engine) } +import { Delay } from '../../as/assembly/dsp/gen/delay' +function createDelay(engine: Engine): Delay { return new Delay(engine) } +import { Diode } from '../../as/assembly/dsp/gen/diode' +function createDiode(engine: Engine): Diode { return new Diode(engine) } +import { Exp } from '../../as/assembly/dsp/gen/exp' +function createExp(engine: Engine): Exp { return new Exp(engine) } +import { Freesound } from '../../as/assembly/dsp/gen/freesound' +function createFreesound(engine: Engine): Freesound { return new Freesound(engine) } +import { Gen } from '../../as/assembly/dsp/gen/gen' +import { Gendy } from '../../as/assembly/dsp/gen/gendy' +function createGendy(engine: Engine): Gendy { return new Gendy(engine) } +import { Grain } from '../../as/assembly/dsp/gen/grain' +function createGrain(engine: Engine): Grain { return new Grain(engine) } +import { Inc } from '../../as/assembly/dsp/gen/inc' +function createInc(engine: Engine): Inc { return new Inc(engine) } +import { Lp } from '../../as/assembly/dsp/gen/lp' +function createLp(engine: Engine): Lp { return new Lp(engine) } +import { Mhp } from '../../as/assembly/dsp/gen/mhp' +function createMhp(engine: Engine): Mhp { return new Mhp(engine) } +import { Mlp } from '../../as/assembly/dsp/gen/mlp' +function createMlp(engine: Engine): Mlp { return new Mlp(engine) } +import { Moog } from '../../as/assembly/dsp/gen/moog' +function createMoog(engine: Engine): Moog { return new Moog(engine) } +import { Noi } from '../../as/assembly/dsp/gen/noi' +function createNoi(engine: Engine): Noi { return new Noi(engine) } +import { Nrate } from '../../as/assembly/dsp/gen/nrate' +function createNrate(engine: Engine): Nrate { return new Nrate(engine) } +import { Osc } from '../../as/assembly/dsp/gen/osc' +import { Ramp } from '../../as/assembly/dsp/gen/ramp' +function createRamp(engine: Engine): Ramp { return new Ramp(engine) } +import { Rate } from '../../as/assembly/dsp/gen/rate' +function createRate(engine: Engine): Rate { return new Rate(engine) } +import { Sap } from '../../as/assembly/dsp/gen/sap' +function createSap(engine: Engine): Sap { return new Sap(engine) } +import { Saw } from '../../as/assembly/dsp/gen/saw' +function createSaw(engine: Engine): Saw { return new Saw(engine) } +import { Say } from '../../as/assembly/dsp/gen/say' +function createSay(engine: Engine): Say { return new Say(engine) } +import { Sbp } from '../../as/assembly/dsp/gen/sbp' +function createSbp(engine: Engine): Sbp { return new Sbp(engine) } +import { Shp } from '../../as/assembly/dsp/gen/shp' +function createShp(engine: Engine): Shp { return new Shp(engine) } +import { Sin } from '../../as/assembly/dsp/gen/sin' +function createSin(engine: Engine): Sin { return new Sin(engine) } +import { Slp } from '../../as/assembly/dsp/gen/slp' +function createSlp(engine: Engine): Slp { return new Slp(engine) } +import { Smp } from '../../as/assembly/dsp/gen/smp' +function createSmp(engine: Engine): Smp { return new Smp(engine) } +import { Sno } from '../../as/assembly/dsp/gen/sno' +function createSno(engine: Engine): Sno { return new Sno(engine) } +import { Spk } from '../../as/assembly/dsp/gen/spk' +function createSpk(engine: Engine): Spk { return new Spk(engine) } +import { Sqr } from '../../as/assembly/dsp/gen/sqr' +function createSqr(engine: Engine): Sqr { return new Sqr(engine) } +import { Svf } from '../../as/assembly/dsp/gen/svf' +function createSvf(engine: Engine): Svf { return new Svf(engine) } +import { Tanh } from '../../as/assembly/dsp/gen/tanh' +function createTanh(engine: Engine): Tanh { return new Tanh(engine) } +import { Tanha } from '../../as/assembly/dsp/gen/tanha' +function createTanha(engine: Engine): Tanha { return new Tanha(engine) } +import { Tap } from '../../as/assembly/dsp/gen/tap' +function createTap(engine: Engine): Tap { return new Tap(engine) } +import { Tri } from '../../as/assembly/dsp/gen/tri' +function createTri(engine: Engine): Tri { return new Tri(engine) } +import { Zero } from '../../as/assembly/dsp/gen/zero' +function createZero(engine: Engine): Zero { return new Zero(engine) } +export const Factory: ((engine: Engine) => Gen)[] = [createAdsr,createAosc,createAtan,createBap,createBbp,createBhp,createBhs,createBiquad,createBlp,createBls,createBno,createBpk,createClamp,createClip,createComp,createDaverb,createDcc,createDclip,createDclipexp,createDcliplin,createDelay,createDiode,createExp,createFreesound,createZero,createGendy,createGrain,createInc,createLp,createMhp,createMlp,createMoog,createNoi,createNrate,createZero,createRamp,createRate,createSap,createSaw,createSay,createSbp,createShp,createSin,createSlp,createSmp,createSno,createSpk,createSqr,createSvf,createTanh,createTanha,createTap,createTri,createZero] \ No newline at end of file diff --git a/generated/assembly/dsp-offsets.ts b/generated/assembly/dsp-offsets.ts new file mode 100644 index 0000000..21a294d --- /dev/null +++ b/generated/assembly/dsp-offsets.ts @@ -0,0 +1,110 @@ +import { Adsr } from '../../as/assembly/dsp/gen/adsr' +import { Aosc } from '../../as/assembly/dsp/gen/aosc' +import { Atan } from '../../as/assembly/dsp/gen/atan' +import { Bap } from '../../as/assembly/dsp/gen/bap' +import { Bbp } from '../../as/assembly/dsp/gen/bbp' +import { Bhp } from '../../as/assembly/dsp/gen/bhp' +import { Bhs } from '../../as/assembly/dsp/gen/bhs' +import { Biquad } from '../../as/assembly/dsp/gen/biquad' +import { Blp } from '../../as/assembly/dsp/gen/blp' +import { Bls } from '../../as/assembly/dsp/gen/bls' +import { Bno } from '../../as/assembly/dsp/gen/bno' +import { Bpk } from '../../as/assembly/dsp/gen/bpk' +import { Clamp } from '../../as/assembly/dsp/gen/clamp' +import { Clip } from '../../as/assembly/dsp/gen/clip' +import { Comp } from '../../as/assembly/dsp/gen/comp' +import { Daverb } from '../../as/assembly/dsp/gen/daverb' +import { Dcc } from '../../as/assembly/dsp/gen/dcc' +import { Dclip } from '../../as/assembly/dsp/gen/dclip' +import { Dclipexp } from '../../as/assembly/dsp/gen/dclipexp' +import { Dcliplin } from '../../as/assembly/dsp/gen/dcliplin' +import { Delay } from '../../as/assembly/dsp/gen/delay' +import { Diode } from '../../as/assembly/dsp/gen/diode' +import { Exp } from '../../as/assembly/dsp/gen/exp' +import { Freesound } from '../../as/assembly/dsp/gen/freesound' +import { Gen } from '../../as/assembly/dsp/gen/gen' +import { Gendy } from '../../as/assembly/dsp/gen/gendy' +import { Grain } from '../../as/assembly/dsp/gen/grain' +import { Inc } from '../../as/assembly/dsp/gen/inc' +import { Lp } from '../../as/assembly/dsp/gen/lp' +import { Mhp } from '../../as/assembly/dsp/gen/mhp' +import { Mlp } from '../../as/assembly/dsp/gen/mlp' +import { Moog } from '../../as/assembly/dsp/gen/moog' +import { Noi } from '../../as/assembly/dsp/gen/noi' +import { Nrate } from '../../as/assembly/dsp/gen/nrate' +import { Osc } from '../../as/assembly/dsp/gen/osc' +import { Ramp } from '../../as/assembly/dsp/gen/ramp' +import { Rate } from '../../as/assembly/dsp/gen/rate' +import { Sap } from '../../as/assembly/dsp/gen/sap' +import { Saw } from '../../as/assembly/dsp/gen/saw' +import { Say } from '../../as/assembly/dsp/gen/say' +import { Sbp } from '../../as/assembly/dsp/gen/sbp' +import { Shp } from '../../as/assembly/dsp/gen/shp' +import { Sin } from '../../as/assembly/dsp/gen/sin' +import { Slp } from '../../as/assembly/dsp/gen/slp' +import { Smp } from '../../as/assembly/dsp/gen/smp' +import { Sno } from '../../as/assembly/dsp/gen/sno' +import { Spk } from '../../as/assembly/dsp/gen/spk' +import { Sqr } from '../../as/assembly/dsp/gen/sqr' +import { Svf } from '../../as/assembly/dsp/gen/svf' +import { Tanh } from '../../as/assembly/dsp/gen/tanh' +import { Tanha } from '../../as/assembly/dsp/gen/tanha' +import { Tap } from '../../as/assembly/dsp/gen/tap' +import { Tri } from '../../as/assembly/dsp/gen/tri' +import { Zero } from '../../as/assembly/dsp/gen/zero' +export const Offsets: usize[][] = [ + [offsetof('gain'),offsetof('attack'),offsetof('decay'),offsetof('sustain'),offsetof('release'),offsetof('on'),offsetof('off')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q'),offsetof('amt')], + [offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q'),offsetof('amt')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q'),offsetof('amt')], + [offsetof('gain'),offsetof('min'),offsetof('max'),offsetof('in')], + [offsetof('gain'),offsetof('threshold'),offsetof('in')], + [offsetof('gain'),offsetof('threshold'),offsetof('ratio'),offsetof('attack'),offsetof('release'),offsetof('in'),offsetof('sidechain')], + [offsetof('gain'),offsetof('in'),offsetof('pd'),offsetof('bw'),offsetof('fi'),offsetof('si'),offsetof('dc'),offsetof('ft'),offsetof('st'),offsetof('dp'),offsetof('ex'),offsetof('ed')], + [offsetof('gain'),offsetof('ceil'),offsetof('in'),offsetof('sample')], + [offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('factor'),offsetof('in')], + [offsetof('gain'),offsetof('threshold'),offsetof('factor'),offsetof('in')], + [offsetof('gain'),offsetof('ms'),offsetof('fb'),offsetof('in')], + [offsetof('gain'),offsetof('cut'),offsetof('hpf'),offsetof('sat'),offsetof('q'),offsetof('in')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('offset'),offsetof('length'),offsetof('trig'),offsetof('id')], + [offsetof('gain')], + [offsetof('gain'),offsetof('step')], + [offsetof('gain'),offsetof('amt')], + [offsetof('gain'),offsetof('amt'),offsetof('trig')], + [offsetof('gain'),offsetof('cut'),offsetof('in')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('normal')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('samples')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('offset'),offsetof('length'),offsetof('trig'),offsetof('text')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('offset'),offsetof('length'),offsetof('trig')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('ms'),offsetof('in')], + [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], + [offsetof('gain')] +] \ No newline at end of file diff --git a/generated/assembly/dsp-op.ts b/generated/assembly/dsp-op.ts new file mode 100644 index 0000000..5cd20b9 --- /dev/null +++ b/generated/assembly/dsp-op.ts @@ -0,0 +1,20 @@ +// TypeScript + AssemblyScript Ops Enum +// auto-generated from scripts +export enum Op { + End, + Begin, + CreateGen, + CreateAudios, + CreateValues, + AudioToScalar, + LiteralToAudio, + Pick, + Pan, + SetValue, + SetValueDynamic, + SetProperty, + UpdateGen, + ProcessAudio, + ProcessAudioStereo, + BinaryOp +} diff --git a/generated/assembly/dsp-runner.ts b/generated/assembly/dsp-runner.ts new file mode 100644 index 0000000..13761fd --- /dev/null +++ b/generated/assembly/dsp-runner.ts @@ -0,0 +1,136 @@ +// AssemblyScript VM Runner +// auto-generated from scripts +import { Op } from './dsp-op' +import { Dsp, DspBinaryOp } from '../../as/assembly/dsp/vm/dsp' +import { Sound } from '../../as/assembly/dsp/vm/sound' + +const dsp = new Dsp() + +export function run(ctx: Sound, ops$: usize): void { + const ops = changetype>(ops$) + + let i: i32 = 0 + let op: i32 = 0 + + while (unchecked(op = ops[i++])) { + switch (op) { + + case Op.CreateGen: + dsp.CreateGen( + ctx, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.CreateAudios: + dsp.CreateAudios( + ctx, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.CreateValues: + dsp.CreateValues( + ctx, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.AudioToScalar: + dsp.AudioToScalar( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.LiteralToAudio: + dsp.LiteralToAudio( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.Pick: + dsp.Pick( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.Pan: + dsp.Pan( + ctx, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.SetValue: + dsp.SetValue( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.SetValueDynamic: + dsp.SetValueDynamic( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.SetProperty: + dsp.SetProperty( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.UpdateGen: + dsp.UpdateGen( + ctx, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.ProcessAudio: + dsp.ProcessAudio( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.ProcessAudioStereo: + dsp.ProcessAudioStereo( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.BinaryOp: + dsp.BinaryOp( + ctx, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + } // end switch + } // end while +} diff --git a/generated/assembly/tsconfig.json b/generated/assembly/tsconfig.json new file mode 100644 index 0000000..891cc23 --- /dev/null +++ b/generated/assembly/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ] +} diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts new file mode 100644 index 0000000..7c31b00 --- /dev/null +++ b/generated/typescript/dsp-gens.ts @@ -0,0 +1,396 @@ +// +// auto-generated Wed Oct 16 2024 07:59:10 GMT+0300 (Eastern European Summer Time) + +import { Value } from '../../src/dsp/value.ts' + +export const dspGens = { + adsr: { + inherits: 'gen', + props: [ 'attack', 'decay', 'sustain', 'release', 'on', 'off' ], + hasAudioOut: true + }, + aosc: { inherits: 'osc', props: [], hasAudioOut: true }, + atan: { inherits: 'gen', props: [ 'gain', 'in' ], hasAudioOut: true }, + bap: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, + bbp: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, + bhp: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, + bhs: { + inherits: 'biquad', + props: [ 'cut', 'q', 'amt' ], + hasAudioOut: true + }, + biquad: { inherits: 'gen', props: [ 'in' ], hasAudioOut: true }, + blp: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, + bls: { + inherits: 'biquad', + props: [ 'cut', 'q', 'amt' ], + hasAudioOut: true + }, + bno: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, + bpk: { + inherits: 'biquad', + props: [ 'cut', 'q', 'amt' ], + hasAudioOut: true + }, + clamp: { inherits: 'gen', props: [ 'min', 'max', 'in' ], hasAudioOut: true }, + clip: { inherits: 'gen', props: [ 'threshold', 'in' ], hasAudioOut: true }, + comp: { + inherits: 'gen', + props: [ 'threshold', 'ratio', 'attack', 'release', 'in', 'sidechain' ], + hasAudioOut: true + }, + daverb: { + inherits: 'gen', + props: [ + 'in', 'pd', 'bw', + 'fi', 'si', 'dc', + 'ft', 'st', 'dp', + 'ex', 'ed' + ], + hasAudioOut: true, + hasStereoOut: true + }, + dcc: { + inherits: 'gen', + props: [ 'ceil', 'in', 'sample' ], + hasAudioOut: true + }, + dclip: { inherits: 'gen', props: [ 'in' ], hasAudioOut: true }, + dclipexp: { inherits: 'gen', props: [ 'factor', 'in' ], hasAudioOut: true }, + dcliplin: { + inherits: 'gen', + props: [ 'threshold', 'factor', 'in' ], + hasAudioOut: true + }, + delay: { inherits: 'gen', props: [ 'ms', 'fb', 'in' ], hasAudioOut: true }, + diode: { + inherits: 'gen', + props: [ 'cut', 'hpf', 'sat', 'q', 'in' ], + hasAudioOut: true + }, + exp: { inherits: 'osc', props: [], hasAudioOut: true }, + freesound: { inherits: 'smp', props: [ 'id' ], hasAudioOut: true }, + gen: { props: [ 'gain' ], hasAudioOut: true, hasStereoOut: true }, + gendy: { inherits: 'gen', props: [ 'step' ], hasAudioOut: true }, + grain: { inherits: 'gen', props: [ 'amt' ], hasAudioOut: true }, + inc: { inherits: 'gen', props: [ 'amt', 'trig' ], hasAudioOut: true }, + lp: { inherits: 'gen', props: [ 'cut', 'in' ], hasAudioOut: true }, + mhp: { inherits: 'moog', props: [ 'cut', 'q' ], hasAudioOut: true }, + mlp: { inherits: 'moog', props: [ 'cut', 'q' ], hasAudioOut: true }, + moog: { inherits: 'gen', props: [ 'in' ] }, + noi: { inherits: 'osc', props: [], hasAudioOut: true }, + nrate: { inherits: 'gen', props: [ 'normal' ] }, + osc: { + inherits: 'gen', + props: [ 'hz', 'trig', 'offset' ], + hasAudioOut: true + }, + ramp: { inherits: 'aosc', props: [], hasAudioOut: true }, + rate: { inherits: 'gen', props: [ 'samples' ] }, + sap: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, + saw: { inherits: 'aosc', props: [], hasAudioOut: true }, + say: { inherits: 'smp', props: [ 'text' ], hasAudioOut: true }, + sbp: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, + shp: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, + sin: { inherits: 'osc', props: [], hasAudioOut: true }, + slp: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, + smp: { + inherits: 'gen', + props: [ 'offset', 'length', 'trig' ], + hasAudioOut: true + }, + sno: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, + spk: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, + sqr: { inherits: 'aosc', props: [], hasAudioOut: true }, + svf: { inherits: 'gen', props: [ 'in' ] }, + tanh: { inherits: 'gen', props: [ 'gain', 'in' ], hasAudioOut: true }, + tanha: { inherits: 'gen', props: [ 'gain', 'in' ], hasAudioOut: true }, + tap: { inherits: 'gen', props: [ 'ms', 'in' ], hasAudioOut: true }, + tri: { inherits: 'aosc', props: [], hasAudioOut: true }, + zero: { inherits: 'gen', props: [], hasAudioOut: true } +} as const + +export namespace Props { + export interface Adsr extends Gen { + attack?: Value | number + decay?: Value | number + sustain?: Value | number + release?: Value | number + on?: Value | number + off?: Value | number + } + export interface Aosc extends Osc { + + } + export interface Atan extends Gen { + gain?: Value | number + in?: Value.Audio + } + export interface Bap extends Biquad { + cut?: Value | number + q?: Value | number + } + export interface Bbp extends Biquad { + cut?: Value | number + q?: Value | number + } + export interface Bhp extends Biquad { + cut?: Value | number + q?: Value | number + } + export interface Bhs extends Biquad { + cut?: Value | number + q?: Value | number + amt?: Value | number + } + export interface Biquad extends Gen { + in?: Value.Audio + } + export interface Blp extends Biquad { + cut?: Value | number + q?: Value | number + } + export interface Bls extends Biquad { + cut?: Value | number + q?: Value | number + amt?: Value | number + } + export interface Bno extends Biquad { + cut?: Value | number + q?: Value | number + } + export interface Bpk extends Biquad { + cut?: Value | number + q?: Value | number + amt?: Value | number + } + export interface Clamp extends Gen { + min?: Value | number + max?: Value | number + in?: Value.Audio + } + export interface Clip extends Gen { + threshold?: Value | number + in?: Value.Audio + } + export interface Comp extends Gen { + threshold?: Value | number + ratio?: Value | number + attack?: Value | number + release?: Value | number + in?: Value.Audio + sidechain?: Value.Audio + } + export interface Daverb extends Gen { + in?: Value.Audio + pd?: Value | number + bw?: Value | number + fi?: Value | number + si?: Value | number + dc?: Value | number + ft?: Value | number + st?: Value | number + dp?: Value | number + ex?: Value | number + ed?: Value | number + } + export interface Dcc extends Gen { + ceil?: Value | number + in?: Value.Audio + sample?: Value | number + } + export interface Dclip extends Gen { + in?: Value.Audio + } + export interface Dclipexp extends Gen { + factor?: Value | number + in?: Value.Audio + } + export interface Dcliplin extends Gen { + threshold?: Value | number + factor?: Value | number + in?: Value.Audio + } + export interface Delay extends Gen { + ms?: Value | number + fb?: Value | number + in?: Value.Audio + } + export interface Diode extends Gen { + cut?: Value | number + hpf?: Value | number + sat?: Value | number + q?: Value | number + in?: Value.Audio + } + export interface Exp extends Osc { + + } + export interface Freesound extends Smp { + id?: string + } + export interface Gen { + gain?: Value | number + } + export interface Gendy extends Gen { + step?: Value | number + } + export interface Grain extends Gen { + amt?: Value | number + } + export interface Inc extends Gen { + amt?: Value | number + trig?: Value | number + } + export interface Lp extends Gen { + cut?: Value | number + in?: Value.Audio + } + export interface Mhp extends Moog { + cut?: Value | number + q?: Value | number + } + export interface Mlp extends Moog { + cut?: Value | number + q?: Value | number + } + export interface Moog extends Gen { + in?: Value.Audio + } + export interface Noi extends Osc { + + } + export interface Nrate extends Gen { + normal?: Value | number + } + export interface Osc extends Gen { + hz?: Value | number + trig?: Value | number + offset?: Value | number + } + export interface Ramp extends Aosc { + + } + export interface Rate extends Gen { + samples?: Value | number + } + export interface Sap extends Svf { + cut?: Value | number + q?: Value | number + } + export interface Saw extends Aosc { + + } + export interface Say extends Smp { + text?: string + } + export interface Sbp extends Svf { + cut?: Value | number + q?: Value | number + } + export interface Shp extends Svf { + cut?: Value | number + q?: Value | number + } + export interface Sin extends Osc { + + } + export interface Slp extends Svf { + cut?: Value | number + q?: Value | number + } + export interface Smp extends Gen { + offset?: Value | number + length?: Value | number + trig?: Value | number + } + export interface Sno extends Svf { + cut?: Value | number + q?: Value | number + } + export interface Spk extends Svf { + cut?: Value | number + q?: Value | number + } + export interface Sqr extends Aosc { + + } + export interface Svf extends Gen { + in?: Value.Audio + } + export interface Tanh extends Gen { + gain?: Value | number + in?: Value.Audio + } + export interface Tanha extends Gen { + gain?: Value | number + in?: Value.Audio + } + export interface Tap extends Gen { + ms?: Value | number + in?: Value.Audio + } + export interface Tri extends Aosc { + + } + export interface Zero extends Gen { + + } +} + +export interface Gen { + adsr: (p: Props.Adsr) => Value.Audio + aosc: (p: Props.Aosc) => Value.Audio + atan: (p: Props.Atan) => Value.Audio + bap: (p: Props.Bap) => Value.Audio + bbp: (p: Props.Bbp) => Value.Audio + bhp: (p: Props.Bhp) => Value.Audio + bhs: (p: Props.Bhs) => Value.Audio + biquad: (p: Props.Biquad) => Value.Audio + blp: (p: Props.Blp) => Value.Audio + bls: (p: Props.Bls) => Value.Audio + bno: (p: Props.Bno) => Value.Audio + bpk: (p: Props.Bpk) => Value.Audio + clamp: (p: Props.Clamp) => Value.Audio + clip: (p: Props.Clip) => Value.Audio + comp: (p: Props.Comp) => Value.Audio + daverb: (p: Props.Daverb) => Value.Audio + dcc: (p: Props.Dcc) => Value.Audio + dclip: (p: Props.Dclip) => Value.Audio + dclipexp: (p: Props.Dclipexp) => Value.Audio + dcliplin: (p: Props.Dcliplin) => Value.Audio + delay: (p: Props.Delay) => Value.Audio + diode: (p: Props.Diode) => Value.Audio + exp: (p: Props.Exp) => Value.Audio + freesound: (p: Props.Freesound) => Value.Audio + gen: (p: Props.Gen) => Value.Audio + gendy: (p: Props.Gendy) => Value.Audio + grain: (p: Props.Grain) => Value.Audio + inc: (p: Props.Inc) => Value.Audio + lp: (p: Props.Lp) => Value.Audio + mhp: (p: Props.Mhp) => Value.Audio + mlp: (p: Props.Mlp) => Value.Audio + moog: (p: Props.Moog) => void + noi: (p: Props.Noi) => Value.Audio + nrate: (p: Props.Nrate) => void + osc: (p: Props.Osc) => Value.Audio + ramp: (p: Props.Ramp) => Value.Audio + rate: (p: Props.Rate) => void + sap: (p: Props.Sap) => Value.Audio + saw: (p: Props.Saw) => Value.Audio + say: (p: Props.Say) => Value.Audio + sbp: (p: Props.Sbp) => Value.Audio + shp: (p: Props.Shp) => Value.Audio + sin: (p: Props.Sin) => Value.Audio + slp: (p: Props.Slp) => Value.Audio + smp: (p: Props.Smp) => Value.Audio + sno: (p: Props.Sno) => Value.Audio + spk: (p: Props.Spk) => Value.Audio + sqr: (p: Props.Sqr) => Value.Audio + svf: (p: Props.Svf) => void + tanh: (p: Props.Tanh) => Value.Audio + tanha: (p: Props.Tanha) => Value.Audio + tap: (p: Props.Tap) => Value.Audio + tri: (p: Props.Tri) => Value.Audio + zero: (p: Props.Zero) => Value.Audio +} diff --git a/generated/typescript/dsp-vm.ts b/generated/typescript/dsp-vm.ts new file mode 100644 index 0000000..bab6f2b --- /dev/null +++ b/generated/typescript/dsp-vm.ts @@ -0,0 +1,120 @@ +// TypeScript VM Producer Factory +// auto-generated from scripts +import { Op } from '~/generated/assembly/dsp-op.ts' +import { DEBUG } from '~/src/as/dsp/constants.ts' + +type usize = number +type i32 = number +type u32 = number +type f32 = number + +export type DspVm = ReturnType + +export function createVm(ops: Int32Array) { + const ops_i32 = ops + const ops_u32 = new Uint32Array(ops.buffer, ops.byteOffset, ops.length) + const ops_f32 = new Float32Array(ops.buffer, ops.byteOffset, ops.length) + let i = 0 + return { + get index() { + return i + }, + Begin(): void { + DEBUG && console.log('Begin') + i = 0 + }, + End(): number { + DEBUG && console.log('End') + ops_u32[i++] = 0 + return i + }, + CreateGen(kind_index: i32): void { + DEBUG && console.log('CreateGen', kind_index) + ops_u32[i++] = Op.CreateGen + ops_i32[i++] = kind_index + }, + CreateAudios(count: i32): void { + DEBUG && console.log('CreateAudios', count) + ops_u32[i++] = Op.CreateAudios + ops_i32[i++] = count + }, + CreateValues(count: i32): void { + DEBUG && console.log('CreateValues', count) + ops_u32[i++] = Op.CreateValues + ops_i32[i++] = count + }, + AudioToScalar(audio$: i32, scalar$: i32): void { + DEBUG && console.log('AudioToScalar', audio$, scalar$) + ops_u32[i++] = Op.AudioToScalar + ops_i32[i++] = audio$ + ops_i32[i++] = scalar$ + }, + LiteralToAudio(literal$: i32, audio$: i32): void { + DEBUG && console.log('LiteralToAudio', literal$, audio$) + ops_u32[i++] = Op.LiteralToAudio + ops_i32[i++] = literal$ + ops_i32[i++] = audio$ + }, + Pick(list$: i32, list_length: i32, list_index_value$: i32, out_value$: i32): void { + DEBUG && console.log('Pick', list$, list_length, list_index_value$, out_value$) + ops_u32[i++] = Op.Pick + ops_i32[i++] = list$ + ops_i32[i++] = list_length + ops_i32[i++] = list_index_value$ + ops_i32[i++] = out_value$ + }, + Pan(value$: i32): void { + DEBUG && console.log('Pan', value$) + ops_u32[i++] = Op.Pan + ops_i32[i++] = value$ + }, + SetValue(value$: i32, kind: i32, ptr: i32): void { + DEBUG && console.log('SetValue', value$, kind, ptr) + ops_u32[i++] = Op.SetValue + ops_i32[i++] = value$ + ops_i32[i++] = kind + ops_i32[i++] = ptr + }, + SetValueDynamic(value$: i32, scalar$: i32, audio$: i32): void { + DEBUG && console.log('SetValueDynamic', value$, scalar$, audio$) + ops_u32[i++] = Op.SetValueDynamic + ops_i32[i++] = value$ + ops_i32[i++] = scalar$ + ops_i32[i++] = audio$ + }, + SetProperty(gen$: i32, prop$: i32, kind: i32, value$: i32): void { + DEBUG && console.log('SetProperty', gen$, prop$, kind, value$) + ops_u32[i++] = Op.SetProperty + ops_i32[i++] = gen$ + ops_i32[i++] = prop$ + ops_i32[i++] = kind + ops_i32[i++] = value$ + }, + UpdateGen(gen$: i32): void { + DEBUG && console.log('UpdateGen', gen$) + ops_u32[i++] = Op.UpdateGen + ops_i32[i++] = gen$ + }, + ProcessAudio(gen$: i32, audio$: i32): void { + DEBUG && console.log('ProcessAudio', gen$, audio$) + ops_u32[i++] = Op.ProcessAudio + ops_i32[i++] = gen$ + ops_i32[i++] = audio$ + }, + ProcessAudioStereo(gen$: i32, audio_0$: i32, audio_1$: i32): void { + DEBUG && console.log('ProcessAudioStereo', gen$, audio_0$, audio_1$) + ops_u32[i++] = Op.ProcessAudioStereo + ops_i32[i++] = gen$ + ops_i32[i++] = audio_0$ + ops_i32[i++] = audio_1$ + }, + BinaryOp(op: usize, lhs$: i32, rhs$: i32, out$: i32): void { + DEBUG && console.log('BinaryOp', op, lhs$, rhs$, out$) + ops_u32[i++] = Op.BinaryOp + ops_u32[i++] = op + ops_i32[i++] = lhs$ + ops_i32[i++] = rhs$ + ops_i32[i++] = out$ + } + } +} diff --git a/package.json b/package.json index a1ff49b..1fd16b0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "migrate:latest": "kysely migrate:latest && bun run models", "migrate:rollback": "kysely migrate:rollback --all", "models": "kysely-zod-codegen --env-file=.env.development --camel-case --out-file=api/models.ts", + "scripts": "bun run scripts/generate-dsp-vm.ts && bun run scripts/update-dsp-factory.ts && bun run scripts/update-gens-offsets.ts", "pwa": "pwa-assets-generator --preset minimal-2023 public/favicon.svg", "postinstall": "link-local || exit 0" }, @@ -52,6 +53,7 @@ "vite": "^5.4.8", "vite-plugin-externalize-dependencies": "^1.0.1", "vite-plugin-pwa": "^0.20.0", + "vite-plugin-watch-and-run": "^1.7.1", "vite-tsconfig-paths": "^5.0.1", "workbox-core": "^7.1.0", "workbox-precaching": "^7.1.0", diff --git a/scripts/generate-dsp-vm.ts b/scripts/generate-dsp-vm.ts new file mode 100644 index 0000000..3940036 --- /dev/null +++ b/scripts/generate-dsp-vm.ts @@ -0,0 +1,136 @@ +import fs from 'node:fs' + +function writeIfNotEqual(filename: string, text: string): void { + let existingText = '' + + try { + existingText = fs.readFileSync(filename, 'utf-8') + } + catch (e) { + const error: NodeJS.ErrnoException = e as any + if (error.code !== 'ENOENT') { + throw error + } + } + + if (existingText !== text) { + fs.writeFileSync(filename, text, 'utf-8') + console.log(`File "${filename}" ${existingText ? 'updated' : 'created'}.`) + } +} + +const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1) +const numericTypes = new Set('usize i32 u32 f32'.split(' ')) +const floatTypes = new Set('f32') +const parseFuncsRegExp = /(?[a-z]+)\((?.*)\)(?::.*?){/gi +const indent = (n: number, s: string) => s.split('\n').map(x => ' '.repeat(n) + x).join('\n') + +const srcFilename = './as/assembly/dsp/vm/dsp.ts' +const code = fs.readFileSync(srcFilename, 'utf-8') +const fns = [...code.matchAll(parseFuncsRegExp)].map(m => ({ + fn: m.groups!.fn, + args: m.groups!.args + .split(', ') + .slice(1) // arg 0 is always context, which is passed to the factory earlier + .map(x => x.split(': ')) as [name: string, type: string][] +})).filter(({ fn }) => fn !== 'constructor') + +const api = [ + `Begin(): void { + DEBUG && console.log('Begin') + i = 0 +}`, + `End(): number { + DEBUG && console.log('End') + ops_u32[i++] = 0 + return i +}`, + ...fns.map(({ fn, args }) => `${fn}(${args.map(([name, type]) => + `${name}: ${numericTypes.has(type) ? type : 'usize'}`).join(', ')}): void { + DEBUG && console.log('${fn}', ${args.map(([name, type]) => name).join(', ')}) + ops_u32[i++] = Op.${fn} +${indent(2, args.map(([name, type]) => `ops_${numericTypes.has(type) ? type : 'u32'}[i++] = ${name}`).join('\n'))} +}` + ) +] + +{ + const out = /*ts*/`\ +// TypeScript VM Producer Factory +// auto-generated from scripts +import { Op } from '~/generated/assembly/dsp-op.ts' +import { DEBUG } from '~/src/as/dsp/constants.ts' + +${[...numericTypes].map(x => `type ${x} = number`).join('\n')} + +export type DspVm = ReturnType + +export function createVm(ops: Int32Array) { + const ops_i32 = ops + const ops_u32 = new Uint32Array(ops.buffer, ops.byteOffset, ops.length) + const ops_f32 = new Float32Array(ops.buffer, ops.byteOffset, ops.length) + let i = 0 + return { + get index() { + return i + }, +${indent(4, api.join(',\n'))} + } +} +` + const outFilename = './generated/typescript/dsp-vm.ts' + writeIfNotEqual(outFilename, out) +} + +{ + const out = /*ts*/`\ +// TypeScript + AssemblyScript Ops Enum +// auto-generated from scripts +export enum Op { + End, + Begin, +${indent(2, fns.map(({ fn }) => `${fn}`).join(',\n'))} +} +` + const outFilename = './generated/assembly/dsp-op.ts' + writeIfNotEqual(outFilename, out) +} + +{ + const out = /*ts*/`\ +// AssemblyScript VM Runner +// auto-generated from scripts +import { Op } from './dsp-op' +import { Dsp, DspBinaryOp } from '../../as/assembly/dsp/vm/dsp' +import { Sound } from '../../as/assembly/dsp/vm/sound' + +const dsp = new Dsp() + +export function run(ctx: Sound, ops$: usize): void { + const ops = changetype>(ops$) + + let i: i32 = 0 + let op: i32 = 0 + + while (unchecked(op = ops[i++])) { + switch (op) { + +${indent(6, fns.map(({ fn, args }) => + `case Op.${fn}: + dsp.${fn}( + ctx, +${indent(4, args.map(([, type]) => + `changetype<${type}>(unchecked(ops[i++]))` + ).join(',\n'))} + ) +continue +`).join('\n'))} + } // end switch + } // end while +} +` + const outFilename = './generated/assembly/dsp-runner.ts' + writeIfNotEqual(outFilename, out) +} + +console.log('done generate-dsp-vm.') diff --git a/scripts/update-dsp-factory.ts b/scripts/update-dsp-factory.ts new file mode 100644 index 0000000..a985f84 --- /dev/null +++ b/scripts/update-dsp-factory.ts @@ -0,0 +1,35 @@ +import fs from 'fs' +import { basename, join } from 'path' +import { capitalize, writeIfNotEqual } from '~/scripts/util.ts' + +const gensRoot = './as/assembly/dsp/gen' +const files = fs.readdirSync(gensRoot).sort() + +const extendsRegExp = /extends\s([^\s]+)/ + +let out: string[] = [] +out.push(`import { Engine } from '../../as/assembly/dsp/core/engine'`) +const factories: string[] = [] +for (const file of files) { + const base = basename(file, '.ts') + const filename = join(gensRoot, file) + const text = fs.readFileSync(filename, 'utf-8') + const parentCtor = text.match(extendsRegExp)?.[1] + const ctor = capitalize(base) + const factory = `create${ctor}` + out.push(`import { ${ctor} } from '../.${gensRoot}/${base}'`) + if (['osc', 'gen'].includes(base)) { + factories.push(`createZero`) // dummy because they are abstract + continue + } + factories.push(factory) + out.push(`function ${factory}(engine: Engine): ${ctor} { return new ${ctor}(engine) }`) +} + +out.push(`export const Factory: ((engine: Engine) => Gen)[] = [${factories}]`) + +const targetPath = './generated/assembly/dsp-factory.ts' +const text = out.join('\n') +writeIfNotEqual(targetPath, text) + +console.log('done update-dsp-factory.') diff --git a/scripts/update-gens-offsets.ts b/scripts/update-gens-offsets.ts new file mode 100644 index 0000000..4c01b52 --- /dev/null +++ b/scripts/update-gens-offsets.ts @@ -0,0 +1,21 @@ +import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts' +import { capitalize, writeIfNotEqual } from '~/scripts/util.ts' +import { getAllPropsDetailed } from '~/src/as/dsp/util.ts' + +let out: string[] = [] +const offsets: string[] = [] +for (const k in dspGens) { + const props = getAllPropsDetailed(k as keyof Gen) + out.push(`import { ${capitalize(k)} } from '../../as/assembly/dsp/gen/${k.toLowerCase()}'`) + offsets.push(` [${props.map(x => `offsetof<${capitalize(x.ctor)}>('${x.name}')`)}]`) +} + +out.push('export const Offsets: usize[][] = [') +out.push(offsets.join(',\n')) +out.push(']') + +const targetPath = './generated/assembly/dsp-offsets.ts' +const text = out.join('\n') +writeIfNotEqual(targetPath, text) + +console.log('done update-gens-offsets.') diff --git a/scripts/util.ts b/scripts/util.ts new file mode 100644 index 0000000..89f05c1 --- /dev/null +++ b/scripts/util.ts @@ -0,0 +1,22 @@ +import fs from 'fs' + +export const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1) + +export function writeIfNotEqual(filename: string, text: string): void { + let existingText = '' + + try { + existingText = fs.readFileSync(filename, 'utf-8') + } + catch (e) { + const error: NodeJS.ErrnoException = e as any + if (error.code !== 'ENOENT') { + throw error + } + } + + if (existingText !== text) { + fs.writeFileSync(filename, text, 'utf-8') + console.log(`File "${filename}" ${existingText ? 'updated' : 'created'}.`) + } +} diff --git a/src/as/dsp/constants.ts b/src/as/dsp/constants.ts new file mode 100644 index 0000000..3e33401 --- /dev/null +++ b/src/as/dsp/constants.ts @@ -0,0 +1 @@ +export const DEBUG = false diff --git a/src/as/dsp/dsp-service.ts b/src/as/dsp/dsp-service.ts new file mode 100644 index 0000000..396fd53 --- /dev/null +++ b/src/as/dsp/dsp-service.ts @@ -0,0 +1,36 @@ +import { Sigui, nu } from 'sigui' +import { Deferred, rpc } from 'utils' +import { Clock } from './dsp-shared.ts' +import type { DspWorker } from './dsp-worker.ts' +import DspWorkerFactory from './dsp-worker.ts?worker' + +export type DspService = ReturnType + +export function DspService(ctx: AudioContext) { + using $ = Sigui() + + const deferred = Deferred() + const ready = deferred.promise + const worker = new DspWorkerFactory() + + const service = rpc(worker, { + async isReady() { + deferred.resolve() + } + }) + + class DspInfo { + dsp = $.unwrap(() => ready.then(() => service.createDsp(ctx.sampleRate))) + @nu get clock() { + const { dsp } = $.of(this) + if (dsp instanceof Error) return + $() + const clock = Clock(dsp.memory.buffer, dsp.clock$) + return clock + } + } + + const info = $(new DspInfo) + + return { info, ready, service } +} diff --git a/src/as/dsp/dsp-shared.ts b/src/as/dsp/dsp-shared.ts new file mode 100644 index 0000000..6e7db9f --- /dev/null +++ b/src/as/dsp/dsp-shared.ts @@ -0,0 +1,21 @@ +import { Struct } from 'utils' + +export type Clock = typeof Clock.type + +export const Clock = Struct({ + time: 'f64', + timeStep: 'f64', + prevTime: 'f64', + startTime: 'f64', + endTime: 'f64', + bpm: 'f64', + coeff: 'f64', + barTime: 'f64', + barTimeStep: 'f64', + loopStart: 'f64', + loopEnd: 'f64', + sampleRate: 'u32', + jumpBar: 'i32', + ringPos: 'u32', + nextRingPos: 'u32', +}) diff --git a/src/as/dsp/dsp-worker.ts b/src/as/dsp/dsp-worker.ts new file mode 100644 index 0000000..d7c8c03 --- /dev/null +++ b/src/as/dsp/dsp-worker.ts @@ -0,0 +1,158 @@ +// @ts-ignore +self.document = { + querySelectorAll() { return [] as any }, + baseURI: location.origin +} + +import { wasm } from 'dsp' +import { Lru, rpc } from 'utils' +import { Dsp, Sound } from '~/src/as/dsp/dsp.ts' +import { Note } from '~/src/as/dsp/notes-shared.ts' +import { ParamValue } from '~/src/as/dsp/params-shared.ts' +import { AstNode } from '~/src/lang/interpreter.ts' +import { Token, tokenize } from '~/src/lang/tokenize.ts' + +export type DspWorker = typeof worker + +const sounds = new Map() + +const getFloats = Lru(20, (key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) +const getBuffer = Lru(20, (length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) +const getPointers = Lru(20, (length: number) => wasm.alloc(Uint32Array, length), item => item.fill(0), item => item.free()) + +const worker = { + dsp: null as null | Dsp, + tokensAstNode: new Map(), + waveLength: 1, + error: null as Error | null, + async createDsp(sampleRate: number) { + const dsp = this.dsp = Dsp({ sampleRate }) + return { + memory: wasm.memory, + clock$: dsp.clock.ptr, + } + }, + async createSound() { + const dsp = this.dsp + if (!dsp) { + throw new Error('Dsp not ready.') + } + const sound = dsp.Sound() + sounds.set(+sound.sound$, sound) + return +sound.sound$ as number + }, + async renderSource( + sound$: number, + audioLength: number, + code: string, + voicesCount: number, + hasMidiIn: boolean, + notes: Note[], + params: ParamValue[][], + ) { + const dsp = this.dsp + if (!dsp) { + throw new Error('Dsp not ready.') + } + + const sound = sounds.get(sound$) + if (!sound) { + throw new Error('Sound not found, id: ' + sound$) + } + + const { clock } = dsp + const info = this + + try { + // ensure an audio in the stack + code = code.trim() || '[zero]' + + const tokens = [...tokenize({ code })] + + sound.reset() + + clock.time = 0 + clock.barTime = 0 + clock.bpm ||= 144 + + wasm.updateClock(clock.ptr) + + const { program, out } = sound.process( + tokens, + voicesCount, + hasMidiIn, + params.length, + ) + + if (!out.LR) { + return { error: 'No audio in the stack.' } + } + + info.tokensAstNode = program.value.tokensAstNode + + const length = Math.floor(audioLength * clock.sampleRate / clock.coeff) + + const key = `${sound$}:${length}` + const floats = getFloats(key, length) + + const notesData = getBuffer(notes.length * 4) // * 4 elements: n, time, length, vel + { + let i = 0 + for (const note of notes) { + const p = (i++) * 4 + notesData[p] = note.n + notesData[p + 1] = note.time + notesData[p + 2] = note.length + notesData[p + 3] = note.vel + } + } + + const paramsData = getPointers(params.length * 2) // * 1 el: ptr, length + { + let i = 0 + for (const values of params) { + const data = getBuffer(values.length * 4) + const p = (i++) * 2 + paramsData[p] = data.ptr + paramsData[p + 1] = values.length + + let j = 0 + for (const value of values) { + const p = (j++) * 4 + data[p] = value.time + data[p + 1] = value.length + data[p + 2] = value.slope + data[p + 3] = value.amt + } + } + } + + wasm.fillSound(sound.sound$, + sound.ops.ptr, + notesData.ptr, + notes.length, + paramsData.ptr, + params.length, + out.LR.getAudio(), + 0, + floats.length, + floats.ptr, + ) + + return { floats } + } + catch (e) { + if (e instanceof Error) { + console.warn(e) + console.warn(...((e as any)?.cause?.nodes ?? [])) + info.error = e + return { error: e.message } + } + throw e + } + }, +} + +const host = rpc<{ isReady(): void }>(self as any, worker) +host.isReady() +console.log('[dsp-worker] started') diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts new file mode 100644 index 0000000..02f6d0a --- /dev/null +++ b/src/as/dsp/dsp.ts @@ -0,0 +1,617 @@ +import { wasm } from './wasm.ts' +import { Sigui } from 'sigui' +import { Struct, fromEntries, getMemoryView, keys } from 'utils' +import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS, MAX_SCALARS } from '../../../as/assembly/dsp/constants.ts' +import { DspBinaryOp, SoundData as SoundDataShape } from '../../../as/assembly/dsp/vm/dsp-shared.ts' +import { Op } from '../../../generated/assembly/dsp-op.ts' +import { Gen, dspGens } from '../../../generated/typescript/dsp-gens.ts' +import { createVm } from '../../../generated/typescript/dsp-vm.ts' +import { AstNode, interpret } from '../../lang/interpreter.ts' +import { Token, tokenize } from '../../lang/tokenize.ts' +import { parseNumber } from '../../lang/util.ts' +import { Clock } from './dsp-shared.ts' +import { getAllProps } from './util.ts' +import { Value } from './value.ts' + +const DEBUG = false + +const MAX_OPS = 4096 + +const SoundData = Struct({ + begin: 'u32', + end: 'u32', + pan: 'f32', +}) + +const dspGensKeys = keys(dspGens) + +export type Dsp = ReturnType +export type Sound = ReturnType +export type SoundContext = ReturnType + +function getContext() { + return { + gens: 0, + values: 0, + floats: 0, + scalars: 0, + audios: 0, + literals: 0, + literalsf: null as unknown as Float32Array, + lists: 0, + listsi: null as unknown as Int32Array, + } +} + +const tokensCopyMap = new Map() +const tokensCopyRevMap = new Map() +function copyToken(token: Token) { + const newToken = { ...token } + tokensCopyMap.set(token, newToken) + tokensCopyRevMap.set(newToken, token) + return newToken +} + +let preludeTokens: Token[] + +export function Dsp({ sampleRate, core$ }: { + sampleRate: number + core$?: ReturnType +}) { + using $ = Sigui() + + core$ ??= wasm.createCore(sampleRate) + const pin = (x: T): T => { wasm.__pin(+x); return x } + const engine$ = wasm.createEngine(sampleRate, core$) + const clock$ = wasm.getEngineClock(engine$) + const clock = Clock(wasm.memory.buffer, clock$) + + const view = getMemoryView(wasm.memory) + + preludeTokens ??= [...tokenize({ + code: + // we implicit call [nrate 1] before our code + // so that the sample rate is reset. + `[nrate 1]` + // some builtin procedures + + ` { .5* .5+ } norm= ` + + ` { at= p= sp= 1 [inc sp co* at] clip - p^ } dec= ` + // + `{ n= p= sp= 1 [inc sp co* t n*] clip - p^ } decay=` + // + ` { t= p= sp= 1 [inc sp co* t] clip - p^ } down= ` + })] + + function Sound() { + const sound$ = wasm.createSound(engine$) + + const data$ = wasm.getSoundData(sound$) + const data = SoundData(wasm.memory.buffer, +data$) satisfies SoundDataShape + const context = getContext() + + const ops = wasm.alloc(Int32Array, MAX_OPS) + const vm = createVm(ops) + const setup_ops = wasm.alloc(Int32Array, MAX_OPS) + const setup_vm = createVm(setup_ops) + + const lists$ = wasm.getSoundLists(sound$) + const lists = view.getI32(lists$, MAX_LISTS) + context.listsi = lists + + const scalars$ = wasm.getSoundScalars(sound$) + const scalars = view.getF32(scalars$, MAX_SCALARS) + + const literals$ = wasm.getSoundLiterals(sound$) + const literals = view.getF32(literals$, MAX_LITERALS) + const preset = createLiteralsPreset(literals) + preset.set() + + function reset() { + wasm.resetSound(sound$) + } + + function prepare() { + context.gens = + context.audios = + context.literals = + context.floats = + context.scalars = + context.lists = + context.values = + 0 + } + + function run() { + wasm.dspRun(sound$, ops.ptr) + } + + function commit() { + vm.End() + + if (DEBUG) { + let op$: Op + let i = 0 + while (op$ = ops[i++]) { + const op = Op[op$] + const len = (vm as any)[op].length + const slice = ops.subarray(i, i + len) + console.log(op, ...slice) + i += len + } + } + } + + function setup() { + setup_vm.CreateAudios(context.audios) + setup_vm.CreateValues(context.values) + setup_vm.End() + wasm.dspRun(sound$, setup_ops.ptr) + } + + function getAudio(index: number) { + const ptr = wasm.getSoundAudio(sound$, index) + const audio = view.getF32(ptr, BUFFER_SIZE) + return audio + } + + function createLiteralsPreset( + literals: Float32Array = wasm.alloc(Float32Array, MAX_LITERALS) + ) { + function set() { + context.literalsf = literals + wasm.setSoundLiterals(sound$, literals.byteOffset) + } + return { literals, set } + } + + function binaryOp( + BinOp: DspBinaryOp, + LiteralLiteral: (a: number, b: number) => number + ): BinaryOp { + return function binaryOp(lhs: Value | number, rhs: Value | number): any { + if (typeof lhs === 'number' && typeof rhs === 'number') { + return LiteralLiteral(lhs, rhs) + } + + const { Value } = sound + + let out = Value.Dynamic.create() + + let l: Value + let r: Value + + if (commutative.has(BinOp)) { + if (typeof lhs === 'number') { + if (typeof rhs === 'number') throw 'unreachable' + + l = rhs + r = Value.Literal.create(lhs) + } + else if (typeof rhs === 'number') { + l = lhs + r = Value.Literal.create(rhs) + } + else { + l = lhs + r = rhs + } + } + else { + if (typeof lhs === 'number') { + if (typeof rhs === 'number') throw 'unreachable' + + l = Value.Literal.create(lhs) + r = rhs + } + else if (typeof rhs === 'number') { + l = lhs + r = Value.Literal.create(rhs) + } + else { + l = lhs + r = rhs + } + } + + if (!l) { + throw new Error('Missing left operand.') + } + + if (!r) { + throw new Error('Missing right operand.') + } + + sound.vm.BinaryOp( + BinOp, + l.value$, + r.value$, + out.value$ + ) + + return out + } + } + + const soundPartial = { + context, + vm, + } + + const math = { + add: binaryOp(DspBinaryOp.Add, (a, b) => a + b), + mul: binaryOp(DspBinaryOp.Mul, (a, b) => a * b), + sub: binaryOp(DspBinaryOp.Sub, (a, b) => a - b), + div: binaryOp(DspBinaryOp.Div, (a, b) => a / b), + pow: binaryOp(DspBinaryOp.Pow, (a, b) => a ** b), + } + + function defineGen(name: keyof Gen, stereo: boolean): any { + const props = getAllProps(name) + const Gen = dspGens[name] + const kind_index = dspGensKeys.indexOf(name) + + function handle(opt: any) { + const gen$ = context.gens++ + setup_vm.CreateGen(kind_index) + DEBUG && console.log('CreateGen', name, gen$) + + for (let p in opt) { + const prop = `${name}.${p}` + const prop$ = props.indexOf(p as any) + DEBUG && console.log('Property', prop, opt[p]) + + if (prop$ >= 0) { + let value: number | undefined + outer: { + if (opt[p] instanceof Value) { + const v: Value = opt[p] + + // Audio + if (v.kind === Value.Kind.Audio) { + if (audioProps.has(p)) { + vm.SetProperty(gen$, prop$, Value.Kind.Audio as number, v.value$) + } + else { + const scalar = sound.Value.Scalar.create() + vm.AudioToScalar(v.ptr, scalar.ptr) + vm.SetProperty(gen$, prop$, Value.Kind.Scalar as number, scalar.value$) + } + break outer + } + else { + if (audioProps.has(p)) { + vm.SetProperty(gen$, prop$, Value.Kind.Audio as number, v.value$) + } + else { + vm.SetProperty(gen$, prop$, Value.Kind.Scalar as number, v.value$) + } + break outer + } + } + // Text + else if (typeof opt[p] === 'string') { + if (name === 'say' && p === 'text') { + const floats = sound.Value.Floats.create() + const text = opt[p] + // loadSayText(floats, text) + vm.SetProperty(gen$, prop$, Value.Kind.Floats as number, floats.value$) + break outer + } + if (name === 'freesound' && p === 'id') { + const floats = sound.Value.Floats.create() + const text = opt[p] + // loadFreesound(floats, text) + vm.SetProperty(gen$, prop$, Value.Kind.Floats as number, floats.value$) + break outer + } + value = 0 + } + // Literal + else if (typeof opt[p] === 'number') { + value = opt[p] + } + else { + throw new TypeError( + `Invalid type for property "${prop}": ${typeof opt[p]}`) + } + + if (typeof value !== 'number') { + throw new TypeError( + `Invalid value for property "${prop}": ${value}`) + } + + const literal = sound.Value.Literal.create(value) + if (audioProps.has(p)) { + const audio = sound.Value.Audio.create() + vm.LiteralToAudio(literal.ptr, audio.ptr) + vm.SetProperty(gen$, prop$, Value.Kind.Audio as number, audio.value$) + } + else { + vm.SetProperty(gen$, prop$, Value.Kind.Scalar as number, literal.value$) + } + } + } + } + + return gen$ + } + + function processMono(opt: any): Value | void { + const gen$ = handle(opt) + + if ('hasAudioOut' in Gen && Gen.hasAudioOut) { + const audio = sound.Value.Audio.create() + vm.ProcessAudio(gen$, audio.ptr) + return audio + } + else { + vm.UpdateGen(gen$) + } + } + + function processStereo(opt: any): [Value, Value] | void { + const gen$ = handle(opt) + + if ('hasStereoOut' in Gen && Gen.hasStereoOut) { + const out_0 = sound.Value.Audio.create() + const out_1 = sound.Value.Audio.create() + vm.ProcessAudioStereo(gen$, out_0.ptr, out_1.ptr) + return [out_0, out_1] + } + else { + vm.UpdateGen(gen$) + } + } + + return stereo + ? processStereo + : processMono + } + + const globals = {} as { + sr: Value.Scalar + t: Value.Scalar + rt: Value.Scalar + co: Value.Scalar + + n0n: Value.Scalar + n0f: Value.Scalar + n0t: Value.Scalar + n0v: Value.Scalar + + n1n: Value.Scalar + n1f: Value.Scalar + n1t: Value.Scalar + n1v: Value.Scalar + + n2n: Value.Scalar + n2f: Value.Scalar + n2t: Value.Scalar + n2v: Value.Scalar + + n3n: Value.Scalar + n3f: Value.Scalar + n3t: Value.Scalar + n3v: Value.Scalar + + n4n: Value.Scalar + n4f: Value.Scalar + n4t: Value.Scalar + n4v: Value.Scalar + + n5n: Value.Scalar + n5f: Value.Scalar + n5t: Value.Scalar + n5v: Value.Scalar + + p0: Value.Scalar + p1: Value.Scalar + p2: Value.Scalar + p3: Value.Scalar + p4: Value.Scalar + p5: Value.Scalar + } + + const api = { + globals, + math: { + ...math, + '+': math.add, + '*': math.mul, + '-': math.sub, + '/': math.div, + '^': math.pow, + }, + pan(value: Value | number): void { + if (typeof value === 'number') { + value = sound.Value.Literal.create(value) + } + vm.Pan(value.value$) + }, + pick(values: T[], index: Value | number): Value { + const list$ = context.lists + + const length = values.length + context.lists += length + + let i = list$ + for (let v of values) { + if (typeof v === 'number') { + const literal = sound.Value.Literal.create(v) + context.listsi[i++] = literal.value$ + } + else { + context.listsi[i++] = v.value$ + } + } + + if (typeof index === 'number') { + index = sound.Value.Literal.create(index) + } + + const out = sound.Value.Dynamic.create() + vm.Pick(list$, length, index.value$, out.value$) + return out + }, + gen: fromEntries( + dspGensKeys.map(name => + [name, defineGen(name, false)] + ) + ) as Gen, + gen_st: fromEntries( + dspGensKeys.map(name => + [name, defineGen(name, true)] + ) + ) as Gen + } + + let prevHashId: any + + function process(tokens: Token[], voicesCount: number, hasMidiIn: boolean, paramsCount: number) { + const scope = {} as any + const literals: AstNode[] = [] + + let tokensCopy = [ + ...preludeTokens, + ...tokens, + ] + .filter(t => t.type !== Token.Type.Comment) + .map(copyToken) + + + if (voicesCount && hasMidiIn) { + const voices = tokenize({ + code: Array.from({ length: voicesCount }, (_, i) => + `[midi_in n${i}v n${i}t n${i}f n${i}n]` + ).join('\n') + ` @` + }) + tokensCopy = [...tokensCopy, ...voices] + } + + // create hash id from tokens. We compare this afterwards to determine + // if we should make a new sound or update the old one. + const hashId = + [tokens.filter(t => t.type === Token.Type.Number).length].join('') + + tokens.filter(t => + [Token.Type.Id, Token.Type.Op].includes(t.type) + ).map(t => t.text).join('') + + voicesCount + + const isNew = hashId !== prevHashId + prevHashId = hashId + + // replace number tokens with literal references ids + // and populate initial scope for those ids. + for (const t of tokensCopy) { + if (t.type === Token.Type.Number) { + const id = `lit_${literals.length}` + const literal = new AstNode(AstNode.Type.Literal, parseNumber(t.text), [t]) + literals.push(literal) + scope[id] = literal + t.type = Token.Type.Id + t.text = id + } + } + + vm.Begin() + setup_vm.Begin() + prepare() + if (isNew) { + wasm.clearSound(sound$) + } + + globals.sr = sound.Value.Scalar.create() + globals.t = sound.Value.Scalar.create() + globals.rt = sound.Value.Scalar.create() + globals.co = sound.Value.Scalar.create() + + const { sr, t, rt, co } = globals + scope.sr = new AstNode(AstNode.Type.Result, { value: sr }) + scope.t = new AstNode(AstNode.Type.Result, { value: t }) + scope.rt = new AstNode(AstNode.Type.Result, { value: rt }) + scope.co = new AstNode(AstNode.Type.Result, { value: co }) + + for (let i = 0; i < 6; i++) { + for (const p of 'nftv') { + const name = `n${i}${p}` + const value = (globals as any)[name] = sound.Value.Scalar.create() + scope[name] = new AstNode(AstNode.Type.Result, { value }) + } + } + + for (let i = 0; i < 6; i++) { + const name = `p${i}` + const value = (globals as any)[name] = sound.Value.Scalar.create() + scope[name] = new AstNode(AstNode.Type.Result, { value }) + } + + const program = interpret(sound, scope, tokensCopy) + + let L = program.scope.vars['L'] + let R = program.scope.vars['R'] + let LR = program.scope.vars['LR'] + + const slice = program.scope.stack.slice(-2) + if (slice.length === 2) { + R ??= slice.pop() + L ??= slice.pop() + } + else if (slice.length === 1) { + LR ??= slice.pop() + } + + const out = { + L: L?.value as Value.Audio | undefined, + R: R?.value as Value.Audio | undefined, + LR: LR?.value as Value.Audio | undefined, + } + + commit() + + if (isNew) { + setup() + } + + return { + program, + isNew, + out, + } + } + + const sound = { + context, + sound$, + data$, + data, + vm, + ops, + setup_vm, + setup_ops, + preset, + run, + reset, + commit, + getAudio, + createLiteralsPreset, + values: [] as Value[], + Value: Value.Factory(soundPartial), + api, + process, + } + + return sound + } + + return { engine$, core$, clock, Sound } +} + +const commutative = new Set([DspBinaryOp.Add, DspBinaryOp.Mul]) +const audioProps = new Set(['in', 'sidechain']) +const textProps = new Set(['text', 'id']) + +export type BinaryOp = { + (lhs: number, rhs: number): number + (lhs: number, rhs: Value): Value + (lhs: Value, rhs: number): Value + (lhs: Value, rhs: Value): Value + // (lhs: Value | number, rhs: Value | number): Value | number +} diff --git a/src/as/dsp/index.ts b/src/as/dsp/index.ts new file mode 100644 index 0000000..23898e5 --- /dev/null +++ b/src/as/dsp/index.ts @@ -0,0 +1,3 @@ +export * from './dsp.ts' +export * from './util.ts' +export * from './wasm.ts' diff --git a/src/as/dsp/notes-shared.ts b/src/as/dsp/notes-shared.ts new file mode 100644 index 0000000..0b022ee --- /dev/null +++ b/src/as/dsp/notes-shared.ts @@ -0,0 +1,17 @@ +import { Struct } from 'utils' + +export interface Note { + n: number + time: number + length: number + vel: number +} + +export type NoteView = ReturnType + +export const NoteView = Struct({ + n: 'f32', + time: 'f32', + length: 'f32', + vel: 'f32', +}) diff --git a/src/as/dsp/params-shared.ts b/src/as/dsp/params-shared.ts new file mode 100644 index 0000000..8140d9f --- /dev/null +++ b/src/as/dsp/params-shared.ts @@ -0,0 +1,17 @@ +import { Struct } from 'utils' + +export interface ParamValue { + time: number + length: number + slope: number + amt: number +} + +export type ParamValueView = ReturnType + +export const ParamValueView = Struct({ + time: 'f32', + length: 'f32', + slope: 'f32', + amt: 'f32', +}) diff --git a/src/as/dsp/util.ts b/src/as/dsp/util.ts new file mode 100644 index 0000000..4d10093 --- /dev/null +++ b/src/as/dsp/util.ts @@ -0,0 +1,32 @@ +import { ValuesOf } from 'utils' +import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts' + +export function getAllProps(k: keyof Gen) { + const gen = dspGens[k] + const props: ValuesOf<(typeof dspGens)[keyof Gen]['props']>[] = [] + if ('inherits' in gen && gen.inherits) { + props.push(...getAllProps(gen.inherits)) + } + props.push(...gen.props) + return props +} + +export function getAllPropsReverse(k: keyof Gen) { + const gen = dspGens[k] + const props: ValuesOf<(typeof dspGens)[keyof Gen]['props']>[] = [] + props.push(...gen.props) + if ('inherits' in gen && gen.inherits) { + props.push(...getAllPropsReverse(gen.inherits)) + } + return props +} + +export function getAllPropsDetailed(k: keyof Gen) { + const gen = dspGens[k] + const props: { name: ValuesOf<(typeof dspGens)[keyof Gen]['props']>, ctor: typeof k }[] = [] + if ('inherits' in gen && gen.inherits) { + props.push(...getAllPropsDetailed(gen.inherits)) + } + props.push(...gen.props.map(x => ({ name: x, ctor: k }))) + return props +} diff --git a/src/as/dsp/value.ts b/src/as/dsp/value.ts new file mode 100644 index 0000000..980787b --- /dev/null +++ b/src/as/dsp/value.ts @@ -0,0 +1,118 @@ +import { Sound, SoundContext } from 'dsp' +import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts' +import { DspVm } from '~/generated/typescript/dsp-vm.ts' + +type SoundPartial = { context: SoundContext, vm: DspVm } + +export class Value { + value$: number + ptr: number = 0 + scalar$: number = 0 + audio$: number = 0 + context: Sound['context'] + constructor(sound: SoundPartial, kind: T extends Value.Kind.I32 | Value.Kind.Literal ? T : never, value: number) + constructor(sound: SoundPartial, kind: T extends Value.Kind.Null | Value.Kind.Floats | Value.Kind.Scalar | Value.Kind.Audio | Value.Kind.Dynamic ? T : never) + constructor(public sound: SoundPartial, public kind: Value.Kind, value?: number) { + this.context = sound.context + this.value$ = this.context.values++ + + switch (kind) { + case Value.Kind.Null: + this.ptr = 0 + break + case Value.Kind.I32: + this.ptr = value! + break + case Value.Kind.Floats: + this.ptr = this.context.floats++ + break + case Value.Kind.Literal: + this.ptr = this.context.literals++ + this.context.literalsf[this.ptr] = value! + break + case Value.Kind.Scalar: + this.ptr = this.context.scalars++ + break + case Value.Kind.Audio: + this.ptr = this.context.audios++ + break + case Value.Kind.Dynamic: + this.scalar$ = this.context.scalars++ + this.audio$ = this.context.audios++ + break + } + + if (kind === Value.Kind.Dynamic) { + sound.vm.SetValueDynamic(this.value$, this.scalar$, this.audio$) + } + else { + sound.vm.SetValue(this.value$, kind as number, this.ptr) + } + } + getAudio() { + if (this.kind === Value.Kind.Dynamic) { + return this.audio$ + } + else { + return this.ptr + } + } +} + +export namespace Value { + export enum Kind { + Null = SoundValueKind.Null, + I32 = SoundValueKind.I32, + Floats = SoundValueKind.Floats, + Literal = SoundValueKind.Literal, + Scalar = SoundValueKind.Scalar, + Audio = SoundValueKind.Audio, + Dynamic = SoundValueKind.Dynamic, + } + export type Null = Value + export type I32 = Value + export type Floats = Value + export type Literal = Value + export type Scalar = Value + export type Audio = Value + export type Dynamic = Value + + export function Factory(sound: SoundPartial) { + const Null = { + create() { return new Value(sound, Kind.Null) } + } + const I32 = { + create(value: number) { return new Value(sound, Kind.I32, value) } + } + const Floats = { + create() { return new Value(sound, Kind.Floats) } + } + const Literal = { + create(value: number) { return new Value(sound, Kind.Literal, value) } + } + const Scalar = { + create() { return new Value(sound, Kind.Scalar) } + } + const Audio = { + create() { return new Value(sound, Kind.Audio) } + } + const Dynamic = { + create() { return new Value(sound, Kind.Dynamic) } + } + function toScalar(x: Value) { + const scalar = Scalar.create() + sound.vm.AudioToScalar(x.ptr, scalar.ptr) + return scalar + } + return { + Null, + I32, + Floats, + Literal, + Scalar, + Audio, + Dynamic, + toScalar + } + } +} diff --git a/src/as/dsp/wasm.ts b/src/as/dsp/wasm.ts new file mode 100644 index 0000000..0530c80 --- /dev/null +++ b/src/as/dsp/wasm.ts @@ -0,0 +1,24 @@ +import { instantiate } from '~/as/build/dsp.js' +import url from '~/as/build/dsp.wasm?url' +import { hexToBinary, initWasm } from '~/src/as/init-wasm.ts' + +let mod: WebAssembly.Module + +if (import.meta.env && import.meta.env.MODE !== 'production') { + const hex = (await import('~/as/build/dsp.wasm?raw-hex')).default + const wasmMapUrl = new URL('/as/build/dsp.wasm.map', location.origin).href + const binary = hexToBinary(hex, wasmMapUrl) + mod = await WebAssembly.compile(binary) +} +else { + mod = await WebAssembly.compileStreaming(fetch(new URL(url, location.href))) +} + +const wasmInstance = await instantiate(mod, { + env: { + } +}) + +const { alloc } = initWasm(wasmInstance) + +export const wasm = Object.assign(wasmInstance, { alloc }) diff --git a/src/as/gfx/wasm.ts b/src/as/gfx/wasm.ts index 994a55a..291365d 100644 --- a/src/as/gfx/wasm.ts +++ b/src/as/gfx/wasm.ts @@ -23,7 +23,6 @@ function setFlushSketchFn(fn: (count: number) => void) { const wasmInstance = await instantiate(mod, { env: { - log: console.log, flushSketch(count: number) { DEBUG && console.debug('[flush]', count) flushSketchFn(count) diff --git a/src/as/init-wasm.ts b/src/as/init-wasm.ts index d80a0b6..4dad53d 100644 --- a/src/as/init-wasm.ts +++ b/src/as/init-wasm.ts @@ -102,7 +102,7 @@ export function initWasm(wasm: Wasm) { // Might not be ideal in all situations. // We shouldn't refresh if the failure is right after a new refresh, // otherwise we enter into infinite refreshes loop. - if (+new Date() - +new Date(performance.timeOrigin) > 2_000) { + if (+new Date() - +new Date(performance.timeOrigin) > 5_000) { location.href = location.href } diff --git a/src/as/pkg/wasm.ts b/src/as/pkg/wasm.ts index 066e5d3..f1f7880 100644 --- a/src/as/pkg/wasm.ts +++ b/src/as/pkg/wasm.ts @@ -16,7 +16,6 @@ else { const wasm = await instantiate(mod, { env: { - log: console.log, } }) diff --git a/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx new file mode 100644 index 0000000..f14efbf --- /dev/null +++ b/src/comp/DspEditor.tsx @@ -0,0 +1,243 @@ +import { pointToLinecol, type Pane, type WordWrapProcessor } from 'editor' +import { Sigui, type Signal } from 'sigui' +import { assign, clamp } from 'utils' +import { Token, tokenize } from '~/src/lang/tokenize.ts' +import { screen } from '~/src/screen.ts' +import { theme } from '~/src/theme.ts' +import { Editor } from '~/src/ui/Editor.tsx' +import { HoverMarkWidget } from '~/src/ui/editor/widgets/index.ts' + +export function DspEditor({ code, width, height }: { + width: Signal + height: Signal + code: Signal +}) { + using $ = Sigui() + + const info = $({ + c: null as null | CanvasRenderingContext2D, + pr: screen.$.pr, + width, + height, + code, + }) + + const mouse = { x: 0, y: 0 } + let number: RegExpMatchArray | undefined + let value: number + let digits: number + let isDot = false + const hoverMark = HoverMarkWidget() + + function getHoveringNumber(pane: Pane) { + const word = pane.buffer.wordUnderLinecol(pane.mouse.info.linecol) + if (word != null) { + digits = word[0].split('.')[1]?.length ?? 0 + let string = parseFloat(word[0]).toFixed(digits) + isDot = word[0][0] === '.' + if (isDot && string === '0') string = '0.0' + if (string === `${isDot ? '0' : ''}${word[0]}`) { + string = parseFloat(word[0]).toFixed(digits) + return word + } + } + } + + function updateNumberMark(pane: Pane) { + const temp = getHoveringNumber(pane) + if (temp && !pane.info.isHoveringScrollbarX && !pane.info.isHoveringScrollbarY) { + const number = temp + pane.view.el.style.cursor = 'default' + const linecol = pointToLinecol(pane.buffer.indexToVisualPoint(number.index)) + assign( + hoverMark.widget.bounds, + linecol, + { + right: pointToLinecol(pane.buffer.indexToVisualPoint(number.index + number[0].length)).col, + length: number[0].length, + bottom: linecol.line, + } + ) + pane.draw.widgets.mark.add(hoverMark.widget) + pane.draw.info.triggerUpdateTokenDrawInfo++ + pane.view.anim.info.epoch++ + } + else { + pane.draw.widgets.mark.delete(hoverMark.widget) + pane.view.anim.info.epoch++ + pane.view.el.style.cursor = pane.view.info.cursor + } + } + + function handleNumberDrag(dx: number, dy: number) { + if (number?.index == null) return + + if (Math.abs(dx) + Math.abs(dy) < .5) return + + let s: string + + let dv = + Math.max(.001, Math.abs(dx) ** 1.4) * Math.sign(dx) + + Math.max(.001, Math.abs(dy) ** 1.4) * Math.sign(dy) + + if (dv === 0) dv = 0.001 * (dy > dx ? Math.sign(dy) : Math.sign(dx)) + if (dv === 0) dv = 0.001 + if (dv > 0) dv = Math.max(0.001, dv) + if (dv < 0) dv = Math.min(-0.001, dv) + + let min: number + let max: number + let mul = 0.0017 + if (value >= 10_000 && value < 100_000) { + min = 10_000 + max = parseFloat('99999.' + (digits ? '9'.repeat(digits) : '0')) + } + else if (value >= 1_000 && value < 10_000) { + min = 1_000 + max = parseFloat('9999.' + (digits ? '9'.repeat(digits) : '0')) + } + else if (value >= 100 && value < 1_000) { + min = 100 + max = parseFloat('999.' + (digits ? '9'.repeat(digits) : '0')) + } + else if (value >= 10 && value < 100) { + min = 10 + max = parseFloat('99.' + (digits ? '9'.repeat(digits) : '0')) + } + else if (value >= 1 && value < 10) { + min = 1 + max = parseFloat('9.' + (digits ? '9'.repeat(digits) : '0')) + if (!digits) mul = 0.005 + } + else if (value >= 0 && value < 1) { + min = 0 + max = parseFloat('0.' + (digits ? '9'.repeat(digits) : '0')) + if (digits === 1) mul = 0.005 + } + else { + return + } + + const scale = max - min + let normal = (value - min) / scale + normal = clamp(0, 1, normal - dv * mul) + value = normal * scale + min + s = value.toFixed(digits) + if (isDot) s = s.slice(1) + const { code } = editor.info.pane.buffer + editor.info.pane.buffer.code = `${code.slice(0, number.index)}${s}${code.slice(number.index + s.length)}` + } + + function onKeyDown(pane: Pane) { + queueMicrotask(() => { + updateNumberMark(pane) + }) + } + + function onKeyUp(pane: Pane) { + updateNumberMark(pane) + if (!pane.kbd.info.ctrl) { + number = void 0 + } + } + + function onMouseWheel(pane: Pane) { + let ret = false + + if (pane.mouse.info.ctrl || pane.kbd.info.ctrl) { + updateNumberMark(pane) + if (number?.index != null || onMouseDown(pane)) ret = true + if (ret) { + handleNumberDrag( + -pane.mouse.info.wheel.x * .06, + -pane.mouse.info.wheel.y * .06 + ) + } + } + else { + number = void 0 + } + + return ret + } + + function onMouseDown(pane: Pane) { + number = getHoveringNumber(pane) + if (number?.index == null) return + value = parseFloat(number[0]) + mouse.x = pane.mouse.info.x + mouse.y = pane.mouse.info.y + return true + } + + function onMouseMove(pane: Pane) { + if (number?.index == null) { + updateNumberMark(pane) + return + } + const p = pane.mouse.info + const dx = mouse.x - p.x + const dy = p.y - mouse.y + mouse.x = p.x + mouse.y = p.y + handleNumberDrag(dx, dy) + return true + } + + function onMouseUp(pane: Pane) { + if (number) { + number = void 0 + return true + } + } + + const inputHandlers = { + onKeyDown, + onKeyUp, + onMouseWheel, + onMouseDown, + onMouseUp, + onMouseMove, + } + + const colors: Partial> = { + [Token.Type.Native]: { fill: theme.colors.sky[500], stroke: theme.colors.sky[500] }, + [Token.Type.String]: { fill: theme.colors.fuchsia[700], stroke: theme.colors.fuchsia[700] }, + [Token.Type.Keyword]: { fill: theme.colors.orange[500], stroke: theme.colors.orange[500] }, + [Token.Type.Op]: { fill: theme.colors.sky[500], stroke: theme.colors.sky[500] }, + [Token.Type.Id]: { fill: theme.colors.yellow[500], stroke: theme.colors.yellow[500] }, + [Token.Type.Number]: { fill: theme.colors.green[500], stroke: theme.colors.green[500] }, + [Token.Type.BlockComment]: { fill: theme.colors.neutral[700], stroke: theme.colors.neutral[700] }, + [Token.Type.Comment]: { fill: theme.colors.neutral[700], stroke: theme.colors.neutral[700] }, + [Token.Type.Any]: { fill: theme.colors.neutral[500], stroke: theme.colors.neutral[500] }, + } + + function colorize(token: Token) { + const { fill = '#888', stroke = '#888' } = colors[token.type] ?? {} + return { fill, stroke } + } + + const wordWrapProcessor: WordWrapProcessor = { + pre(input: string) { + return input.replace(/\[(\w+)([^\]\n]+)\]/g, (_: any, word: string, chunk: string) => { + const c = chunk.replace(/\s/g, '\u0000') + return `[${word}${c}]` + }) + }, + post(input: string) { + return input.replaceAll('\u0000', ' ') + } + } + + const editor = Editor({ + width: info.$.width, + height: info.$.height, + code: info.$.code, + colorize, + tokenize, + wordWrapProcessor, + inputHandlers, + }) + + return editor +} diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts new file mode 100644 index 0000000..c7c7389 --- /dev/null +++ b/src/lang/interpreter.ts @@ -0,0 +1,403 @@ +import { getAllPropsReverse, Sound } from 'dsp' +import { dspGens } from '~/generated/typescript/dsp-gens.ts' +import { Token } from '~/src/lang/tokenize.ts' +import { parseNumber } from '~/src/lang/util.ts' + +const DEBUG = false + +export class AstNode { + constructor( + public type: AstNode.Type, + data: Partial = {}, + public captured: Token[] = [] + ) { + Object.assign(this, data) + } + id: any + kind: any + scope: Scope = new Scope(null) + value: any + get bounds() { + return Token.bounds(this.captured) + } +} + +export namespace AstNode { + export enum Type { + Program = 'Program', + Proc = 'Proc', + ProcCall = 'ProcCall', + Procedure = 'Procedure', + List = 'List', + Result = 'Result', + Literal = 'Literal', + Id = 'Id', + String = 'String', + } + export const BlockType = { + '[': AstNode.Type.ProcCall, + '{': AstNode.Type.Procedure, + '(': AstNode.Type.List, + } + export enum ProcKind { + Gen, + GenStereo, + User, + Special, + } +} + +class Scope { + constructor( + public parent: Scope | null, + public vars: Record = {} + ) { } + stack: any[] = [] + stackPop() { + return this.stack.pop() + } + stackPush(x: {}) { + this.stack.push(x) + } + stackUnshiftOfTypes(types: any[], climb?: boolean) { + let s: Scope | null = this + let res: any + do { + res = unshiftOfTypes(s.stack, types) + if (res) return res + if (!climb) return + } while (s = s.parent) + } + stackPopOfTypes(types: any[], climb?: boolean) { + let s: Scope | null = this + let res: any + do { + res = popOfTypes(s.stack, types) + if (res) return res + if (!climb) return + } while (s = s.parent) + } + lookup(prop: string, climb?: boolean) { + let s: Scope | null = this + do { + if (prop in s.vars) return s.vars[prop] + if (!climb) return + } while (s = s.parent) + } +} + +function unshiftOfTypes(arr: any[], types: any[]) { + for (let i = 0; i < arr.length; i++) { + if (types.includes(arr[i]?.type)) { + return arr.splice(i, 1)[0] + } + } +} + +function popOfTypes(arr: any[], types: any[]) { + for (let i = arr.length - 1; i >= 0; i--) { + if (types.includes(arr[i]?.type)) { + return arr.splice(i, 1)[0] + } + } +} + +const ConsumeTypes = [ + AstNode.Type.Id, + AstNode.Type.Literal, + AstNode.Type.Result, + AstNode.Type.Procedure, + AstNode.Type.String, +] + +const ScopeNatives = Object.fromEntries( + [ + ...Object.entries(dspGens).map(([id]) => + [id, new AstNode(AstNode.Type.Proc, { id, kind: AstNode.ProcKind.Gen })] + ), + ...Object.entries(dspGens).filter(([, g]) => 'hasStereoOut' in g && g.hasStereoOut).map(([id]) => + [id + '_st', new AstNode(AstNode.Type.Proc, { id, kind: AstNode.ProcKind.GenStereo })] + ), + ] +) + +const ScopeSpecial = { + pan: new AstNode(AstNode.Type.Proc, { id: 'pan', kind: AstNode.ProcKind.Special }) +} + +const BinOps = new Set('+ * - / ^'.split(' ')) +const AssignOps = new Set('= += *= -= /= ^='.split(' ')) + +export function interpret(sound: Sound, data: Record, tokens: Token[]) { + const g = sound.api + const scope: Scope = new Scope(null, { ...ScopeNatives, ...ScopeSpecial, ...data }) + const results: (Record & { result: AstNode })[] = [] + const tokensAstNode: Map = new Map() + const capturing = new Set() + + function map(tokens: Token[], node: AstNode) { + tokens.forEach(t => tokensAstNode.set(t, node)) + return node + } + + type Context = ReturnType + + function createContext(tokens: Token[]) { + let i = 0 + let t: Token + + function next() { + t = tokens[i++] + capturing.forEach(c => c.push(t)) + return t + } + function peek() { + return tokens[i] + } + function expectText(text: string) { + if (text && peek()?.text !== text) { + throw new SyntaxError('Expected text ' + text, { cause: { nodes: [peek()] } }) + } + return next() + } + function until(parent: Scope, text?: string) { + const scope = new Scope(parent) + const captured: Token[] = [t] + capturing.add(captured) + while (i < tokens.length && (!text || peek()?.text !== text)) { + const res = process(c, scope, next()) + if (res) { + if (Array.isArray(res)) { + res.reverse().forEach(x => scope.stackPush(x as any)) + } + else { + scope.stackPush(res) + } + } + } + if (text) expectText(text) + capturing.delete(captured) + return { scope, captured } + } + function tokensUntil(text?: string) { + const resTokens: Token[] = [] + while (i < tokens.length && (!text || peek()?.text !== text)) { + resTokens.push(next()) + } + if (text) expectText(text) + return { tokens: resTokens } + } + + const c = { tokens, next, peek, expectText, until, tokensUntil } + + return c + } + + const root = createContext(tokens) + + function processProcCall(node: AstNode) { + const proc: AstNode | undefined = node.scope.stack.shift() + + if (proc) { + if (proc.kind === AstNode.ProcKind.Gen || proc.kind === AstNode.ProcKind.GenStereo) { + const genId = proc.id as keyof typeof dspGens + const genInfo = dspGens[genId] + const allProps = getAllPropsReverse(genId) as any + + for (const p of allProps) { + DEBUG && console.log(p) + let item = node.scope.lookup(p) + if (!item) { + item = node.scope.stackUnshiftOfTypes(ConsumeTypes) + if (['in'].includes(p)) { + item = node.scope.parent!.stackPopOfTypes(ConsumeTypes, true) + } + } + if (item) { + node.scope.vars[p] = item + } + } + + const genData = Object.fromEntries( + Object.entries(node.scope.vars).map(([key, { value }]: any) => + [key, value] + ) + ) + + // console.log(genId, genData) + if (allProps.includes('in') && !('in' in genData)) { + throw new Error('Missing input for ' + genId, { cause: { nodes: [node] } }) + } + + if (proc.kind === AstNode.ProcKind.Gen) { + const value = g.gen[genId](genData) + if (value) { + // console.log(genId, value) + const result = new AstNode(AstNode.Type.Result, { value }, node.captured) + results.push({ result, genId, genData }) + + // TODO: only return for hasAudioOut + return result + } + } + else if (proc.kind === AstNode.ProcKind.GenStereo) { + const value: any = g.gen_st[genId](genData) + if (value) { + // console.log(genId, value) + const result_left = new AstNode(AstNode.Type.Result, { value: value[0] }, node.captured) + results.push({ result: result_left, genId, genData }) + const result_right = new AstNode(AstNode.Type.Result, { value: value[1] }, node.captured) + results.push({ result: result_right, genId, genData }) + + // TODO: only return for hasAudioOut + return [result_left, result_right] + } + } + } + else if (proc.kind === AstNode.ProcKind.User) { + const c = createContext(proc.value.tokens) + const { scope } = c.until(node.scope) + return scope.stack.at(-1) + } + else if (proc.kind === AstNode.ProcKind.Special) { + if (proc.id === 'pan') { + const value = node.scope.stackUnshiftOfTypes(ConsumeTypes) + g.pan(value.value) + } + } + } + } + + function process(c: Context, scope: Scope, t: Token) { + // console.log('process', t.type, t.text) + switch (t.type) { + case Token.Type.Number: { + return new AstNode(AstNode.Type.Literal, { value: parseNumber(t.text) }, [t]) + } + + case Token.Type.String: { + return new AstNode(AstNode.Type.String, { value: t.text.slice(1, -1) }) + } + + case Token.Type.Id: { + const node = new AstNode(AstNode.Type.Id, { value: t.text }, [t]) + const prop = scope.lookup(t.text, true) + if (prop) { + // not a procedure and not a literal, then map it so that + // we can do syntax highlighting as a variable. + if (prop.type !== AstNode.Type.Proc && prop.type !== AstNode.Type.Literal) { + map([t], node) + } + // if it's a procedure not at the call position, call it. + if ((prop.type === AstNode.Type.Proc || prop.type === AstNode.Type.Procedure) && scope.stack.length > 0) { + const childScope = new Scope(scope) + childScope.stackPush(prop) + const node = new AstNode(AstNode.Type.ProcCall, { scope: childScope, captured: [t] }) + return processProcCall(node) + } + return prop + } + // it's a bare identifier, so it's a user variable, so map it + // for syntax highlighting. + return map([t], node) + } + + case Token.Type.Keyword: { + if (t.text === '@') { + if (scope.stack.length === 1) return scope.stack.pop() + let l: any + let r = scope.stack.pop()?.value + if (r == null) return + while (scope.stack.length) { + l = scope.stack.pop() + r = g.math.add(l.value, r) + } + const node = new AstNode(AstNode.Type.Result, { value: r }, [t]) + return node + } + break + } + + case Token.Type.Op: + if (t.text === '?') { + const index = scope.stackPopOfTypes(ConsumeTypes) + const list = scope.stackPopOfTypes([AstNode.Type.List], true) + if (!index) { + throw new Error('Missing index for pick (?).', { cause: { nodes: [t] } }) + } + if (!list) { + throw new Error('Missing list for pick (?).', { cause: { nodes: [t] } }) + } + const value = g.pick(list.scope.stack.map((x: any) => x.value), index.value) + const node = new AstNode(AstNode.Type.Result, { value }, [t]) + results.push({ result: node, op: t, index, list }) + return node + } + if (BinOps.has(t.text)) { + const r = scope.stackPopOfTypes(ConsumeTypes) + const l = scope.stackPopOfTypes(ConsumeTypes, true) + if (!r) { + throw new Error('Missing right operand.', { cause: { nodes: [t] } }) + } + if (!l) { + throw new Error('Missing left operand.', { cause: { nodes: [t] } }) + } + const value = (g.math as any)[t.text](l.value, r.value) + const node = new AstNode(AstNode.Type.Result, { value }, [t]) + results.push({ result: node, op: t, lhs: l, rhs: r }) + return node + } + if (AssignOps.has(t.text)) { + const r = scope.stackPopOfTypes(ConsumeTypes) + const l = scope.stackPopOfTypes(ConsumeTypes, true) + if (!r) { + throw new Error('Missing right operand.', { cause: { nodes: [t] } }) + } + if (r.type !== AstNode.Type.Id) { + console.error(r, l, scope) + throw new Error('Expected identifier for assignment operation.', { cause: { nodes: [r] } }) + } + if (!l) { + throw new Error('Missing left operand.', { cause: { nodes: [t] } }) + } + scope.vars[r.value] = l + return + } + switch (t.text) { + case '{': { + const op = t.text + const close = Token.Close[op] + const node = new AstNode(AstNode.Type.Procedure, { value: c.tokensUntil(close) }) + node.kind = AstNode.ProcKind.User + return node + } + case '[': + case '(': { + const op = t.text + const close = Token.Close[op] + const node = new AstNode(AstNode.BlockType[op], c.until(scope, close)) + switch (node.type) { + case AstNode.Type.ProcCall: { + return processProcCall(node) + } + } + return node + } + } + break + } + throw new SyntaxError('Cannot handle token: ' + t.type + ' ' + t.text, { cause: { nodes: [t] } }) + } + + function program() { + return new AstNode(AstNode.Type.Program, { + ...root.until(scope), + value: { + results, + tokensAstNode, + } + }) + } + + return program() +} diff --git a/src/lang/util.ts b/src/lang/util.ts new file mode 100644 index 0000000..88206cd --- /dev/null +++ b/src/lang/util.ts @@ -0,0 +1,63 @@ +export type NumberFormat = 'f' | 'd' | 'h' | 'k' | '#' + +export interface NumberInfo { + value: number + format: 'f' | 'd' | 'h' | 'k' | '#' + digits: number +} + +const testModifierRegExp = /[\.khd#]/ + +export function parseNumber(x: string): NumberInfo { + let value: number + let format: NumberFormat = 'f' + let digits = 0 + + let res: any + out: { + if (res = testModifierRegExp.exec(x)) { + switch (res[0]) { + case '.': { + const [, b = ''] = x.split('.') + digits = b.length + value = Number(x) + break out + } + + case 'k': { + const [a, b = ''] = x.split('k') + format = 'k' + digits = b.length + value = Number(a) * 1000 + Number(b) * (1000 / (10 ** digits)) + break out + } + + case 'h': { + const [a, b = ''] = x.split('h') + format = 'h' + digits = b.length + value = Number(a) * 100 + Number(b) * (100 / (10 ** digits)) + break out + } + + case 'd': { + const [a, b = ''] = x.split('d') + format = 'd' + digits = b.length + value = Number(a) * 10 + Number(b) * digits + break out + } + + case '#': { + format = '#' + value = parseInt(x.slice(1), 16) + break out + } + } + } + + value = Number(x) + } + + return { value, format, digits } +} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index b6fe7dc..bc00a9f 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -10,6 +10,7 @@ import { About } from '~/src/pages/About.tsx' import { AssemblyScript } from '~/src/pages/AssemblyScript.tsx' import { CanvasDemo } from '~/src/pages/CanvasDemo' import { Chat } from '~/src/pages/Chat/Chat.tsx' +import { DspDemo } from '~/src/pages/DspDemo.tsx' import { EditorDemo } from '~/src/pages/EditorDemo.tsx' import { Home } from '~/src/pages/Home.tsx' import { OAuthRegister } from '~/src/pages/OAuthRegister.tsx' @@ -40,6 +41,7 @@ export function App() { '!/canvas': () => , '/webgl': () => , '/editor': () => , + '/dsp': () => , '/asc': () => , '/qrcode': () => , '/about': () => , diff --git a/src/pages/DspDemo.tsx b/src/pages/DspDemo.tsx new file mode 100644 index 0000000..bc4c765 --- /dev/null +++ b/src/pages/DspDemo.tsx @@ -0,0 +1,136 @@ +import { Dsp, wasm as wasmDsp } from 'dsp' +import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' +import { Sigui } from 'sigui' +import { assign, Lru } from 'utils' +import { DspEditor } from '~/src/comp/DspEditor.tsx' +import { Token, tokenize } from '~/src/lang/tokenize.ts' +import { Canvas } from '~/src/ui/Canvas.tsx' +import type { Editor } from '~/src/ui/Editor.tsx' +import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { H2 } from '~/src/ui/Heading.tsx' + +const getFloats = Lru(20, (key: string, length: number) => wasmDsp.alloc(Float32Array, length), item => item.fill(0), item => item.free()) +const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) +const getBuffer = Lru(20, (length: number) => wasmDsp.alloc(Float32Array, length), item => item.fill(0), item => item.free()) +const getPointers = Lru(20, (length: number) => wasmDsp.alloc(Uint32Array, length), item => item.fill(0), item => item.free()) + +export function DspDemo() { + using $ = Sigui() + + const info = $({ + width: 400, + height: 300, + code: `[sin 40.10 303 [exp 2.01] 5.20^ * +] [exp 1.00] 9.99^ *`, + floats: new Float32Array(), + }) + + const ctx = new AudioContext({ sampleRate: 48000 }) + const dsp = Dsp({ sampleRate: ctx.sampleRate }) + const { clock } = dsp + const sound = dsp.Sound() + + const barsCount = .25 + const length = 8192 //Math.floor(barsCount * clock.sampleRate / clock.coeff) + + const canvas = as HTMLCanvasElement + const gfx = Gfx({ canvas }) + const view = Rect(0, 0, 500, 500) + const matrix = Matrix() + const c = gfx.createContext(view, matrix) + const shapes = c.createShapes() + c.sketch.scene.add(shapes) + + const plot = WaveGlWidget(shapes) + plot.widget.rect.w = 400 + plot.widget.rect.h = 300 + plot.info.floats = wasmGfx.alloc(Float32Array, length) + + const waveWidgets: WaveGlWidget[] = [] + + requestAnimationFrame(() => { + $.fx(() => { + const { pane } = dspEditor.info + const { codeVisual } = pane.buffer.info + $() + + sound.reset() + const tokens = Array.from(tokenize({ code: codeVisual })) + const { program, out } = sound.process(tokens, 6, false, 0) + if (!out.LR) { + throw new Error('No audio in the stack.') + } + const floats = getFloats('floats', length) + info.floats = floats + const notes = [] + const params = [] + const notesData = getBuffer(notes.length * 4) + const paramsData = getPointers(params.length * 2) + wasmDsp.fillSound( + sound.sound$, + sound.ops.ptr, + notesData.ptr, + notes.length, + paramsData.ptr, + params.length, + out.LR.getAudio(), + 0, + floats.length, + floats.ptr, + ) + + + // pane.draw.widgets.update() + + plot.info.floats.set(floats) + requestAnimationFrame($.fn(() => { + c.meshes.draw() + let nodeCount = 0 + + program.value.results.sort((a: any, b: any) => + a.result.captured[0].line === + b.result.captured[0].line + ? a.result.captured[0].col - + b.result.captured[0].col + : a.result.captured[0].line - + b.result.captured[0].line + ) + + let last + for (const node of program.value.results) { + if ('genId' in node) { + const bounds = Token.bounds(node.result.captured) + if (last && last.bounds.line === bounds.line && last.bounds.right > bounds.col) { + last.bounds.right = bounds.col - 1 + last.wave.widget.bounds.right = bounds.col - 1 + // console.log('yes') + } + // console.log(nodeCount, bounds) + const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + wave.info.floats = wave.info.floats.length ? wave.info.floats : getFloatsGfx(`${nodeCount}`, 8192) + wave.info.floats.set(sound.getAudio(node.result.value.ptr)) + assign(wave.widget.bounds, bounds) + pane.draw.widgets.deco.add(wave.widget) + node.bounds = bounds + node.wave = wave + last = node + nodeCount++ + } + } + + pane.draw.info.triggerUpdateTokenDrawInfo++ + pane.view.anim.info.epoch++ + })) + }) + }) + const dspEditor = as Editor + + return
+

Dsp demo

+ {dspEditor} + {canvas} +
+} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index ef8afe7..20ca5af 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -28,6 +28,7 @@ export function Home() { Canvas WebGL Editor + Dsp AssemblyScript QrCode About diff --git a/src/ui/Editor.tsx b/src/ui/Editor.tsx index 50e11b3..115e27f 100644 --- a/src/ui/Editor.tsx +++ b/src/ui/Editor.tsx @@ -1,8 +1,8 @@ import { Input, Misc, Pane, Rect, View, type InputHandlers, type WordWrapProcessor } from 'editor' import { Sigui, type $, type Signal } from 'sigui' import type { Source, Token } from '~/src/lang/tokenize.ts' -import { screen } from '~/src/screen.ts' -import { state } from '~/src/state.ts' + +export type Editor = ReturnType export function Editor({ code, width, height, colorize, tokenize, wordWrapProcessor, inputHandlers }: { code: Signal diff --git a/src/ui/editor/view.tsx b/src/ui/editor/view.tsx index 43b2966..76ca78c 100644 --- a/src/ui/editor/view.tsx +++ b/src/ui/editor/view.tsx @@ -42,7 +42,11 @@ export function View({ width, height }: { " /> as HTMLTextAreaElement - const el =
({ cursor: info.cursor })}> + const el =
({ + cursor: info.cursor, + width: `${info.width}px`, + height: `${info.height}px`, + })}> {canvas} {glCanvas} {svg} diff --git a/src/ui/editor/widgets/wave-gl.ts b/src/ui/editor/widgets/wave-gl.ts index aa848f3..90d27d6 100644 --- a/src/ui/editor/widgets/wave-gl.ts +++ b/src/ui/editor/widgets/wave-gl.ts @@ -3,6 +3,8 @@ import { wasm, type Shapes } from 'gfx' import { Sigui } from 'sigui' import { hexToInt } from '~/src/ui/editor/util/rgb.ts' +export type WaveGlWidget = ReturnType + export function WaveGlWidget(shapes: Shapes) { using $ = Sigui() diff --git a/tsconfig.json b/tsconfig.json index 7185ef9..0d5c4d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,9 @@ { "include": [ "admin", + "generated/typescript", "lib", + "scripts", "src", "vendor" ], @@ -44,6 +46,9 @@ "editor": [ "./src/ui/editor/index.ts" ], + "dsp": [ + "src/as/dsp/index.ts" + ], "gfx": [ "./src/as/gfx/index.ts" ], diff --git a/vendor/as-transform-update-dsp-gens.js b/vendor/as-transform-update-dsp-gens.js new file mode 100644 index 0000000..6ea55c4 --- /dev/null +++ b/vendor/as-transform-update-dsp-gens.js @@ -0,0 +1,95 @@ +'use strict' + +import { Transform } from 'assemblyscript/transform' +import Binaryen from 'assemblyscript/binaryen' +import * as fs from 'fs' +import * as path from 'path' +import * as util from 'util' +import { capitalize, extendsRegExp, sortObjectInPlace } from './util.js' + +class UpdateDspGens extends Transform { + afterCompile(module) { + const fnCount = module.getNumFunctions() + const dspGens = {} + for (let i = 0; i < fnCount; i++) { + const fnRef = module.getFunctionByIndex(i) + const info = Binaryen.getFunctionInfo(fnRef) + const name = info.name + if (name.includes('~visit')) + continue + if (name.startsWith('as/assembly/')) { + const fnId = name.split('/').pop() + let [instanceName, propId = ''] = fnId.split('#') + if (/^[A-Z]/.test(instanceName) && /\/gen\//.test(name)) { + instanceName = instanceName.toLowerCase() + const propName = propId?.split(':')[1] + let inherits + if (!(instanceName in dspGens)) { + const filepath = './' + name.toLowerCase().split('#')[0].split('/').slice(0, -1).join('/') + '.ts' + const text2 = fs.readFileSync(filepath, 'utf-8') + const parentCtor = text2.match(extendsRegExp)?.[1] + if (parentCtor) + inherits = parentCtor.toLowerCase() + } + const obj = dspGens[instanceName] ??= { + inherits, + props: [] + } + if (!obj.inherits) + delete obj.inherits + if (propId.startsWith('set:') || propId.startsWith('get:')) { + if (!propName.startsWith('_') && !obj.props.includes(propName)) + obj.props.push(propName) + } else if (propId === '_audio') { + obj.hasAudioOut = true + } else if (propId === '_audio_stereo') { + obj.hasStereoOut = true + } + } + } + } + sortObjectInPlace(dspGens) + const audioProps = ['in', 'sidechain'] + const textProps = ['text', 'id'] + const interfaces = Object.entries(dspGens).map( + ([k, v]) => ` export interface ${capitalize(k)} ${v.inherits ? `extends ${capitalize(v.inherits)} ` : ''}{ +${v.props.map((x, i) => ` ${x}?: ${audioProps.includes(x) ? 'Value.Audio' : textProps.includes(x) ? 'string' : 'Value | number'}`).join('\n')} + }` + ).join('\n') + function hasAudioOut(k) { + const gen = dspGens[k] + let res + if (gen.hasAudioOut) + res = true + else if (gen.inherits && gen.inherits !== 'gen' && hasAudioOut(gen.inherits)) + res = true + if (res) { + gen.hasAudioOut = true + } + return res + } + const types = Object.keys(dspGens).map( + (k) => ` ${k}: (p: Props.${capitalize(k)}) => ${hasAudioOut(k) ? 'Value.Audio' : 'void'}` + ).join('\n') + const formatted = util.format('%O', dspGens) + const date = new Date() + const text = /*ts*/`// +// auto-generated ${date.toDateString()} ${date.toTimeString()} + +import { Value } from '../../src/dsp/value.ts' + +export const dspGens = ${formatted} as const + +export namespace Props { +${interfaces} +} + +export interface Gen { +${types} +} +` + const filename = path.join(process.cwd(), 'generated', 'typescript', 'dsp-gens.ts') + fs.writeFileSync(filename, text) + } +} +export default UpdateDspGens diff --git a/vendor/util.js b/vendor/util.js new file mode 100644 index 0000000..3bdf6f4 --- /dev/null +++ b/vendor/util.js @@ -0,0 +1,31 @@ +"use strict"; +function replaceNonAlphaNumeric(x, replaceValue) { + return x.replaceAll(/[^a-z0-9]/ig, replaceValue); +} +export function cleanupExportName(x) { + x = replaceNonAlphaNumeric(x.split("assembly/").slice(1).join("_"), "_"); + const parts = x.split("_"); + const [pre, name, fn, ...methodParts] = parts; + if (!fn) { + return x; + } else if (fn.toLowerCase() !== name) { + return `${pre === "gen" ? `${pre}_` : ""}${name}_${[fn, ...methodParts].join("_")}`; + } else { + return `${pre === "gen" ? `${pre}_` : ""}${name}_${methodParts.join("_")}`; + } +} +export const capitalize = (s) => s[0].toUpperCase() + s.slice(1); +export const extendsRegExp = /extends\s([^\s]+)/; +export function sortCompareKeys([a], [b]) { + return a < b ? -1 : a > b ? 1 : 0; +} +export function sortObjectInPlace(data) { + const sorted = Object.fromEntries( + Object.entries(data).sort(sortCompareKeys) + ); + for (const key in data) { + delete data[key]; + } + Object.assign(data, sorted); + return data; +} diff --git a/vite.config.ts b/vite.config.ts index aca76e8..80ad10f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,7 @@ import { ViteHexLoader } from './vendor/vite-plugin-hex-loader.ts' import { ViteOpenInEditor } from './vendor/vite-plugin-open-in-editor.ts' import { VitePrintAddress } from './vendor/vite-plugin-print-address.ts' import { ViteUsing } from './vendor/vite-plugin-using.ts' +import { watchAndRun } from 'vite-plugin-watch-and-run' const root = process.cwd() const homedir = os.homedir() @@ -97,6 +98,17 @@ export default ({ mode }) => { (moduleName) => moduleName.startsWith('node:') ], }), + ViteAssemblyScript({ + configFile: 'asconfig-dsp.json', + projectRoot: '.', + srcMatch: 'as/assembly/dsp', + srcEntryFile: 'as/assembly/dsp/index.ts', + mapFile: './as/build/dsp.wasm.map', + extra: [ + '--transform', './vendor/as-transform-unroll.js', + '--transform', './vendor/as-transform-update-dsp-gens.js', + ] + }), ViteAssemblyScript({ configFile: 'asconfig-pkg.json', projectRoot: '.', @@ -127,6 +139,15 @@ export default ({ mode }) => { '--transform', './vendor/as-transform-unroll.js', ] }), + watchAndRun([ + { + name: 'scripts', + watchKind: ['add', 'change', 'unlink'], + watch: path.resolve('scripts/**.ts'), + run: 'pnpm scripts', + delay: 100 + } + ]) as Plugin, ] return defineConfig({ From 96784aba52972b3b10c9faf1e98e426ab62dcf31 Mon Sep 17 00:00:00 2001 From: stagas Date: Wed, 16 Oct 2024 09:38:50 +0300 Subject: [PATCH 05/33] wip better typings --- generated/typescript/dsp-gens.ts | 8 +-- src/as/dsp/dsp.ts | 28 ++++----- src/lang/interpreter.ts | 83 ++++++++++++++++++-------- src/pages/DspDemo.tsx | 29 ++++----- vendor/as-transform-update-dsp-gens.js | 26 +++++++- 5 files changed, 111 insertions(+), 63 deletions(-) diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 7c31b00..28e07a6 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,7 +1,7 @@ // -// auto-generated Wed Oct 16 2024 07:59:10 GMT+0300 (Eastern European Summer Time) +// auto-generated Wed Oct 16 2024 08:42:09 GMT+0300 (Eastern European Summer Time) -import { Value } from '../../src/dsp/value.ts' +import { Value } from '../../src/as/dsp/value.ts' export const dspGens = { adsr: { @@ -354,7 +354,7 @@ export interface Gen { clamp: (p: Props.Clamp) => Value.Audio clip: (p: Props.Clip) => Value.Audio comp: (p: Props.Comp) => Value.Audio - daverb: (p: Props.Daverb) => Value.Audio + daverb: (p: Props.Daverb) => [Value.Audio, Value.Audio] dcc: (p: Props.Dcc) => Value.Audio dclip: (p: Props.Dclip) => Value.Audio dclipexp: (p: Props.Dclipexp) => Value.Audio @@ -363,7 +363,7 @@ export interface Gen { diode: (p: Props.Diode) => Value.Audio exp: (p: Props.Exp) => Value.Audio freesound: (p: Props.Freesound) => Value.Audio - gen: (p: Props.Gen) => Value.Audio + gen: (p: Props.Gen) => [Value.Audio, Value.Audio] gendy: (p: Props.Gendy) => Value.Audio grain: (p: Props.Grain) => Value.Audio inc: (p: Props.Inc) => Value.Audio diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index 02f6d0a..3d7f96f 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -166,7 +166,7 @@ export function Dsp({ sampleRate, core$ }: { BinOp: DspBinaryOp, LiteralLiteral: (a: number, b: number) => number ): BinaryOp { - return function binaryOp(lhs: Value | number, rhs: Value | number): any { + return function binaryOp(lhs: Value | number, rhs: Value | number) { if (typeof lhs === 'number' && typeof rhs === 'number') { return LiteralLiteral(lhs, rhs) } @@ -227,7 +227,7 @@ export function Dsp({ sampleRate, core$ }: { ) return out - } + } as BinaryOp } const soundPartial = { @@ -243,19 +243,19 @@ export function Dsp({ sampleRate, core$ }: { pow: binaryOp(DspBinaryOp.Pow, (a, b) => a ** b), } - function defineGen(name: keyof Gen, stereo: boolean): any { + function defineGen(name: keyof Gen, stereo: boolean) { const props = getAllProps(name) const Gen = dspGens[name] const kind_index = dspGensKeys.indexOf(name) - function handle(opt: any) { + function handle(opt: Record) { const gen$ = context.gens++ setup_vm.CreateGen(kind_index) DEBUG && console.log('CreateGen', name, gen$) for (let p in opt) { const prop = `${name}.${p}` - const prop$ = props.indexOf(p as any) + const prop$ = props.indexOf(p) DEBUG && console.log('Property', prop, opt[p]) if (prop$ >= 0) { @@ -334,7 +334,7 @@ export function Dsp({ sampleRate, core$ }: { return gen$ } - function processMono(opt: any): Value | void { + function processMono(opt: Record): Value | void { const gen$ = handle(opt) if ('hasAudioOut' in Gen && Gen.hasAudioOut) { @@ -347,7 +347,7 @@ export function Dsp({ sampleRate, core$ }: { } } - function processStereo(opt: any): [Value, Value] | void { + function processStereo(opt: Record): [Value, Value] | void { const gen$ = handle(opt) if ('hasStereoOut' in Gen && Gen.hasStereoOut) { @@ -455,15 +455,15 @@ export function Dsp({ sampleRate, core$ }: { dspGensKeys.map(name => [name, defineGen(name, false)] ) - ) as Gen, + ), gen_st: fromEntries( dspGensKeys.map(name => [name, defineGen(name, true)] ) - ) as Gen - } + ) + } as const - let prevHashId: any + let prevHashId: string function process(tokens: Token[], voicesCount: number, hasMidiIn: boolean, paramsCount: number) { const scope = {} as any @@ -551,11 +551,11 @@ export function Dsp({ sampleRate, core$ }: { const slice = program.scope.stack.slice(-2) if (slice.length === 2) { - R ??= slice.pop() - L ??= slice.pop() + R ??= slice.pop()! + L ??= slice.pop()! } else if (slice.length === 1) { - LR ??= slice.pop() + LR ??= slice.pop()! } const out = { diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts index c7c7389..bac4cb9 100644 --- a/src/lang/interpreter.ts +++ b/src/lang/interpreter.ts @@ -1,10 +1,29 @@ import { getAllPropsReverse, Sound } from 'dsp' import { dspGens } from '~/generated/typescript/dsp-gens.ts' +import type { Value } from '~/src/as/dsp/value.ts' import { Token } from '~/src/lang/tokenize.ts' -import { parseNumber } from '~/src/lang/util.ts' +import { parseNumber, type NumberFormat, type NumberInfo } from '~/src/lang/util.ts' const DEBUG = false +export interface ProgramValue { + results: ProgramValueResult[] + tokensAstNode: Map +} + +export type ProgramValueResult = { result: AstNode } & ({ + genId: string + genData: any +} | { + op: Token + index: AstNode + list: AstNode & { type: AstNode.Type.List } +} | { + op: Token + lhs: AstNode + rhs: AstNode +}) + export class AstNode { constructor( public type: AstNode.Type, @@ -13,10 +32,21 @@ export class AstNode { ) { Object.assign(this, data) } - id: any - kind: any + id?: string + kind?: AstNode.ProcKind scope: Scope = new Scope(null) - value: any + value?: + | { tokens: Token[] } + | Value.Audio + | [Value.Audio, Value.Audio] + | NumberInfo + | string + | number + | ProgramValue + + format?: NumberFormat + digits?: number + get bounds() { return Token.bounds(this.captured) } @@ -50,16 +80,16 @@ export namespace AstNode { class Scope { constructor( public parent: Scope | null, - public vars: Record = {} + public vars: Record = {} ) { } - stack: any[] = [] + stack: AstNode[] = [] stackPop() { return this.stack.pop() } - stackPush(x: {}) { + stackPush(x: AstNode) { this.stack.push(x) } - stackUnshiftOfTypes(types: any[], climb?: boolean) { + stackUnshiftOfTypes(types: AstNode.Type[], climb?: boolean): AstNode | undefined { let s: Scope | null = this let res: any do { @@ -68,7 +98,7 @@ class Scope { if (!climb) return } while (s = s.parent) } - stackPopOfTypes(types: any[], climb?: boolean) { + stackPopOfTypes(types: AstNode.Type[], climb?: boolean): AstNode | undefined { let s: Scope | null = this let res: any do { @@ -131,7 +161,7 @@ const AssignOps = new Set('= += *= -= /= ^='.split(' ')) export function interpret(sound: Sound, data: Record, tokens: Token[]) { const g = sound.api const scope: Scope = new Scope(null, { ...ScopeNatives, ...ScopeSpecial, ...data }) - const results: (Record & { result: AstNode })[] = [] + const results: ProgramValueResult[] = [] const tokensAstNode: Map = new Map() const capturing = new Set() @@ -168,7 +198,7 @@ export function interpret(sound: Sound, data: Record, tokens: Token const res = process(c, scope, next()) if (res) { if (Array.isArray(res)) { - res.reverse().forEach(x => scope.stackPush(x as any)) + res.reverse().forEach(x => scope.stackPush(x)) } else { scope.stackPush(res) @@ -202,7 +232,7 @@ export function interpret(sound: Sound, data: Record, tokens: Token if (proc.kind === AstNode.ProcKind.Gen || proc.kind === AstNode.ProcKind.GenStereo) { const genId = proc.id as keyof typeof dspGens const genInfo = dspGens[genId] - const allProps = getAllPropsReverse(genId) as any + const allProps = getAllPropsReverse(genId) as string[] for (const p of allProps) { DEBUG && console.log(p) @@ -219,7 +249,7 @@ export function interpret(sound: Sound, data: Record, tokens: Token } const genData = Object.fromEntries( - Object.entries(node.scope.vars).map(([key, { value }]: any) => + Object.entries(node.scope.vars).map(([key, { value }]) => [key, value] ) ) @@ -241,8 +271,8 @@ export function interpret(sound: Sound, data: Record, tokens: Token } } else if (proc.kind === AstNode.ProcKind.GenStereo) { - const value: any = g.gen_st[genId](genData) - if (value) { + const value = g.gen_st[genId](genData) + if (value && Array.isArray(value)) { // console.log(genId, value) const result_left = new AstNode(AstNode.Type.Result, { value: value[0] }, node.captured) results.push({ result: result_left, genId, genData }) @@ -254,15 +284,16 @@ export function interpret(sound: Sound, data: Record, tokens: Token } } } - else if (proc.kind === AstNode.ProcKind.User) { - const c = createContext(proc.value.tokens) + else if (proc.kind === AstNode.ProcKind.User && typeof proc.value === 'object' && 'tokens' in proc.value) { + const c = createContext(proc.value?.tokens) const { scope } = c.until(node.scope) return scope.stack.at(-1) } else if (proc.kind === AstNode.ProcKind.Special) { if (proc.id === 'pan') { const value = node.scope.stackUnshiftOfTypes(ConsumeTypes) - g.pan(value.value) + if (value?.value == null) throw new TypeError('Value expected.') + g.pan(value.value as Value) } } } @@ -305,11 +336,11 @@ export function interpret(sound: Sound, data: Record, tokens: Token case Token.Type.Keyword: { if (t.text === '@') { if (scope.stack.length === 1) return scope.stack.pop() - let l: any - let r = scope.stack.pop()?.value + let l: AstNode & { value: Value } + let r: Value = scope.stack.pop()?.value as Value if (r == null) return while (scope.stack.length) { - l = scope.stack.pop() + l = scope.stack.pop() as AstNode & { value: Value } r = g.math.add(l.value, r) } const node = new AstNode(AstNode.Type.Result, { value: r }, [t]) @@ -321,14 +352,14 @@ export function interpret(sound: Sound, data: Record, tokens: Token case Token.Type.Op: if (t.text === '?') { const index = scope.stackPopOfTypes(ConsumeTypes) - const list = scope.stackPopOfTypes([AstNode.Type.List], true) + const list = scope.stackPopOfTypes([AstNode.Type.List], true) as (AstNode & { type: AstNode.Type.List }) | undefined if (!index) { throw new Error('Missing index for pick (?).', { cause: { nodes: [t] } }) } if (!list) { throw new Error('Missing list for pick (?).', { cause: { nodes: [t] } }) } - const value = g.pick(list.scope.stack.map((x: any) => x.value), index.value) + const value = g.pick(list.scope.stack.map(x => x.value as Value), index.value as number | Value) const node = new AstNode(AstNode.Type.Result, { value }, [t]) results.push({ result: node, op: t, index, list }) return node @@ -360,7 +391,7 @@ export function interpret(sound: Sound, data: Record, tokens: Token if (!l) { throw new Error('Missing left operand.', { cause: { nodes: [t] } }) } - scope.vars[r.value] = l + scope.vars[r.value as string] = l return } switch (t.text) { @@ -395,8 +426,8 @@ export function interpret(sound: Sound, data: Record, tokens: Token value: { results, tokensAstNode, - } - }) + } as ProgramValue + }) as AstNode & { value: ProgramValue } } return program() diff --git a/src/pages/DspDemo.tsx b/src/pages/DspDemo.tsx index bc4c765..b72d071 100644 --- a/src/pages/DspDemo.tsx +++ b/src/pages/DspDemo.tsx @@ -2,7 +2,9 @@ import { Dsp, wasm as wasmDsp } from 'dsp' import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' import { Sigui } from 'sigui' import { assign, Lru } from 'utils' +import type { Value } from '~/src/as/dsp/value.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' +import type { AstNode, ProgramValueResult } from '~/src/lang/interpreter.ts' import { Token, tokenize } from '~/src/lang/tokenize.ts' import { Canvas } from '~/src/ui/Canvas.tsx' import type { Editor } from '~/src/ui/Editor.tsx' @@ -86,33 +88,28 @@ export function DspDemo() { c.meshes.draw() let nodeCount = 0 - program.value.results.sort((a: any, b: any) => - a.result.captured[0].line === - b.result.captured[0].line - ? a.result.captured[0].col - - b.result.captured[0].col - : a.result.captured[0].line - - b.result.captured[0].line + program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => + a.line === b.line + ? a.col - b.col + : a.line - b.line ) - let last + let last: AstNode | null = null + const waves = new Map() for (const node of program.value.results) { if ('genId' in node) { - const bounds = Token.bounds(node.result.captured) + const bounds = node.result.bounds if (last && last.bounds.line === bounds.line && last.bounds.right > bounds.col) { last.bounds.right = bounds.col - 1 - last.wave.widget.bounds.right = bounds.col - 1 - // console.log('yes') + waves.get(last)!.widget.bounds.right = bounds.col - 1 } - // console.log(nodeCount, bounds) const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats : getFloatsGfx(`${nodeCount}`, 8192) - wave.info.floats.set(sound.getAudio(node.result.value.ptr)) + wave.info.floats.set(sound.getAudio((node.result.value as Value.Audio).ptr)) assign(wave.widget.bounds, bounds) pane.draw.widgets.deco.add(wave.widget) - node.bounds = bounds - node.wave = wave - last = node + waves.set(node.result, wave) + last = node.result nodeCount++ } } diff --git a/vendor/as-transform-update-dsp-gens.js b/vendor/as-transform-update-dsp-gens.js index 6ea55c4..c9792a0 100644 --- a/vendor/as-transform-update-dsp-gens.js +++ b/vendor/as-transform-update-dsp-gens.js @@ -1,7 +1,7 @@ 'use strict' -import { Transform } from 'assemblyscript/transform' import Binaryen from 'assemblyscript/binaryen' +import { Transform } from 'assemblyscript/transform' import * as fs from 'fs' import * as path from 'path' import * as util from 'util' @@ -48,7 +48,9 @@ class UpdateDspGens extends Transform { } } } + sortObjectInPlace(dspGens) + const audioProps = ['in', 'sidechain'] const textProps = ['text', 'id'] const interfaces = Object.entries(dspGens).map( @@ -56,6 +58,7 @@ class UpdateDspGens extends Transform { ${v.props.map((x, i) => ` ${x}?: ${audioProps.includes(x) ? 'Value.Audio' : textProps.includes(x) ? 'string' : 'Value | number'}`).join('\n')} }` ).join('\n') + function hasAudioOut(k) { const gen = dspGens[k] let res @@ -68,15 +71,32 @@ ${v.props.map((x, i) => ` ${x}?: ${audioProps.includes(x) ? 'Value.Audio' : t } return res } + + function hasStereoOut(k) { + const gen = dspGens[k] + let res + if (gen.hasStereoOut) + res = true + else if (gen.inherits && gen.inherits !== 'gen' && hasStereoOut(gen.inherits)) + res = true + if (res) { + gen.hasStereoOut = true + } + return res + } + const types = Object.keys(dspGens).map( - (k) => ` ${k}: (p: Props.${capitalize(k)}) => ${hasAudioOut(k) ? 'Value.Audio' : 'void'}` + (k) => ` ${k}: (p: Props.${capitalize(k)}) => ${hasStereoOut(k) + ? '[Value.Audio, Value.Audio]' + : hasAudioOut(k) ? 'Value.Audio' : 'void'}` ).join('\n') + const formatted = util.format('%O', dspGens) const date = new Date() const text = /*ts*/`// // auto-generated ${date.toDateString()} ${date.toTimeString()} -import { Value } from '../../src/dsp/value.ts' +import { Value } from '../../src/as/dsp/value.ts' export const dspGens = ${formatted} as const From 1f72ed53324848900fdedf0dc44248b1fa9cf37a Mon Sep 17 00:00:00 2001 From: stagas Date: Wed, 16 Oct 2024 11:21:06 +0300 Subject: [PATCH 06/33] improve number hovering --- src/comp/DspEditor.tsx | 21 +++++++++++++++++++-- src/pages/DspDemo.tsx | 19 +++++++++++++------ src/ui/editor/caret.ts | 2 +- src/ui/editor/constants.ts | 1 + src/ui/editor/draw.ts | 33 ++++++++++++++++----------------- src/ui/editor/index.ts | 1 + src/ui/editor/mouse.ts | 8 ++++---- 7 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 src/ui/editor/constants.ts diff --git a/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx index f14efbf..2925c75 100644 --- a/src/comp/DspEditor.tsx +++ b/src/comp/DspEditor.tsx @@ -1,4 +1,4 @@ -import { pointToLinecol, type Pane, type WordWrapProcessor } from 'editor' +import { CLICK_TIMEOUT, pointToLinecol, type Pane, type WordWrapProcessor } from 'editor' import { Sigui, type Signal } from 'sigui' import { assign, clamp } from 'utils' import { Token, tokenize } from '~/src/lang/tokenize.ts' @@ -30,6 +30,8 @@ export function DspEditor({ code, width, height }: { const hoverMark = HoverMarkWidget() function getHoveringNumber(pane: Pane) { + if (!pane.mouse.info.linecol.hoverLine) return + const word = pane.buffer.wordUnderLinecol(pane.mouse.info.linecol) if (word != null) { digits = word[0].split('.')[1]?.length ?? 0 @@ -44,6 +46,7 @@ export function DspEditor({ code, width, height }: { } function updateNumberMark(pane: Pane) { + if (pane.mouse.info.isDown) return const temp = getHoveringNumber(pane) if (temp && !pane.info.isHoveringScrollbarX && !pane.info.isHoveringScrollbarY) { const number = temp @@ -161,9 +164,23 @@ export function DspEditor({ code, width, height }: { return ret } + let clickTimeout: any + let clickCount = 0 + function onMouseDown(pane: Pane) { + clickCount++ + + clearTimeout(clickTimeout) + clickTimeout = setTimeout(() => { + clickCount = 0 + }, CLICK_TIMEOUT) + + if (clickCount >= 2) return + number = getHoveringNumber(pane) + if (number?.index == null) return + value = parseFloat(number[0]) mouse.x = pane.mouse.info.x mouse.y = pane.mouse.info.y @@ -185,7 +202,7 @@ export function DspEditor({ code, width, height }: { } function onMouseUp(pane: Pane) { - if (number) { + if (number && clickCount <= 1) { number = void 0 return true } diff --git a/src/pages/DspDemo.tsx b/src/pages/DspDemo.tsx index b72d071..c00ed5a 100644 --- a/src/pages/DspDemo.tsx +++ b/src/pages/DspDemo.tsx @@ -4,8 +4,8 @@ import { Sigui } from 'sigui' import { assign, Lru } from 'utils' import type { Value } from '~/src/as/dsp/value.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' -import type { AstNode, ProgramValueResult } from '~/src/lang/interpreter.ts' -import { Token, tokenize } from '~/src/lang/tokenize.ts' +import type { AstNode } from '~/src/lang/interpreter.ts' +import { tokenize } from '~/src/lang/tokenize.ts' import { Canvas } from '~/src/ui/Canvas.tsx' import type { Editor } from '~/src/ui/Editor.tsx' import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' @@ -22,7 +22,10 @@ export function DspDemo() { const info = $({ width: 400, height: 300, - code: `[sin 40.10 303 [exp 2.01] 5.20^ * +] [exp 1.00] 9.99^ *`, + code: `[sin 42.11 303 +[exp 2.01] 6.66^ * +] +[exp 1.00] 9.99^ * +`, floats: new Float32Array(), }) @@ -80,11 +83,11 @@ export function DspDemo() { floats.ptr, ) - // pane.draw.widgets.update() plot.info.floats.set(floats) - requestAnimationFrame($.fn(() => { + + queueMicrotask($.fn(() => { c.meshes.draw() let nodeCount = 0 @@ -96,6 +99,7 @@ export function DspDemo() { let last: AstNode | null = null const waves = new Map() + pane.draw.widgets.deco.clear() for (const node of program.value.results) { if ('genId' in node) { const bounds = node.result.bounds @@ -104,7 +108,9 @@ export function DspDemo() { waves.get(last)!.widget.bounds.right = bounds.col - 1 } const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) - wave.info.floats = wave.info.floats.length ? wave.info.floats : getFloatsGfx(`${nodeCount}`, 8192) + wave.info.floats = wave.info.floats.length + ? wave.info.floats + : getFloatsGfx(`${nodeCount}`, 8192) wave.info.floats.set(sound.getAudio((node.result.value as Value.Audio).ptr)) assign(wave.widget.bounds, bounds) pane.draw.widgets.deco.add(wave.widget) @@ -119,6 +125,7 @@ export function DspDemo() { })) }) }) + const dspEditor = top top += lineHeight + if (l?.subs) top += l.subs + 2 } - y = linesVisual.length - 1 + ty = linesVisual.length - 1 + hoverLine = false } - const line = clamp(0, linesVisual.length - 1, y) - const col = clamp(0, linesVisual[y]?.text.length ?? 0, Math.round((x - 3) / charWidth)) + const line = clamp(0, linesVisual.length - 1, ty) + const col = clamp(0, linesVisual[ty]?.text.length ?? 0, Math.round((x - 3) / charWidth)) - return { line, col } + return { line, col, hoverLine } } // update token draw info @@ -227,15 +229,11 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize // update caret view point $.fx(() => { + const { triggerUpdateTokenDrawInfo } = info const { tokens } = buffer.info const { x, y } = caret.visual $() - // NOTE: bugfix while toggling block comments caret wasn't updated. - // there must be a real solution to this but for now this works. - assign(caretViewPoint, viewPointFromLinecol({ line: y, col: x })) - queueMicrotask(() => { - assign(caretViewPoint, viewPointFromLinecol({ line: y, col: x })) - }) + assign(caretViewPoint, viewPointFromLinecol(pointToLinecol(caret.visual))) }) // wait for fonts to load @@ -250,6 +248,7 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize // update inner size $.fx(() => { + const { triggerUpdateTokenDrawInfo } = info const { w, h } = info.rect const { linesVisual } = buffer.info const { charWidth, lineHeight, innerSize } = dims.info diff --git a/src/ui/editor/index.ts b/src/ui/editor/index.ts index a0c8ede..c42ff48 100644 --- a/src/ui/editor/index.ts +++ b/src/ui/editor/index.ts @@ -1,5 +1,6 @@ export * from './buffer.ts' export * from './caret.ts' +export * from './constants.ts' export * from './dims.ts' export * from './draw.ts' export * from './history.ts' diff --git a/src/ui/editor/mouse.ts b/src/ui/editor/mouse.ts index 229ebc3..bf208b1 100644 --- a/src/ui/editor/mouse.ts +++ b/src/ui/editor/mouse.ts @@ -1,8 +1,7 @@ import { Linecol, Point, type Caret, type Dims, type Draw, type PaneInfo, type Selection } from 'editor' import { Sigui } from 'sigui' import { assign, MouseButtons } from 'utils' - -const CLICK_TIMEOUT = 350 +import { CLICK_TIMEOUT } from '~/src/ui/editor/constants.ts' export function Mouse({ paneInfo, dims, selection, caret, draw }: { paneInfo: PaneInfo @@ -21,7 +20,7 @@ export function Mouse({ paneInfo, dims, selection, caret, draw }: { x: 0, y: 0, actual: $(Point()), - linecol: $(Linecol()), + linecol: $({ ...Linecol(), hoverLine: false }), wheel: $(Point()), ctrl: false, }) @@ -29,7 +28,8 @@ export function Mouse({ paneInfo, dims, selection, caret, draw }: { $.fx(() => { const { x, y } = info $() - assign(info.linecol, draw.linecolFromViewPoint(info)) + const res = draw.linecolFromViewPoint(info) + assign(info.linecol, res) }) // blink caret From 430d19701d63ebff9ddd2821871d46a793f3db1a Mon Sep 17 00:00:00 2001 From: stagas Date: Wed, 16 Oct 2024 15:15:18 +0300 Subject: [PATCH 07/33] dsp editor working --- src/as/dsp/dsp.ts | 12 +++-- src/comp/DspEditor.tsx | 30 +++++++---- src/pages/DspDemo.tsx | 88 ++++++++++++++++++-------------- src/ui/editor/buffer.ts | 6 +-- src/ui/editor/caret.ts | 7 ++- src/ui/editor/draw.ts | 6 +-- src/ui/editor/kbd.tsx | 2 +- src/ui/editor/pane.ts | 2 +- src/ui/editor/view.tsx | 2 +- src/ui/editor/widgets/wave-gl.ts | 2 +- 10 files changed, 93 insertions(+), 64 deletions(-) diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index 3d7f96f..98287eb 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -52,7 +52,8 @@ function copyToken(token: Token) { return newToken } -let preludeTokens: Token[] +let preTokens: Token[] +let postTokens: Token[] export function Dsp({ sampleRate, core$ }: { sampleRate: number @@ -68,7 +69,7 @@ export function Dsp({ sampleRate, core$ }: { const view = getMemoryView(wasm.memory) - preludeTokens ??= [...tokenize({ + preTokens ??= [...tokenize({ code: // we implicit call [nrate 1] before our code // so that the sample rate is reset. @@ -80,6 +81,10 @@ export function Dsp({ sampleRate, core$ }: { // + ` { t= p= sp= 1 [inc sp co* t] clip - p^ } down= ` })] + postTokens ??= [...tokenize({ + code: `@` + })] + function Sound() { const sound$ = wasm.createSound(engine$) @@ -470,8 +475,9 @@ export function Dsp({ sampleRate, core$ }: { const literals: AstNode[] = [] let tokensCopy = [ - ...preludeTokens, + ...preTokens, ...tokens, + ...postTokens, ] .filter(t => t.type !== Token.Type.Comment) .map(copyToken) diff --git a/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx index 2925c75..ff97ba0 100644 --- a/src/comp/DspEditor.tsx +++ b/src/comp/DspEditor.tsx @@ -217,16 +217,28 @@ export function DspEditor({ code, width, height }: { onMouseMove, } + const baseColors = { + [Token.Type.Native]: theme.colors.sky, + [Token.Type.String]: theme.colors.sky, + [Token.Type.Keyword]: theme.colors.sky, + [Token.Type.Op]: theme.colors.neutral, + [Token.Type.Id]: theme.colors.neutral, + [Token.Type.Number]: theme.colors.neutral, + [Token.Type.BlockComment]: theme.colors.sky, + [Token.Type.Comment]: theme.colors.sky, + [Token.Type.Any]: theme.colors.sky, + } + const colors: Partial> = { - [Token.Type.Native]: { fill: theme.colors.sky[500], stroke: theme.colors.sky[500] }, - [Token.Type.String]: { fill: theme.colors.fuchsia[700], stroke: theme.colors.fuchsia[700] }, - [Token.Type.Keyword]: { fill: theme.colors.orange[500], stroke: theme.colors.orange[500] }, - [Token.Type.Op]: { fill: theme.colors.sky[500], stroke: theme.colors.sky[500] }, - [Token.Type.Id]: { fill: theme.colors.yellow[500], stroke: theme.colors.yellow[500] }, - [Token.Type.Number]: { fill: theme.colors.green[500], stroke: theme.colors.green[500] }, - [Token.Type.BlockComment]: { fill: theme.colors.neutral[700], stroke: theme.colors.neutral[700] }, - [Token.Type.Comment]: { fill: theme.colors.neutral[700], stroke: theme.colors.neutral[700] }, - [Token.Type.Any]: { fill: theme.colors.neutral[500], stroke: theme.colors.neutral[500] }, + [Token.Type.Native]: { fill: baseColors[Token.Type.Native][500], stroke: baseColors[Token.Type.Native][500] }, + [Token.Type.String]: { fill: baseColors[Token.Type.String][700], stroke: baseColors[Token.Type.String][700] }, + [Token.Type.Keyword]: { fill: baseColors[Token.Type.Keyword][500], stroke: baseColors[Token.Type.Keyword][500] }, + [Token.Type.Op]: { fill: baseColors[Token.Type.Op][500], stroke: baseColors[Token.Type.Op][500] }, + [Token.Type.Id]: { fill: baseColors[Token.Type.Id][400], stroke: baseColors[Token.Type.Id][400] }, + [Token.Type.Number]: { fill: baseColors[Token.Type.Number][100], stroke: baseColors[Token.Type.Number][100] }, + [Token.Type.BlockComment]: { fill: baseColors[Token.Type.BlockComment][700], stroke: baseColors[Token.Type.BlockComment][700] }, + [Token.Type.Comment]: { fill: baseColors[Token.Type.Comment][700], stroke: baseColors[Token.Type.Comment][700] }, + [Token.Type.Any]: { fill: baseColors[Token.Type.Any][500], stroke: baseColors[Token.Type.Any][500] }, } function colorize(token: Token) { diff --git a/src/pages/DspDemo.tsx b/src/pages/DspDemo.tsx index c00ed5a..aa82464 100644 --- a/src/pages/DspDemo.tsx +++ b/src/pages/DspDemo.tsx @@ -27,6 +27,7 @@ export function DspDemo() { [exp 1.00] 9.99^ * `, floats: new Float32Array(), + error: null as Error | null, }) const ctx = new AudioContext({ sampleRate: 48000 }) @@ -52,44 +53,41 @@ export function DspDemo() { const waveWidgets: WaveGlWidget[] = [] - requestAnimationFrame(() => { + queueMicrotask(() => { $.fx(() => { const { pane } = dspEditor.info const { codeVisual } = pane.buffer.info $() - - sound.reset() - const tokens = Array.from(tokenize({ code: codeVisual })) - const { program, out } = sound.process(tokens, 6, false, 0) - if (!out.LR) { - throw new Error('No audio in the stack.') - } - const floats = getFloats('floats', length) - info.floats = floats - const notes = [] - const params = [] - const notesData = getBuffer(notes.length * 4) - const paramsData = getPointers(params.length * 2) - wasmDsp.fillSound( - sound.sound$, - sound.ops.ptr, - notesData.ptr, - notes.length, - paramsData.ptr, - params.length, - out.LR.getAudio(), - 0, - floats.length, - floats.ptr, - ) - - // pane.draw.widgets.update() - - plot.info.floats.set(floats) - - queueMicrotask($.fn(() => { - c.meshes.draw() - let nodeCount = 0 + pane.draw.widgets.deco.clear() + plot.info.floats.fill(0) + + let nodeCount = 0 + try { + const tokens = Array.from(tokenize({ code: codeVisual })) + sound.reset() + const { program, out } = sound.process(tokens, 6, false, 0) + if (!out.LR) { + throw new Error('No audio in the stack!') + } + const floats = getFloats('floats', length) + info.floats = floats + const notes = [] + const params = [] + const notesData = getBuffer(notes.length * 4) + const paramsData = getPointers(params.length * 2) + wasmDsp.fillSound( + sound.sound$, + sound.ops.ptr, + notesData.ptr, + notes.length, + paramsData.ptr, + params.length, + out.LR.getAudio(), + 0, + floats.length, + floats.ptr, + ) + plot.info.floats.set(floats) program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => a.line === b.line @@ -99,7 +97,7 @@ export function DspDemo() { let last: AstNode | null = null const waves = new Map() - pane.draw.widgets.deco.clear() + for (const node of program.value.results) { if ('genId' in node) { const bounds = node.result.bounds @@ -119,10 +117,24 @@ export function DspDemo() { nodeCount++ } } + } + catch (err) { + if (err instanceof Error) { + info.error = err + } + else { + throw err + } + } + + let delta = waveWidgets.length - nodeCount + while (delta-- > 0) waveWidgets.pop()?.dispose() + + pane.draw.info.triggerUpdateTokenDrawInfo++ - pane.draw.info.triggerUpdateTokenDrawInfo++ - pane.view.anim.info.epoch++ - })) + c.meshes.draw() + pane.view.anim.info.epoch++ + pane.draw.widgets.update() }) }) diff --git a/src/ui/editor/buffer.ts b/src/ui/editor/buffer.ts index 09dc3e0..a9a0e84 100644 --- a/src/ui/editor/buffer.ts +++ b/src/ui/editor/buffer.ts @@ -129,9 +129,9 @@ export function Buffer({ dims, code, tokenize, wordWrapProcessor = { pre: identi x++ } - if (line.length || word.length || word.length === code.length) { - push() - } + // if (line.length || word.length || word.length === code.length) { + push() + // } return wrapped.map(line => { line.text = wordWrapProcessor.post(line.text) diff --git a/src/ui/editor/caret.ts b/src/ui/editor/caret.ts index da31295..643ccad 100644 --- a/src/ui/editor/caret.ts +++ b/src/ui/editor/caret.ts @@ -4,8 +4,7 @@ import { assign, clamp } from 'utils' export type Caret = ReturnType -export function Caret({ paneInfo, buffer }: { - paneInfo: PaneInfo, +export function Caret({ buffer }: { buffer: Buffer, }) { using $ = Sigui() @@ -84,7 +83,7 @@ export function Caret({ paneInfo, buffer }: { }) } - function moveUpDown(dy: number) { + function moveByLines(dy: number) { const { linesVisual } = buffer.info let newX = caret.visualXIntent let newY = caret.visual.y + dy @@ -146,7 +145,7 @@ export function Caret({ paneInfo, buffer }: { doDelete, moveHome, moveEnd, - moveUpDown, + moveByLines, moveByChars, moveByWord, insert, diff --git a/src/ui/editor/draw.ts b/src/ui/editor/draw.ts index 01dff0f..fe39aad 100644 --- a/src/ui/editor/draw.ts +++ b/src/ui/editor/draw.ts @@ -353,9 +353,9 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize c.clearRect(x, y, w + 1, h + 1) // TODO: temp remove this - c.translate(x, y) - c.fillStyle = '#' + color - c.fillRect(0, 0, w, h) + // c.translate(x, y) + // c.fillStyle = '#' + color + // c.fillRect(0, 0, w, h) c.translate(dims.info.scrollX, dims.info.scrollY) } diff --git a/src/ui/editor/kbd.tsx b/src/ui/editor/kbd.tsx index 97fae09..9d83974 100644 --- a/src/ui/editor/kbd.tsx +++ b/src/ui/editor/kbd.tsx @@ -454,7 +454,7 @@ export function Kbd({ paneInfo, misc, dims, selection, buffer, caret, history }: ) } withSelection(() => - caret.moveUpDown(dy) + caret.moveByLines(dy) ) } diff --git a/src/ui/editor/pane.ts b/src/ui/editor/pane.ts index 146473d..1abc42e 100644 --- a/src/ui/editor/pane.ts +++ b/src/ui/editor/pane.ts @@ -37,7 +37,7 @@ export function Pane({ misc, view, code, rect, colorize, tokenize, wordWrapProce const dims = Dims({ rect }) const buffer = Buffer({ dims, code, tokenize, wordWrapProcessor }) - const caret = Caret({ paneInfo: info, buffer }) + const caret = Caret({ buffer }) const selection = Selection({ buffer, caret }) const history = History({ selection, buffer, caret }) const kbd = Kbd({ paneInfo: info, misc, dims, selection, buffer, caret, history }) diff --git a/src/ui/editor/view.tsx b/src/ui/editor/view.tsx index 76ca78c..c28c72c 100644 --- a/src/ui/editor/view.tsx +++ b/src/ui/editor/view.tsx @@ -19,7 +19,7 @@ export function View({ width, height }: { svgs: new Set(), }) - const canvas = as HTMLCanvasElement + const canvas = as HTMLCanvasElement const glCanvas = as HTMLCanvasElement diff --git a/src/ui/editor/widgets/wave-gl.ts b/src/ui/editor/widgets/wave-gl.ts index 90d27d6..2f879eb 100644 --- a/src/ui/editor/widgets/wave-gl.ts +++ b/src/ui/editor/widgets/wave-gl.ts @@ -10,7 +10,7 @@ export function WaveGlWidget(shapes: Shapes) { const info = $({ floats: wasm.alloc(Float32Array, 0), - color: '#f09', + color: '#fff', }) const widget = Widget() From d67c5afd7801eab11475b92ac92b21c4182b65eb Mon Sep 17 00:00:00 2001 From: stagas Date: Sat, 19 Oct 2024 06:10:58 +0300 Subject: [PATCH 08/33] wip dsp working --- as/assembly/dsp/gen/atan.ts | 1 - as/assembly/dsp/gen/osc.ts | 2 +- as/assembly/dsp/gen/tanh.ts | 1 - as/assembly/dsp/gen/tanha.ts | 5 +- as/assembly/dsp/index.ts | 80 +++--- as/assembly/dsp/shared.ts | 17 ++ as/assembly/dsp/vm/sound.ts | 162 ++--------- as/assembly/dsp/vm/template.ts | 18 -- as/assembly/player/player.ts | 39 +++ asconfig-dsp-nort.json | 37 +++ asconfig-dsp.json | 4 +- generated/assembly/dsp-runner.ts | 31 +-- generated/typescript/dsp-gens.ts | 11 +- scripts/generate-dsp-vm.ts | 5 +- src/as/dsp/dsp-service.ts | 36 --- src/as/dsp/dsp-shared.ts | 21 -- src/as/dsp/dsp-worker.ts | 158 ----------- src/as/dsp/dsp.ts | 163 ++++------- src/as/dsp/index.ts | 2 + src/as/dsp/preview-service.ts | 41 +++ src/as/dsp/preview-worker.ts | 140 ++++++++++ src/as/dsp/shared.ts | 47 ++++ src/as/init-wasm.ts | 2 +- src/as/pkg/player.ts | 2 +- src/as/pkg/worklet.ts | 2 +- src/comp/DspEditor.tsx | 26 +- src/lang/interpreter.ts | 8 +- src/lang/tokenize.ts | 4 + src/pages/App.tsx | 10 +- src/pages/DspAsyncDemo.tsx | 132 +++++++++ src/pages/DspNodeDemo.tsx_ | 151 +++++++++++ src/pages/DspNodeDemo/DspNodeDemo.tsx | 196 ++++++++++++++ src/pages/DspNodeDemo/basic-processor.ts | 53 ++++ src/pages/DspNodeDemo/constants.ts | 4 + src/pages/DspNodeDemo/dsp-worker.ts | 86 ++++++ src/pages/DspNodeDemo/free-queue.ts | 254 ++++++++++++++++++ src/pages/{DspDemo.tsx => DspSyncDemo.tsx} | 41 ++- src/pages/Home.tsx | 5 +- src/pages/WorkerWorklet/WorkerWorkletDemo.tsx | 58 ++++ src/pages/WorkerWorklet/basic-processor.ts | 53 ++++ src/pages/WorkerWorklet/constants.ts | 4 + src/pages/WorkerWorklet/free-queue.ts | 254 ++++++++++++++++++ src/pages/WorkerWorklet/worker.ts | 57 ++++ src/ui/editor/caret.ts | 12 +- src/ui/editor/kbd.tsx | 8 +- src/ui/editor/widgets/error-sub.ts | 31 +++ src/ui/editor/widgets/index.ts | 1 + vite.config.ts | 10 + 48 files changed, 1903 insertions(+), 582 deletions(-) create mode 100644 as/assembly/dsp/shared.ts delete mode 100644 as/assembly/dsp/vm/template.ts create mode 100644 as/assembly/player/player.ts create mode 100644 asconfig-dsp-nort.json delete mode 100644 src/as/dsp/dsp-service.ts delete mode 100644 src/as/dsp/dsp-shared.ts delete mode 100644 src/as/dsp/dsp-worker.ts create mode 100644 src/as/dsp/preview-service.ts create mode 100644 src/as/dsp/preview-worker.ts create mode 100644 src/as/dsp/shared.ts create mode 100644 src/pages/DspAsyncDemo.tsx create mode 100644 src/pages/DspNodeDemo.tsx_ create mode 100644 src/pages/DspNodeDemo/DspNodeDemo.tsx create mode 100644 src/pages/DspNodeDemo/basic-processor.ts create mode 100644 src/pages/DspNodeDemo/constants.ts create mode 100644 src/pages/DspNodeDemo/dsp-worker.ts create mode 100644 src/pages/DspNodeDemo/free-queue.ts rename src/pages/{DspDemo.tsx => DspSyncDemo.tsx} (83%) create mode 100644 src/pages/WorkerWorklet/WorkerWorkletDemo.tsx create mode 100644 src/pages/WorkerWorklet/basic-processor.ts create mode 100644 src/pages/WorkerWorklet/constants.ts create mode 100644 src/pages/WorkerWorklet/free-queue.ts create mode 100644 src/pages/WorkerWorklet/worker.ts create mode 100644 src/ui/editor/widgets/error-sub.ts diff --git a/as/assembly/dsp/gen/atan.ts b/as/assembly/dsp/gen/atan.ts index 781ca7e..76cb27a 100644 --- a/as/assembly/dsp/gen/atan.ts +++ b/as/assembly/dsp/gen/atan.ts @@ -1,7 +1,6 @@ import { Gen } from './gen' export class Atan extends Gen { - gain: f32 = 1.0; in: u32 = 0 _audio(begin: u32, end: u32, out: usize): void { diff --git a/as/assembly/dsp/gen/osc.ts b/as/assembly/dsp/gen/osc.ts index de9ecd8..57492c1 100644 --- a/as/assembly/dsp/gen/osc.ts +++ b/as/assembly/dsp/gen/osc.ts @@ -2,7 +2,7 @@ import { Gen } from './gen' export abstract class Osc extends Gen { /** Frequency. */ - hz: f32 = 440 + hz: f32 = 0 /** Trigger phase sync when set to 0. */ trig: f32 = -1.0 /** Phase offset. */ diff --git a/as/assembly/dsp/gen/tanh.ts b/as/assembly/dsp/gen/tanh.ts index 4e1cefc..af85d34 100644 --- a/as/assembly/dsp/gen/tanh.ts +++ b/as/assembly/dsp/gen/tanh.ts @@ -1,7 +1,6 @@ import { Gen } from './gen' export class Tanh extends Gen { - gain: f32 = 1.0; in: u32 = 0 _audio(begin: u32, end: u32, out: usize): void { diff --git a/as/assembly/dsp/gen/tanha.ts b/as/assembly/dsp/gen/tanha.ts index b743df1..e368f04 100644 --- a/as/assembly/dsp/gen/tanha.ts +++ b/as/assembly/dsp/gen/tanha.ts @@ -1,11 +1,10 @@ import { Gen } from './gen' export class Tanha extends Gen { - gain: f32 = 1 - _gainv: v128 = f32x4.splat(1.0); - in: u32 = 0 + _gainv: v128 = f32x4.splat(1.0) + _update(): void { this._gainv = f32x4.splat(this.gain) } diff --git a/as/assembly/dsp/index.ts b/as/assembly/dsp/index.ts index 9b4f56c..7d59b55 100644 --- a/as/assembly/dsp/index.ts +++ b/as/assembly/dsp/index.ts @@ -1,5 +1,7 @@ +import { Player } from '../player/player' import { Clock } from './core/clock' import { Core, Engine } from './core/engine' +import { Out, PlayerTrack } from './shared' import { Sound } from './vm/sound' export * from '../../../generated/assembly/dsp-factory' @@ -10,12 +12,12 @@ export function createCore(sampleRate: u32): Core { return new Core(sampleRate) } -export function createEngine(sampleRate: u32, core: Core): Engine { - return new Engine(sampleRate, core) +export function createEngine(sampleRate: u32, core: Core): usize { + return changetype(new Engine(sampleRate, core)) } -export function getEngineClock(engine: Engine): usize { - return changetype(engine.clock) +export function getEngineClock(engine$: usize): usize { + return changetype(changetype(engine$).clock) } export function resetClock(clock$: usize): void { @@ -28,36 +30,29 @@ export function updateClock(clock$: usize): void { clock.update() } -export function createSound(engine: Engine): Sound { - return new Sound(engine) +export function createSound(engine$: usize): usize { + return changetype(new Sound(changetype(engine$))) } -export function resetSound(sound: Sound): void { - sound.reset() +export function resetSound(sound$: usize): void { + changetype(sound$).reset() } -export function clearSound(sound: Sound): void { - sound.clear() +export function clearSound(sound$: usize): void { + changetype(sound$).clear() } export function fillSound( - sound: Sound, + sound$: usize, ops$: usize, - notes$: usize, - notesCount: u32, - params$: usize, - paramsCount: u32, - audio_LR$: usize, + audio_LR$: i32, begin: u32, end: u32, out$: usize ): void { + const sound = changetype(sound$) sound.fill( ops$, - notes$, - notesCount, - params$, - paramsCount, audio_LR$, begin, end, @@ -65,26 +60,47 @@ export function fillSound( ) } -export function getSoundData(sound: Sound): usize { - return changetype(sound.data) +export function getSoundAudio(sound$: usize, index: i32): usize { + return changetype(changetype(sound$).audios[index]) } -export function getSoundAudio(sound: Sound, index: i32): usize { - return changetype(sound.audios[index]) +export function getSoundLiterals(sound$: usize): usize { + return changetype(changetype(sound$).literals) } -export function getSoundLiterals(sound: Sound): usize { - return changetype(sound.literals) +export function setSoundLiterals(sound$: usize, literals$: usize): void { + changetype(sound$).literals = changetype>(literals$) } -export function setSoundLiterals(sound: Sound, literals$: usize): void { - sound.literals = changetype>(literals$) +export function getSoundScalars(sound$: usize): usize { + return changetype(changetype(sound$).scalars) } -export function getSoundScalars(sound: Sound): usize { - return changetype(sound.scalars) +export function getSoundLists(sound$: usize): usize { + return changetype(changetype(sound$).lists) } -export function getSoundLists(sound: Sound): usize { - return changetype(sound.lists) +export function createOut(): usize { + return changetype(new Out()) +} + +export function createPlayer(): usize { + return changetype(new Player()) +} + +export function getPlayerOut(player$: usize): usize { + return changetype(changetype(player$).out) +} + +export function getPlayerTracks(player$: usize): usize { + return changetype(changetype(player$).tracks) +} + +export function createPlayerTrack(): usize { + return changetype(new PlayerTrack()) +} + +export function playerProcess(player$: usize, begin: u32, end: u32): void { + const player = changetype(player$) + player.process(begin, end) } diff --git a/as/assembly/dsp/shared.ts b/as/assembly/dsp/shared.ts new file mode 100644 index 0000000..f12ae12 --- /dev/null +++ b/as/assembly/dsp/shared.ts @@ -0,0 +1,17 @@ +@unmanaged +export class PlayerTrack { + sound$: usize = 0 + ops$: usize = 0 + notes$: usize = 0 + notesCount: u32 = 0 + params$: usize = 0 + paramsCount: u32 = 0 + audio_LR$: i32 = 0 + out$: usize = 0 +} + +@unmanaged +export class Out { + L$: usize = 0 + R$: usize = 0 +} diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index e74dcac..060a50e 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -1,11 +1,9 @@ import { run as dspRun } from '../../../../generated/assembly/dsp-runner' -import { Note, ParamValue } from '../../gfx/sketch-shared' -import { clamp } from '../../util' import { BUFFER_SIZE, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_SCALARS } from '../constants' import { Clock } from '../core/clock' import { Engine } from '../core/engine' import { Gen } from '../gen/gen' -import { SoundData, SoundValueKind } from './dsp-shared' +import { SoundValueKind } from './dsp-shared' export function ntof(n: f32): f32 { return 440 * 2 ** ((n - 69) / 12) @@ -23,32 +21,18 @@ export class SoundValue { export class Sound { constructor(public engine: Engine) { } - data: SoundData = new SoundData() - - get begin(): u32 { - return this.data.begin - } - - get end(): u32 { - return this.data.end - } - - set pan(v: f32) { - this.data.pan = v - } - - get pan(): f32 { - return this.data.pan - } + begin: u32 = 0 + end: u32 = 0 + pan: f32 = 0 gens: Gen[] = [] offsets: usize[][] = [] - literals: StaticArray = new StaticArray(MAX_LITERALS) - scalars: StaticArray = new StaticArray(MAX_SCALARS) audios: Array | null> = [] - lists: StaticArray = new StaticArray(MAX_LISTS) floats: StaticArray = new StaticArray(MAX_FLOATS) + lists: StaticArray = new StaticArray(MAX_LISTS) + literals: StaticArray = new StaticArray(MAX_LITERALS) + scalars: StaticArray = new StaticArray(MAX_SCALARS) values: SoundValue[] = [] @@ -73,87 +57,7 @@ export class Sound { this.scalars[Globals.rt] = f32(c.time) } - // TODO: this needs to be updated to handle - // sustained notes which have to keep track - // which nY scalar we're using, e.g - // n0 n1 n2 are pressed together, - // n0 n1 are released, n2 should be filled until it is released - // we need release/note off time. - @inline - updateVoices(notes$: usize, count: i32, start: f32, end: f32): void { - let y = 0 - for (let i = 0; i < count; i++) { - const note = changetype(notes$ + ((i * 4) << 2)) - if (note.time >= start && note.time < end) { - const voice = voices[y++] - this.scalars[voice[Voice.n]] = note.n - this.scalars[voice[Voice.f]] = ntof(note.n) - this.scalars[voice[Voice.t]] = note.time - this.scalars[voice[Voice.v]] = note.vel - if (y === 6) return - } - } - } - - @inline - updateParams(params$: usize, count: i32, start: f32, end: f32): void { - const params = changetype>(params$) - let y = 0 - for (let i = 0; i < count; i++) { - const ptr = unchecked(params[(i * 2)]) - const len = i32(unchecked(params[(i * 2) + 1])) - - let a: ParamValue | null = null - let b: ParamValue | null = null - for (let j = 0; j < len; j++) { - const v = changetype(ptr + ((j * 4) << 2)) - if (!a) a = v - else a = b - b = v - if (v.time >= end) break - } - - if (a) { - const param = Params.p0 + y - y++ - let amt = f32(1.0) - if (b) { - if (a.time === b.time) { - amt = b.amt - } - else { - const diff: f32 = Mathf.max(0, start - a.time) - const width: f32 = b.time - a.time - const alpha = Mathf.min(width, diff) / width - amt = clamp(-1.0, 1.0, 0.0, a.amt + (b.amt - a.amt) * alpha) - // logf2(start, amt) - } - } - this.scalars[param] = amt - // logf(6667) - } - // if (v.time >= start && v.time < end) { - // const param = params[y++] - // this.scalars[param[Param.t]] = v.time - // this.scalars[param[Param.l]] = v.length - // this.scalars[param[Param.s]] = v.slope - // this.scalars[param[Param.a]] = v.amt - // if (y === 6) return - // } - } - } - - fill( - ops$: usize, - notes$: usize, - notesCount: u32, - params$: usize, - paramsCount: u32, - audio_LR$: i32, - begin: u32, - end: u32, - out$: usize - ): void { + fill(ops$: usize, audio_LR$: i32, begin: u32, end: u32, out$: usize): void { const CHUNK_SIZE = 64 let chunkCount = 0 @@ -161,47 +65,39 @@ export class Sound { this.scalars[Globals.sr] = f32(c.sampleRate) this.scalars[Globals.co] = f32(c.coeff) - let timeStart: f32 - let timeEnd: f32 - - this.updateScalars(c) - timeStart = f32(c.barTime / NOTE_SCALE_X) - timeEnd = f32(c.barTime + c.barTimeStep * f64(CHUNK_SIZE)) - this.updateVoices(notes$, notesCount, timeStart, timeEnd) - this.updateParams(params$, paramsCount, timeStart, timeEnd) - let i = begin - const data = this.data - data.begin = i - data.end = i - dspRun(this, ops$) + this.begin = i + this.end = i + dspRun(changetype(this), ops$) + let time = c.time + let barTime = c.barTime for (let x = i; x < end; x += BUFFER_SIZE) { const chunkEnd = x + BUFFER_SIZE > end ? end - x : BUFFER_SIZE for (let i: u32 = 0; i < chunkEnd; i += CHUNK_SIZE) { this.updateScalars(c) - timeStart = f32(c.barTime * NOTE_SCALE_X) - timeEnd = f32((c.barTime + c.barTimeStep * f64(CHUNK_SIZE)) * NOTE_SCALE_X) - this.updateVoices(notes$, notesCount, timeStart, timeEnd) - this.updateParams(params$, paramsCount, timeStart, timeEnd) - data.begin = i - data.end = i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE - dspRun(this, ops$) + this.begin = i + this.end = i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE + dspRun(changetype(this), ops$) chunkCount++ - c.time = f64(chunkCount * CHUNK_SIZE) * c.timeStep - c.barTime = f64(chunkCount * CHUNK_SIZE) * c.barTimeStep + c.time = time + f64(chunkCount * CHUNK_SIZE) * c.timeStep + c.barTime = barTime + f64(chunkCount * CHUNK_SIZE) * c.barTimeStep + } + time = c.time + barTime = c.barTime + + if (audio_LR$ < this.audios.length) { + const audio = this.audios[audio_LR$] + memory.copy( + out$ + (x << 2), + changetype(audio) + (x << 2), + chunkEnd << 2 + ) } - - const audio = this.audios[audio_LR$] - memory.copy( - out$ + (x << 2), - changetype(audio), - chunkEnd << 2 - ) } } } diff --git a/as/assembly/dsp/vm/template.ts b/as/assembly/dsp/vm/template.ts deleted file mode 100644 index ffb118c..0000000 --- a/as/assembly/dsp/vm/template.ts +++ /dev/null @@ -1,18 +0,0 @@ -class Vm { - // - run(ops: StaticArray): void { - // - let i: i32 = 0 - let op: i32 - while (op = ops[i++]) { - switch (op) { - // - } // end switch - } // end while - } -} - -export function createVm(): Vm { - const vm: Vm = new Vm() - return vm -} diff --git a/as/assembly/player/player.ts b/as/assembly/player/player.ts new file mode 100644 index 0000000..f869270 --- /dev/null +++ b/as/assembly/player/player.ts @@ -0,0 +1,39 @@ +import { MAX_TRACKS } from '../dsp/constants' +import { add_audio_audio } from '../dsp/graph/math' +import { Out, PlayerTrack } from '../dsp/shared' +import { Sound } from '../dsp/vm/sound' + +export class Player { + constructor() { } + tracks: StaticArray = new StaticArray(MAX_TRACKS) + out: Out = new Out() + process(begin: u32, end: u32): void { + let sound: Sound + let track: PlayerTrack | null + for (let i = 0; i < this.tracks.length; i++) { + track = this.tracks[i] + if (track === null) break + sound = changetype(track.sound$) + sound.fill( + track.ops$, + track.audio_LR$, + begin, + end, + track.out$, + ) + // console.log(`${f32.load(track.ops$ + 16)}`) + add_audio_audio( + track.out$, + this.out.L$, + begin, + end, + this.out.L$ + ) + memory.copy( + this.out.R$ + (begin << 2), + this.out.L$ + (begin << 2), + (end - begin) << 2 + ) + } + } +} diff --git a/asconfig-dsp-nort.json b/asconfig-dsp-nort.json new file mode 100644 index 0000000..877b793 --- /dev/null +++ b/asconfig-dsp-nort.json @@ -0,0 +1,37 @@ +{ + "targets": { + "debug": { + "outFile": "./as/build/dsp-nort.wasm", + "textFile": "./as/build/dsp-nort.wat", + "sourceMap": true, + "debug": true, + "noAssert": true + }, + "release": { + "outFile": "./as/build/dsp-nort.wasm", + "textFile": "./as/build/dsp-nort.wat", + "sourceMap": true, + "debug": false, + "optimizeLevel": 0, + "shrinkLevel": 0, + "converge": false, + "noAssert": true + } + }, + "options": { + "enable": [ + "simd", + "relaxed-simd", + "threads" + ], + "sharedMemory": true, + "importMemory": true, + "initialMemory": 2000, + "maximumMemory": 2000, + "bindings": "raw", + "runtime": false, + "exportRuntime": false, + "zeroFilledMemory": true, + "uncheckedBehavior": "always" + } +} diff --git a/asconfig-dsp.json b/asconfig-dsp.json index cd49faa..1a18b45 100644 --- a/asconfig-dsp.json +++ b/asconfig-dsp.json @@ -26,8 +26,8 @@ ], "sharedMemory": true, "importMemory": false, - "initialMemory": 5000, - "maximumMemory": 5000, + "initialMemory": 2000, + "maximumMemory": 2000, "bindings": "raw", "runtime": "incremental", "exportRuntime": true diff --git a/generated/assembly/dsp-runner.ts b/generated/assembly/dsp-runner.ts index 13761fd..d8d8d28 100644 --- a/generated/assembly/dsp-runner.ts +++ b/generated/assembly/dsp-runner.ts @@ -6,7 +6,8 @@ import { Sound } from '../../as/assembly/dsp/vm/sound' const dsp = new Dsp() -export function run(ctx: Sound, ops$: usize): void { +export function run(sound$: usize, ops$: usize): void { + const snd = changetype(sound$) const ops = changetype>(ops$) let i: i32 = 0 @@ -17,28 +18,28 @@ export function run(ctx: Sound, ops$: usize): void { case Op.CreateGen: dsp.CreateGen( - ctx, + snd, changetype(unchecked(ops[i++])) ) continue case Op.CreateAudios: dsp.CreateAudios( - ctx, + snd, changetype(unchecked(ops[i++])) ) continue case Op.CreateValues: dsp.CreateValues( - ctx, + snd, changetype(unchecked(ops[i++])) ) continue case Op.AudioToScalar: dsp.AudioToScalar( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) @@ -46,7 +47,7 @@ export function run(ctx: Sound, ops$: usize): void { case Op.LiteralToAudio: dsp.LiteralToAudio( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) @@ -54,7 +55,7 @@ export function run(ctx: Sound, ops$: usize): void { case Op.Pick: dsp.Pick( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), @@ -64,14 +65,14 @@ export function run(ctx: Sound, ops$: usize): void { case Op.Pan: dsp.Pan( - ctx, + snd, changetype(unchecked(ops[i++])) ) continue case Op.SetValue: dsp.SetValue( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) @@ -80,7 +81,7 @@ export function run(ctx: Sound, ops$: usize): void { case Op.SetValueDynamic: dsp.SetValueDynamic( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) @@ -89,7 +90,7 @@ export function run(ctx: Sound, ops$: usize): void { case Op.SetProperty: dsp.SetProperty( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), @@ -99,14 +100,14 @@ export function run(ctx: Sound, ops$: usize): void { case Op.UpdateGen: dsp.UpdateGen( - ctx, + snd, changetype(unchecked(ops[i++])) ) continue case Op.ProcessAudio: dsp.ProcessAudio( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) @@ -114,7 +115,7 @@ export function run(ctx: Sound, ops$: usize): void { case Op.ProcessAudioStereo: dsp.ProcessAudioStereo( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) @@ -123,7 +124,7 @@ export function run(ctx: Sound, ops$: usize): void { case Op.BinaryOp: dsp.BinaryOp( - ctx, + snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])), diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 28e07a6..fcfa179 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Wed Oct 16 2024 08:42:09 GMT+0300 (Eastern European Summer Time) +// auto-generated Fri Oct 18 2024 21:53:09 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' @@ -10,7 +10,7 @@ export const dspGens = { hasAudioOut: true }, aosc: { inherits: 'osc', props: [], hasAudioOut: true }, - atan: { inherits: 'gen', props: [ 'gain', 'in' ], hasAudioOut: true }, + atan: { inherits: 'gen', props: [ 'in' ], hasAudioOut: true }, bap: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, bbp: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, bhp: { inherits: 'biquad', props: [ 'cut', 'q' ], hasAudioOut: true }, @@ -103,8 +103,8 @@ export const dspGens = { spk: { inherits: 'svf', props: [ 'cut', 'q' ], hasAudioOut: true }, sqr: { inherits: 'aosc', props: [], hasAudioOut: true }, svf: { inherits: 'gen', props: [ 'in' ] }, - tanh: { inherits: 'gen', props: [ 'gain', 'in' ], hasAudioOut: true }, - tanha: { inherits: 'gen', props: [ 'gain', 'in' ], hasAudioOut: true }, + tanh: { inherits: 'gen', props: [ 'in' ], hasAudioOut: true }, + tanha: { inherits: 'gen', props: [ 'in' ], hasAudioOut: true }, tap: { inherits: 'gen', props: [ 'ms', 'in' ], hasAudioOut: true }, tri: { inherits: 'aosc', props: [], hasAudioOut: true }, zero: { inherits: 'gen', props: [], hasAudioOut: true } @@ -123,7 +123,6 @@ export namespace Props { } export interface Atan extends Gen { - gain?: Value | number in?: Value.Audio } export interface Bap extends Biquad { @@ -319,11 +318,9 @@ export namespace Props { in?: Value.Audio } export interface Tanh extends Gen { - gain?: Value | number in?: Value.Audio } export interface Tanha extends Gen { - gain?: Value | number in?: Value.Audio } export interface Tap extends Gen { diff --git a/scripts/generate-dsp-vm.ts b/scripts/generate-dsp-vm.ts index 3940036..1a82308 100644 --- a/scripts/generate-dsp-vm.ts +++ b/scripts/generate-dsp-vm.ts @@ -106,7 +106,8 @@ import { Sound } from '../../as/assembly/dsp/vm/sound' const dsp = new Dsp() -export function run(ctx: Sound, ops$: usize): void { +export function run(sound$: usize, ops$: usize): void { + const snd = changetype(sound$) const ops = changetype>(ops$) let i: i32 = 0 @@ -118,7 +119,7 @@ export function run(ctx: Sound, ops$: usize): void { ${indent(6, fns.map(({ fn, args }) => `case Op.${fn}: dsp.${fn}( - ctx, + snd, ${indent(4, args.map(([, type]) => `changetype<${type}>(unchecked(ops[i++]))` ).join(',\n'))} diff --git a/src/as/dsp/dsp-service.ts b/src/as/dsp/dsp-service.ts deleted file mode 100644 index 396fd53..0000000 --- a/src/as/dsp/dsp-service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Sigui, nu } from 'sigui' -import { Deferred, rpc } from 'utils' -import { Clock } from './dsp-shared.ts' -import type { DspWorker } from './dsp-worker.ts' -import DspWorkerFactory from './dsp-worker.ts?worker' - -export type DspService = ReturnType - -export function DspService(ctx: AudioContext) { - using $ = Sigui() - - const deferred = Deferred() - const ready = deferred.promise - const worker = new DspWorkerFactory() - - const service = rpc(worker, { - async isReady() { - deferred.resolve() - } - }) - - class DspInfo { - dsp = $.unwrap(() => ready.then(() => service.createDsp(ctx.sampleRate))) - @nu get clock() { - const { dsp } = $.of(this) - if (dsp instanceof Error) return - $() - const clock = Clock(dsp.memory.buffer, dsp.clock$) - return clock - } - } - - const info = $(new DspInfo) - - return { info, ready, service } -} diff --git a/src/as/dsp/dsp-shared.ts b/src/as/dsp/dsp-shared.ts deleted file mode 100644 index 6e7db9f..0000000 --- a/src/as/dsp/dsp-shared.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Struct } from 'utils' - -export type Clock = typeof Clock.type - -export const Clock = Struct({ - time: 'f64', - timeStep: 'f64', - prevTime: 'f64', - startTime: 'f64', - endTime: 'f64', - bpm: 'f64', - coeff: 'f64', - barTime: 'f64', - barTimeStep: 'f64', - loopStart: 'f64', - loopEnd: 'f64', - sampleRate: 'u32', - jumpBar: 'i32', - ringPos: 'u32', - nextRingPos: 'u32', -}) diff --git a/src/as/dsp/dsp-worker.ts b/src/as/dsp/dsp-worker.ts deleted file mode 100644 index d7c8c03..0000000 --- a/src/as/dsp/dsp-worker.ts +++ /dev/null @@ -1,158 +0,0 @@ -// @ts-ignore -self.document = { - querySelectorAll() { return [] as any }, - baseURI: location.origin -} - -import { wasm } from 'dsp' -import { Lru, rpc } from 'utils' -import { Dsp, Sound } from '~/src/as/dsp/dsp.ts' -import { Note } from '~/src/as/dsp/notes-shared.ts' -import { ParamValue } from '~/src/as/dsp/params-shared.ts' -import { AstNode } from '~/src/lang/interpreter.ts' -import { Token, tokenize } from '~/src/lang/tokenize.ts' - -export type DspWorker = typeof worker - -const sounds = new Map() - -const getFloats = Lru(20, (key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) -const getBuffer = Lru(20, (length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) -const getPointers = Lru(20, (length: number) => wasm.alloc(Uint32Array, length), item => item.fill(0), item => item.free()) - -const worker = { - dsp: null as null | Dsp, - tokensAstNode: new Map(), - waveLength: 1, - error: null as Error | null, - async createDsp(sampleRate: number) { - const dsp = this.dsp = Dsp({ sampleRate }) - return { - memory: wasm.memory, - clock$: dsp.clock.ptr, - } - }, - async createSound() { - const dsp = this.dsp - if (!dsp) { - throw new Error('Dsp not ready.') - } - const sound = dsp.Sound() - sounds.set(+sound.sound$, sound) - return +sound.sound$ as number - }, - async renderSource( - sound$: number, - audioLength: number, - code: string, - voicesCount: number, - hasMidiIn: boolean, - notes: Note[], - params: ParamValue[][], - ) { - const dsp = this.dsp - if (!dsp) { - throw new Error('Dsp not ready.') - } - - const sound = sounds.get(sound$) - if (!sound) { - throw new Error('Sound not found, id: ' + sound$) - } - - const { clock } = dsp - const info = this - - try { - // ensure an audio in the stack - code = code.trim() || '[zero]' - - const tokens = [...tokenize({ code })] - - sound.reset() - - clock.time = 0 - clock.barTime = 0 - clock.bpm ||= 144 - - wasm.updateClock(clock.ptr) - - const { program, out } = sound.process( - tokens, - voicesCount, - hasMidiIn, - params.length, - ) - - if (!out.LR) { - return { error: 'No audio in the stack.' } - } - - info.tokensAstNode = program.value.tokensAstNode - - const length = Math.floor(audioLength * clock.sampleRate / clock.coeff) - - const key = `${sound$}:${length}` - const floats = getFloats(key, length) - - const notesData = getBuffer(notes.length * 4) // * 4 elements: n, time, length, vel - { - let i = 0 - for (const note of notes) { - const p = (i++) * 4 - notesData[p] = note.n - notesData[p + 1] = note.time - notesData[p + 2] = note.length - notesData[p + 3] = note.vel - } - } - - const paramsData = getPointers(params.length * 2) // * 1 el: ptr, length - { - let i = 0 - for (const values of params) { - const data = getBuffer(values.length * 4) - const p = (i++) * 2 - paramsData[p] = data.ptr - paramsData[p + 1] = values.length - - let j = 0 - for (const value of values) { - const p = (j++) * 4 - data[p] = value.time - data[p + 1] = value.length - data[p + 2] = value.slope - data[p + 3] = value.amt - } - } - } - - wasm.fillSound(sound.sound$, - sound.ops.ptr, - notesData.ptr, - notes.length, - paramsData.ptr, - params.length, - out.LR.getAudio(), - 0, - floats.length, - floats.ptr, - ) - - return { floats } - } - catch (e) { - if (e instanceof Error) { - console.warn(e) - console.warn(...((e as any)?.cause?.nodes ?? [])) - info.error = e - return { error: e.message } - } - throw e - } - }, -} - -const host = rpc<{ isReady(): void }>(self as any, worker) -host.isReady() -console.log('[dsp-worker] started') diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index 98287eb..a13f7e4 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -1,28 +1,22 @@ -import { wasm } from './wasm.ts' import { Sigui } from 'sigui' -import { Struct, fromEntries, getMemoryView, keys } from 'utils' -import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS, MAX_SCALARS } from '../../../as/assembly/dsp/constants.ts' -import { DspBinaryOp, SoundData as SoundDataShape } from '../../../as/assembly/dsp/vm/dsp-shared.ts' -import { Op } from '../../../generated/assembly/dsp-op.ts' -import { Gen, dspGens } from '../../../generated/typescript/dsp-gens.ts' -import { createVm } from '../../../generated/typescript/dsp-vm.ts' -import { AstNode, interpret } from '../../lang/interpreter.ts' -import { Token, tokenize } from '../../lang/tokenize.ts' -import { parseNumber } from '../../lang/util.ts' -import { Clock } from './dsp-shared.ts' +import { fromEntries, getMemoryView, keys } from 'utils' +import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS } from '~/as/assembly/dsp/constants.ts' +import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' +import { Op } from '~/generated/assembly/dsp-op.ts' +import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts' +import { createVm } from '~/generated/typescript/dsp-vm.ts' +import { AstNode, interpret } from '~/src/lang/interpreter.ts' +import { Token, tokenize } from '~/src/lang/tokenize.ts' +import { parseNumber } from '~/src/lang/util.ts' +import { Clock } from './shared.ts' import { getAllProps } from './util.ts' import { Value } from './value.ts' +import { wasm } from './wasm.ts' const DEBUG = false const MAX_OPS = 4096 -const SoundData = Struct({ - begin: 'u32', - end: 'u32', - pan: 'f32', -}) - const dspGensKeys = keys(dspGens) export type Dsp = ReturnType @@ -43,13 +37,8 @@ function getContext() { } } -const tokensCopyMap = new Map() -const tokensCopyRevMap = new Map() -function copyToken(token: Token) { - const newToken = { ...token } - tokensCopyMap.set(token, newToken) - tokensCopyRevMap.set(newToken, token) - return newToken +function copy>(obj: T): T { + return { ...obj } } let preTokens: Token[] @@ -86,10 +75,8 @@ export function Dsp({ sampleRate, core$ }: { })] function Sound() { - const sound$ = wasm.createSound(engine$) + const sound$ = pin(wasm.createSound(engine$)) - const data$ = wasm.getSoundData(sound$) - const data = SoundData(wasm.memory.buffer, +data$) satisfies SoundDataShape const context = getContext() const ops = wasm.alloc(Int32Array, MAX_OPS) @@ -101,9 +88,6 @@ export function Dsp({ sampleRate, core$ }: { const lists = view.getI32(lists$, MAX_LISTS) context.listsi = lists - const scalars$ = wasm.getSoundScalars(sound$) - const scalars = view.getF32(scalars$, MAX_SCALARS) - const literals$ = wasm.getSoundLiterals(sound$) const literals = view.getF32(literals$, MAX_LITERALS) const preset = createLiteralsPreset(literals) @@ -272,21 +256,21 @@ export function Dsp({ sampleRate, core$ }: { // Audio if (v.kind === Value.Kind.Audio) { if (audioProps.has(p)) { - vm.SetProperty(gen$, prop$, Value.Kind.Audio as number, v.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Audio, v.value$) } else { const scalar = sound.Value.Scalar.create() vm.AudioToScalar(v.ptr, scalar.ptr) - vm.SetProperty(gen$, prop$, Value.Kind.Scalar as number, scalar.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Scalar, scalar.value$) } break outer } else { if (audioProps.has(p)) { - vm.SetProperty(gen$, prop$, Value.Kind.Audio as number, v.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Audio, v.value$) } else { - vm.SetProperty(gen$, prop$, Value.Kind.Scalar as number, v.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Scalar, v.value$) } break outer } @@ -297,14 +281,14 @@ export function Dsp({ sampleRate, core$ }: { const floats = sound.Value.Floats.create() const text = opt[p] // loadSayText(floats, text) - vm.SetProperty(gen$, prop$, Value.Kind.Floats as number, floats.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Floats, floats.value$) break outer } if (name === 'freesound' && p === 'id') { const floats = sound.Value.Floats.create() const text = opt[p] // loadFreesound(floats, text) - vm.SetProperty(gen$, prop$, Value.Kind.Floats as number, floats.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Floats, floats.value$) break outer } value = 0 @@ -327,10 +311,10 @@ export function Dsp({ sampleRate, core$ }: { if (audioProps.has(p)) { const audio = sound.Value.Audio.create() vm.LiteralToAudio(literal.ptr, audio.ptr) - vm.SetProperty(gen$, prop$, Value.Kind.Audio as number, audio.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Audio, audio.value$) } else { - vm.SetProperty(gen$, prop$, Value.Kind.Scalar as number, literal.value$) + vm.SetProperty(gen$, prop$, Value.Kind.Scalar, literal.value$) } } } @@ -376,43 +360,6 @@ export function Dsp({ sampleRate, core$ }: { t: Value.Scalar rt: Value.Scalar co: Value.Scalar - - n0n: Value.Scalar - n0f: Value.Scalar - n0t: Value.Scalar - n0v: Value.Scalar - - n1n: Value.Scalar - n1f: Value.Scalar - n1t: Value.Scalar - n1v: Value.Scalar - - n2n: Value.Scalar - n2f: Value.Scalar - n2t: Value.Scalar - n2v: Value.Scalar - - n3n: Value.Scalar - n3f: Value.Scalar - n3t: Value.Scalar - n3v: Value.Scalar - - n4n: Value.Scalar - n4f: Value.Scalar - n4t: Value.Scalar - n4v: Value.Scalar - - n5n: Value.Scalar - n5f: Value.Scalar - n5t: Value.Scalar - n5v: Value.Scalar - - p0: Value.Scalar - p1: Value.Scalar - p2: Value.Scalar - p3: Value.Scalar - p4: Value.Scalar - p5: Value.Scalar } const api = { @@ -470,36 +417,35 @@ export function Dsp({ sampleRate, core$ }: { let prevHashId: string - function process(tokens: Token[], voicesCount: number, hasMidiIn: boolean, paramsCount: number) { + function process(tokens: Token[]) { const scope = {} as any const literals: AstNode[] = [] + // fix invisible tokens bounds to the + // last visible token for errors + const last = tokens.at(-1) + function fixToken(x: Token) { + if (!last) return x + x.line = last.line + x.col = last.col + x.right = last.right + x.bottom = last.bottom + x.index = last.index + x.length = last.length + return x + } + let tokensCopy = [ - ...preTokens, + ...preTokens.map(fixToken), ...tokens, - ...postTokens, - ] - .filter(t => t.type !== Token.Type.Comment) - .map(copyToken) - - - if (voicesCount && hasMidiIn) { - const voices = tokenize({ - code: Array.from({ length: voicesCount }, (_, i) => - `[midi_in n${i}v n${i}t n${i}f n${i}n]` - ).join('\n') + ` @` - }) - tokensCopy = [...tokensCopy, ...voices] - } + ...postTokens.map(fixToken), + ].filter(t => t.type !== Token.Type.Comment).map(copy) // create hash id from tokens. We compare this afterwards to determine // if we should make a new sound or update the old one. const hashId = [tokens.filter(t => t.type === Token.Type.Number).length].join('') - + tokens.filter(t => - [Token.Type.Id, Token.Type.Op].includes(t.type) - ).map(t => t.text).join('') - + voicesCount + + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') const isNew = hashId !== prevHashId prevHashId = hashId @@ -577,31 +523,29 @@ export function Dsp({ sampleRate, core$ }: { } return { - program, isNew, out, + program, } } const sound = { + api, + commit, context, - sound$, - data$, - data, - vm, + createLiteralsPreset, + getAudio, ops, - setup_vm, - setup_ops, preset, - run, + process, reset, - commit, - getAudio, - createLiteralsPreset, - values: [] as Value[], + run, + setup_ops, + setup_vm, + sound$, Value: Value.Factory(soundPartial), - api, - process, + values: [] as Value[], + vm, } return sound @@ -619,5 +563,4 @@ export type BinaryOp = { (lhs: number, rhs: Value): Value (lhs: Value, rhs: number): Value (lhs: Value, rhs: Value): Value - // (lhs: Value | number, rhs: Value | number): Value | number } diff --git a/src/as/dsp/index.ts b/src/as/dsp/index.ts index 23898e5..f4648b3 100644 --- a/src/as/dsp/index.ts +++ b/src/as/dsp/index.ts @@ -1,3 +1,5 @@ +export * from './constants.ts' export * from './dsp.ts' export * from './util.ts' +export * from './value.ts' export * from './wasm.ts' diff --git a/src/as/dsp/preview-service.ts b/src/as/dsp/preview-service.ts new file mode 100644 index 0000000..5101ecb --- /dev/null +++ b/src/as/dsp/preview-service.ts @@ -0,0 +1,41 @@ +import { Sigui } from 'sigui' +import { Deferred, rpc } from 'utils' +import type { PreviewWorker } from './preview-worker.ts' +import PreviewWorkerFactory from './preview-worker.ts?worker' + +export type PreviewService = ReturnType + +export function PreviewService(ctx: AudioContext) { + using $ = Sigui() + + const deferred = Deferred() + const isReady = deferred.promise + const worker = new PreviewWorkerFactory() + const service = rpc(worker, { + async isReady() { + deferred.resolve() + } + }) + + const info = $({ + isReady: null as null | true, + dsp: null as null | Awaited>, + }) + + isReady.then(() => { + info.isReady = true + }) + + $.fx(() => { + const { isReady } = $.of(info) + $().then(async () => { + await service.createDsp(ctx.sampleRate) + }) + }) + + function dispose() { + worker.terminate() + } + + return { info, isReady, service, dispose } +} diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts new file mode 100644 index 0000000..b97bf9f --- /dev/null +++ b/src/as/dsp/preview-worker.ts @@ -0,0 +1,140 @@ +// @ts-ignore +self.document = { + querySelectorAll() { return [] as any }, + baseURI: location.origin +} + +import type { Value } from 'dsp' +import { Dsp, Sound, wasm } from 'dsp' +import { assign, Lru, rpc } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' +import { AstNode } from '~/src/lang/interpreter.ts' +import { Token, tokenize } from '~/src/lang/tokenize.ts' + +export type PreviewWorker = typeof worker + +const sounds = new Map() + +const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) + +let epoch = 0 + +const worker = { + dsp: null as null | Dsp, + error: null as null | Error, + async createDsp(sampleRate: number) { + this.dsp = Dsp({ sampleRate }) + }, + async createSound() { + const dsp = this.dsp + if (!dsp) throw new Error('Dsp not ready.') + + const sound = dsp.Sound() + sounds.set(sound.sound$, sound) + return sound.sound$ + }, + async build(sound$: number, code: string) { + const dsp = this.dsp + if (!dsp) throw new Error('Dsp not ready.') + + const sound = sounds.get(sound$) + if (!sound) throw new Error('Sound not found, id: ' + sound$) + + const tokens = [...tokenize({ code })] + const { program, out } = sound.process(tokens) + if (!out.LR) throw new Error('No audio in the stack.', { cause: { nodes: [] } }) + + program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => + a.line === b.line + ? a.col - b.col + : a.line - b.line + ) + + let last: AstNode | null = null + const waves = new Map() + const waveWidgets = [] as { floats: Float32Array | null, bounds: Token.Bounds }[] + let nodeCount = 0 + for (const node of program.value.results) { + if ('genId' in node) { + const bounds = node.result.bounds + if (last && last.bounds.line === bounds.line && last.bounds.right > bounds.col) { + last.bounds.right = bounds.col - 1 + waves.get(last)!.bounds.right = bounds.col - 1 + } + const wave = (waveWidgets[nodeCount] ??= { floats: null, bounds }) + wave.floats = sound.getAudio((node.result.value as Value.Audio).ptr) + assign(wave.bounds, bounds) + waves.set(node.result, wave) + last = node.result + nodeCount++ + } + } + + let delta = waveWidgets.length - nodeCount + while (delta-- > 0) waveWidgets.pop() + + const LR = out.LR.getAudio() + + const length = BUFFER_SIZE + const key = `${sound$}:${length}:${epoch++}` + const floats = getFloats(key, length) + + return { + ops$: sound.ops.ptr, + LR, + floats, + waves: waveWidgets as { floats: Float32Array, bounds: Token.Bounds }[], + } + }, + async renderSource(sound$: number, code: string) { + const dsp = this.dsp + if (!dsp) throw new Error('Dsp not ready.') + + const sound = sounds.get(sound$) + if (!sound) throw new Error('Sound not found, id: ' + sound$) + + const { clock } = dsp + const info = this + + try { + sound.reset() + + clock.time = 0 + clock.barTime = 0 + clock.bpm ||= 144 + + wasm.updateClock(clock.ptr) + + const { LR, floats, waves } = await this.build(sound$, code) + + wasm.fillSound( + sound.sound$, + sound.ops.ptr, + LR, + 0, + floats.length, + floats.ptr, + ) + + return { floats, waves } + } + catch (e) { + if (e instanceof Error) { + console.warn(e) + console.warn(...((e as any)?.cause?.nodes ?? [])) + info.error = e + return { + error: { + message: e.message, + cause: /* (e as any).cause ?? */{ nodes: [] } + } + } + } + throw e + } + }, +} + +const host = rpc<{ isReady(): void }>(self as any, worker) +host.isReady() +console.debug('[preview-worker] started') diff --git a/src/as/dsp/shared.ts b/src/as/dsp/shared.ts new file mode 100644 index 0000000..772bfb4 --- /dev/null +++ b/src/as/dsp/shared.ts @@ -0,0 +1,47 @@ +import { Struct } from 'utils' + +export const enum DspWorkletMode { + Idle, + Reset, + Stop, + Play, + Pause, +} + +export type Clock = typeof Clock.type + +export const Clock = Struct({ + time: 'f64', + timeStep: 'f64', + prevTime: 'f64', + startTime: 'f64', + endTime: 'f64', + bpm: 'f64', + coeff: 'f64', + barTime: 'f64', + barTimeStep: 'f64', + loopStart: 'f64', + loopEnd: 'f64', + sampleRate: 'u32', + jumpBar: 'i32', + ringPos: 'u32', + nextRingPos: 'u32', +}) + +export type PlayerTrack = typeof PlayerTrack.type + +export const PlayerTrack = Struct({ + sound$: 'usize', + ops$: 'usize', + notes$: 'usize', + notesCount: 'u32', + params$: 'usize', + paramsCount: 'u32', + audio_LR$: 'i32', + out$: 'usize', +}) + +export const Out = Struct({ + L$: 'usize', + R$: 'usize', +}) diff --git a/src/as/init-wasm.ts b/src/as/init-wasm.ts index 4dad53d..f5cb6b3 100644 --- a/src/as/init-wasm.ts +++ b/src/as/init-wasm.ts @@ -40,7 +40,7 @@ export function initWasm(wasm: Wasm) { let lru = new Set() const TRIES = 16 - const GC_EVERY = 1024000 + const GC_EVERY = 81920 let allocs = 0 const funcs = new Map([ diff --git a/src/as/pkg/player.ts b/src/as/pkg/player.ts index 996a849..9bd21f4 100644 --- a/src/as/pkg/player.ts +++ b/src/as/pkg/player.ts @@ -83,9 +83,9 @@ export function Player(ctx: AudioContext) { } if (!registeredContexts.has(ctx)) { + registeredContexts.add(ctx) ctx.audioWorklet .addModule(playerWorkletUrl) - .then(() => registeredContexts.add(ctx)) .then(createNode) } else { diff --git a/src/as/pkg/worklet.ts b/src/as/pkg/worklet.ts index 9017e07..9cf8dfa 100644 --- a/src/as/pkg/worklet.ts +++ b/src/as/pkg/worklet.ts @@ -1,5 +1,5 @@ import { getMemoryView, wasmSourceMap } from 'utils' -import { BUFFER_SIZE } from '~/as/assembly/pkg/constants.ts' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import type { __AdaptedExports as WasmExports } from '~/as/build/pkg-nort.d.ts' import hex from '~/as/build/pkg-nort.wasm?raw-hex' import { Out, PlayerMode } from '~/src/as/pkg/shared.ts' diff --git a/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx index ff97ba0..7e78c59 100644 --- a/src/comp/DspEditor.tsx +++ b/src/comp/DspEditor.tsx @@ -5,7 +5,9 @@ import { Token, tokenize } from '~/src/lang/tokenize.ts' import { screen } from '~/src/screen.ts' import { theme } from '~/src/theme.ts' import { Editor } from '~/src/ui/Editor.tsx' -import { HoverMarkWidget } from '~/src/ui/editor/widgets/index.ts' +import { ErrorSubWidget, HoverMarkWidget } from '~/src/ui/editor/widgets/index.ts' + +export type DspEditor = ReturnType export function DspEditor({ code, width, height }: { width: Signal @@ -20,6 +22,7 @@ export function DspEditor({ code, width, height }: { width, height, code, + error: null as Error | null, }) const mouse = { x: 0, y: 0 } @@ -27,6 +30,7 @@ export function DspEditor({ code, width, height }: { let value: number let digits: number let isDot = false + const hoverMark = HoverMarkWidget() function getHoveringNumber(pane: Pane) { @@ -204,6 +208,7 @@ export function DspEditor({ code, width, height }: { function onMouseUp(pane: Pane) { if (number && clickCount <= 1) { number = void 0 + updateNumberMark(pane) return true } } @@ -268,5 +273,22 @@ export function DspEditor({ code, width, height }: { inputHandlers, }) - return editor + // const errorSub = ErrorSubWidget() + // $.fx(() => { + // const { error } = $.of(info) + // const { pane } = editor.info + // $() + // pane.draw.widgets.subs.add(errorSub.widget) + // errorSub.info.error = error + // errorSub.widget.bounds = Token.bounds((error as any).cause?.nodes ?? [] as Token[]) + // pane.draw.info.triggerUpdateTokenDrawInfo++ + // pane.view.anim.info.epoch++ + // return () => { + // pane.draw.widgets.subs.delete(errorSub.widget) + // pane.draw.info.triggerUpdateTokenDrawInfo++ + // pane.view.anim.info.epoch++ + // } + // }) + + return { info, el: editor.el, editor } } diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts index bac4cb9..e3f3bd8 100644 --- a/src/lang/interpreter.ts +++ b/src/lang/interpreter.ts @@ -182,11 +182,11 @@ export function interpret(sound: Sound, data: Record, tokens: Token return t } function peek() { - return tokens[i] + return tokens[i] ?? tokens[i - 1] ?? tokens.at(-1) } function expectText(text: string) { if (text && peek()?.text !== text) { - throw new SyntaxError('Expected text ' + text, { cause: { nodes: [peek()] } }) + throw new SyntaxError('Expected text ' + text, { cause: { nodes: [peek()].filter(Boolean) } }) } return next() } @@ -385,8 +385,8 @@ export function interpret(sound: Sound, data: Record, tokens: Token throw new Error('Missing right operand.', { cause: { nodes: [t] } }) } if (r.type !== AstNode.Type.Id) { - console.error(r, l, scope) - throw new Error('Expected identifier for assignment operation.', { cause: { nodes: [r] } }) + // console.error(r, l, scope) + throw new Error('Expected identifier for assignment operation.', { cause: { nodes: [t] } }) } if (!l) { throw new Error('Missing left operand.', { cause: { nodes: [t] } }) diff --git a/src/lang/tokenize.ts b/src/lang/tokenize.ts index c2a93ff..0b47158 100644 --- a/src/lang/tokenize.ts +++ b/src/lang/tokenize.ts @@ -120,6 +120,10 @@ export namespace Token { if (t.right > right) right = t.right } + line = isFinite(line) ? line : 0 + col = isFinite(col) ? col : 0 + index = isFinite(index) ? index : 0 + return { line, col, right, bottom, index, length: end - index } } } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index bc00a9f..d592ffd 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -10,7 +10,9 @@ import { About } from '~/src/pages/About.tsx' import { AssemblyScript } from '~/src/pages/AssemblyScript.tsx' import { CanvasDemo } from '~/src/pages/CanvasDemo' import { Chat } from '~/src/pages/Chat/Chat.tsx' -import { DspDemo } from '~/src/pages/DspDemo.tsx' +import { DspAsyncDemo } from '~/src/pages/DspAsyncDemo.tsx' +import { DspNodeDemo } from '~/src/pages/DspNodeDemo/DspNodeDemo.tsx' +import { DspSyncDemo } from '~/src/pages/DspSyncDemo' import { EditorDemo } from '~/src/pages/EditorDemo.tsx' import { Home } from '~/src/pages/Home.tsx' import { OAuthRegister } from '~/src/pages/OAuthRegister.tsx' @@ -18,6 +20,7 @@ import { QrCode } from '~/src/pages/QrCode.tsx' import { UiShowcase } from '~/src/pages/UiShowcase.tsx' import { WebGLDemo } from '~/src/pages/WebGLDemo.tsx' import { WebSockets } from '~/src/pages/WebSockets.tsx' +import { WorkerWorkletDemo } from '~/src/pages/WorkerWorklet/WorkerWorkletDemo' import { whoami } from '~/src/rpc/auth.ts' import { state, triggers } from '~/src/state.ts' import { go, Link } from '~/src/ui/Link.tsx' @@ -41,7 +44,10 @@ export function App() { '!/canvas': () => , '/webgl': () => , '/editor': () => , - '/dsp': () => , + '/dsp-sync': () => , + '/dsp-async': () => , + '/dsp-node': () => , + '/worker-worklet': () => , '/asc': () => , '/qrcode': () => , '/about': () => , diff --git a/src/pages/DspAsyncDemo.tsx b/src/pages/DspAsyncDemo.tsx new file mode 100644 index 0000000..0a0bbed --- /dev/null +++ b/src/pages/DspAsyncDemo.tsx @@ -0,0 +1,132 @@ +import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' +import { Sigui } from 'sigui' +import { assign, Lru } from 'utils' +import { PreviewService } from '~/src/as/dsp/preview-service' +import { DspEditor } from '~/src/comp/DspEditor.tsx' +import { Canvas } from '~/src/ui/Canvas.tsx' +import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { H2 } from '~/src/ui/Heading.tsx' + +const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) + +export function DspAsyncDemo() { + using $ = Sigui() + + const info = $({ + width: 400, + height: 300, + code: `[sin 42.11 303 +[exp 2.00] 6.66^ * +] +[exp 1.00] 9.99^ * +`, + floats: new Float32Array(), + sound$: null as null | number, + error: null as null | Error, + }) + + const ctx = new AudioContext({ sampleRate: 48000 }) + + const preview = PreviewService(ctx) + $.fx(() => preview.dispose) + + const length = 8192 + + const canvas = as HTMLCanvasElement + const gfx = Gfx({ canvas }) + const view = Rect(0, 0, 500, 500) + const matrix = Matrix() + const c = gfx.createContext(view, matrix) + const shapes = c.createShapes() + c.sketch.scene.add(shapes) + + const plot = WaveGlWidget(shapes) + plot.widget.rect.w = 400 + plot.widget.rect.h = 300 + plot.info.floats = wasmGfx.alloc(Float32Array, length) + + const waveWidgets: WaveGlWidget[] = [] + + $.fx(() => { + const { isReady } = $.of(preview.info) + $().then(async () => { + info.sound$ = await preview.service.createSound() + }) + }) + + queueMicrotask(() => { + $.fx(() => { + const { sound$ } = $.of(info) + const { pane } = dspEditor.editor.info + const { codeVisual } = pane.buffer.info + $().then(async () => { + let result: Awaited> + let nodeCount = 0 + + try { + result = await preview.service.renderSource(sound$, codeVisual) + + pane.draw.widgets.deco.clear() + plot.info.floats.fill(0) + + if (result.error) { + throw new Error(result.error.message, { cause: result.error.cause }) + } + if (!result?.floats) { + throw new Error('Could not render.') + } + + info.error = null + plot.info.floats.set(result.floats) + + for (const waveData of result.waves) { + const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + wave.info.floats = wave.info.floats.length + ? wave.info.floats + : getFloatsGfx(`${nodeCount}`, 8192) + wave.info.floats.set(waveData.floats) + assign(wave.widget.bounds, waveData.bounds) + pane.draw.widgets.deco.add(wave.widget) + nodeCount++ + } + } + catch (err) { + if (err instanceof Error) { + info.error = err + } + else { + throw err + } + } + + let delta = waveWidgets.length - nodeCount + while (delta-- > 0) waveWidgets.pop()?.dispose() + + pane.draw.info.triggerUpdateTokenDrawInfo++ + + c.meshes.draw() + pane.view.anim.info.epoch++ + pane.draw.widgets.update() + }) + }) + }) + + const dspEditor = DspEditor({ + width: info.$.width, + height: info.$.height, + code: info.$.code, + }) + + $.fx(() => { + const { error } = $.of(info) + if (!error) return + console.error(error) + dspEditor.info.error = error + return () => dspEditor.info.error = null + }) + + return
+

Dsp Async demo

+ {dspEditor} + {canvas} +
+} diff --git a/src/pages/DspNodeDemo.tsx_ b/src/pages/DspNodeDemo.tsx_ new file mode 100644 index 0000000..427acd3 --- /dev/null +++ b/src/pages/DspNodeDemo.tsx_ @@ -0,0 +1,151 @@ +import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' +import { Sigui } from 'sigui' +import { assign, Lru } from 'utils' +import { DspNode } from '~/src/as/dsp/node.ts' +import { DspService } from '~/src/as/dsp/service.ts' +import { PlayerTrack } from '~/src/as/dsp/shared.ts' +import { DspEditor } from '~/src/comp/DspEditor.tsx' +import { Button } from '~/src/ui/Button.tsx' +import { Canvas } from '~/src/ui/Canvas.tsx' +import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { H3 } from '~/src/ui/Heading.tsx' + +const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) + +export function DspNodeDemo() { + using $ = Sigui() + + const info = $({ + width: 400, + height: 300, + code: `[sin 42.11 303 +[exp 2.00] 6.66^ * +] +[exp 1.00] 9.99^ * +`, + validCode: '', + floats: new Float32Array(), + previewSound$: null as null | number, + track: null as null | PlayerTrack, + error: null as null | Error, + }) + + const ctx = new AudioContext({ sampleRate: 48000 }) + $.fx(() => () => ctx.close()) + + const dspService = DspService(ctx) + $.fx(() => dspService.dispose) + + const dspNode = DspNode(ctx, dspService) + $.fx(() => dspNode.dispose) + + const length = 8192 + + const canvas = as HTMLCanvasElement + const gfx = Gfx({ canvas }) + const view = Rect(0, 0, 500, 500) + const matrix = Matrix() + const c = gfx.createContext(view, matrix) + const shapes = c.createShapes() + c.sketch.scene.add(shapes) + + const plot = WaveGlWidget(shapes) + plot.widget.rect.w = 400 + plot.widget.rect.h = 300 + plot.info.floats = wasmGfx.alloc(Float32Array, length) + + const waveWidgets: WaveGlWidget[] = [] + + $.fx(() => { + const { dsp } = $.of(dspService.info) + if (dsp instanceof Error) return + $().then(async () => { + info.sound$ = await dspService.service.createSound() + }) + }) + + queueMicrotask(() => { + $.fx(() => { + const { previewSound$ } = $.of(info) + const { pane } = dspEditor.editor.info + const { codeVisual } = pane.buffer.info + $().then(async () => { + let result: Awaited> + let nodeCount = 0 + + try { + result = await dspService.service.renderSource( + previewSound$, + codeVisual, + ) + + pane.draw.widgets.deco.clear() + plot.info.floats.fill(0) + + if (result.error) { + throw new Error(result.error.message, { cause: result.error.cause }) + } + if (!result?.floats) { + throw new Error('Could not render.') + } + + info.error = null + info.validCode = codeVisual + plot.info.floats.set(result.floats) + + for (const waveData of result.waves) { + const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + wave.info.floats = wave.info.floats.length + ? wave.info.floats + : getFloatsGfx(`${nodeCount}`, 8192) + wave.info.floats.set(waveData.floats) + assign(wave.widget.bounds, waveData.bounds) + pane.draw.widgets.deco.add(wave.widget) + nodeCount++ + } + } + catch (err) { + if (err instanceof Error) { + info.error = err + } + else { + throw err + } + } + + let delta = waveWidgets.length - nodeCount + while (delta-- > 0) waveWidgets.pop()?.dispose() + + pane.draw.info.triggerUpdateTokenDrawInfo++ + + c.meshes.draw() + pane.view.anim.info.epoch++ + pane.draw.widgets.update() + }) + }) + }) + + const dspEditor = DspEditor({ + width: info.$.width, + height: info.$.height, + code: info.$.code, + }) + + $.fx(() => { + const { error } = $.of(info) + if (!error) return + console.error(error) + dspEditor.info.error = error + return () => dspEditor.info.error = null + }) + + return
+

+ DspNode demo + +

+ {dspEditor} + {canvas} +
+} diff --git a/src/pages/DspNodeDemo/DspNodeDemo.tsx b/src/pages/DspNodeDemo/DspNodeDemo.tsx new file mode 100644 index 0000000..28dab90 --- /dev/null +++ b/src/pages/DspNodeDemo/DspNodeDemo.tsx @@ -0,0 +1,196 @@ +import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' +import { Sigui } from 'sigui' +import { assign, Lru, rpc } from 'utils' +import { PreviewService } from '~/src/as/dsp/preview-service.ts' +import { DspEditor } from '~/src/comp/DspEditor.tsx' +import basicProcessorUrl from '~/src/pages/DspNodeDemo/basic-processor.ts?url' +import { QUEUE_SIZE } from '~/src/pages/DspNodeDemo/constants.ts' +import { DspWorker } from '~/src/pages/DspNodeDemo/dsp-worker.ts' +import DspWorkerFactory from '~/src/pages/DspNodeDemo/dsp-worker.ts?worker' +import { FreeQueue } from '~/src/pages/DspNodeDemo/free-queue.ts' +import { Canvas } from '~/src/ui/Canvas.tsx' +import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { H3 } from '~/src/ui/Heading.tsx' + +const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) + +export function DspNodeDemo() { + using $ = Sigui() + + const info = $({ + width: 400, + height: 300, + code: `t 8* x= +[sin 67.51 346 +[exp 1.00 x trig=] 18.95^ * + x trig=] +[exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.535] [clip .44] + +[saw 42 x trig=] [clip .4] .52* [slp 100 2336 [exp 8 x trig=] .2^ * + .92] [exp 8 x trig=] .2^ * [lp 800] +`, + floats: new Float32Array(), + sound$: null as null | number, + error: null as null | Error, + codeWorking: '', + }) + const inputQueue = new FreeQueue(QUEUE_SIZE, 1) + const outputQueue = new FreeQueue(QUEUE_SIZE, 1) + // Create an atomic state for synchronization between worker and AudioWorklet. + const atomicState = new Int32Array(new SharedArrayBuffer(1 * Int32Array.BYTES_PER_ELEMENT)) + const cmd = new Uint8Array(new SharedArrayBuffer(1 * Uint8Array.BYTES_PER_ELEMENT)) + const state = new Uint8Array(new SharedArrayBuffer(16384 * Uint8Array.BYTES_PER_ELEMENT)) + + const dspWorker = new DspWorkerFactory() + $.fx(() => () => dspWorker.terminate()) + + const dspService = rpc(dspWorker, { + isReady() { + dspService.setup({ + sampleRate: audioContext.sampleRate, + inputQueue, + outputQueue, + atomicState, + cmd, + state, + }) + } + }) + + + const audioContext = new AudioContext({ latencyHint: 0.000001 }) + $.fx(() => () => audioContext.close()) + + const encoder = new TextEncoder() + + $.fx(() => { + const { codeWorking } = info + $() + const encoded = encoder.encode(codeWorking) + state.set(encoded) + cmd[0] = encoded.length + }) + + $.fx(() => { + $().then(async () => { + await audioContext.audioWorklet.addModule(basicProcessorUrl) + + const processorNode = new AudioWorkletNode(audioContext, 'basic-processor', { + processorOptions: { + inputQueue, + outputQueue, + atomicState, + } + }) + + const osc = new OscillatorNode(audioContext) + osc.connect(processorNode).connect(audioContext.destination) + }) + }) + + const preview = PreviewService(audioContext) + $.fx(() => preview.dispose) + + const length = 8192 + + const canvas = as HTMLCanvasElement + const gfx = Gfx({ canvas }) + const view = Rect(0, 0, 500, 500) + const matrix = Matrix() + const c = gfx.createContext(view, matrix) + const shapes = c.createShapes() + c.sketch.scene.add(shapes) + + const plot = WaveGlWidget(shapes) + plot.widget.rect.w = 400 + plot.widget.rect.h = 300 + plot.info.floats = wasmGfx.alloc(Float32Array, length) + + const waveWidgets: WaveGlWidget[] = [] + + $.fx(() => { + const { isReady } = $.of(preview.info) + $().then(async () => { + info.sound$ = await preview.service.createSound() + }) + }) + + + queueMicrotask(() => { + $.fx(() => { + const { sound$ } = $.of(info) + const { pane } = dspEditor.editor.info + const { codeVisual } = pane.buffer.info + queueMicrotask(async () => { + const { codeVisual } = pane.buffer.info + + let result: Awaited> + let nodeCount = 0 + + try { + result = await preview.service.renderSource(sound$, codeVisual) + + pane.draw.widgets.deco.clear() + plot.info.floats.fill(0) + + if (result.error) { + throw new Error(result.error.message, { cause: result.error.cause }) + } + if (!result?.floats) { + throw new Error('Could not render.') + } + + info.error = null + info.codeWorking = codeVisual + plot.info.floats.set(result.floats) + + for (const waveData of result.waves) { + const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + wave.info.floats = wave.info.floats.length + ? wave.info.floats + : getFloatsGfx(`${nodeCount}`, 8192) + wave.info.floats.set(waveData.floats) + assign(wave.widget.bounds, waveData.bounds) + pane.draw.widgets.deco.add(wave.widget) + nodeCount++ + } + } + catch (err) { + if (err instanceof Error) { + info.error = err + } + else { + throw err + } + } + + let delta = waveWidgets.length - nodeCount + while (delta-- > 0) waveWidgets.pop()?.dispose() + + pane.draw.info.triggerUpdateTokenDrawInfo++ + + c.meshes.draw() + pane.view.anim.info.epoch++ + pane.draw.widgets.update() + }) + }) + }) + + const dspEditor = DspEditor({ + width: info.$.width, + height: info.$.height, + code: info.$.code, + }) + + $.fx(() => { + const { error } = $.of(info) + if (!error) return + console.warn(error) + dspEditor.info.error = error + return () => dspEditor.info.error = null + }) + + return
+

Dsp Node demo

+ {dspEditor} + {canvas} +
+} diff --git a/src/pages/DspNodeDemo/basic-processor.ts b/src/pages/DspNodeDemo/basic-processor.ts new file mode 100644 index 0000000..88570b9 --- /dev/null +++ b/src/pages/DspNodeDemo/basic-processor.ts @@ -0,0 +1,53 @@ +import { FRAME_SIZE, RENDER_QUANTUM } from '~/src/pages/WorkerWorklet/constants.ts' +import { FreeQueue } from '~/src/pages/WorkerWorklet/free-queue.ts' + +/** + * A simple AudioWorkletProcessor node. + * + * @class BasicProcessor + * @extends AudioWorkletProcessor + */ +class BasicProcessor extends AudioWorkletProcessor { + inputQueue: FreeQueue + outputQueue: FreeQueue + atomicState: Int32Array + + /** + * Constructor to initialize, input and output FreeQueue instances + * and atomicState to synchronise Worker with AudioWorklet + * @param {Object} options AudioWorkletProcessor options + * to initialize inputQueue, outputQueue and atomicState + */ + constructor(options: AudioWorkletNodeOptions) { + super() + + this.inputQueue = options.processorOptions.inputQueue + this.outputQueue = options.processorOptions.outputQueue + this.atomicState = options.processorOptions.atomicState + Object.setPrototypeOf(this.inputQueue, FreeQueue.prototype) + Object.setPrototypeOf(this.outputQueue, FreeQueue.prototype) + } + + process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean { + const input = inputs[0] + const output = outputs[0] + + // Push data from input into inputQueue. + this.inputQueue.push(input, RENDER_QUANTUM) + + // Try to pull data out of outputQueue and store it in output. + const didPull = this.outputQueue.pull(output, RENDER_QUANTUM) + if (!didPull) { + // console.log("failed to pull.") + } + + // Wake up worker to process a frame of data. + if (this.inputQueue.isFrameAvailable(FRAME_SIZE)) { + Atomics.notify(this.atomicState, 0, 1) + } + + return true + } +} + +registerProcessor('basic-processor', BasicProcessor) diff --git a/src/pages/DspNodeDemo/constants.ts b/src/pages/DspNodeDemo/constants.ts new file mode 100644 index 0000000..c7b4e1c --- /dev/null +++ b/src/pages/DspNodeDemo/constants.ts @@ -0,0 +1,4 @@ +export const KERNEL_LENGTH = 40 +export const RENDER_QUANTUM = 128 +export const FRAME_SIZE = KERNEL_LENGTH * RENDER_QUANTUM +export const QUEUE_SIZE = 16384 diff --git a/src/pages/DspNodeDemo/dsp-worker.ts b/src/pages/DspNodeDemo/dsp-worker.ts new file mode 100644 index 0000000..92640eb --- /dev/null +++ b/src/pages/DspNodeDemo/dsp-worker.ts @@ -0,0 +1,86 @@ +// @ts-ignore +self.document = { + querySelectorAll() { return [] as any }, + baseURI: location.origin +} + +import { Dsp, wasm } from 'dsp' +import { Lru, rpc } from 'utils' +import { tokenize } from '~/src/lang/tokenize.ts' +import { FRAME_SIZE } from '~/src/pages/DspNodeDemo/constants.ts' +import { FreeQueue } from '~/src/pages/DspNodeDemo/free-queue.ts' + +export type DspWorker = typeof worker + +const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) + +let epoch = 0 + +const worker = { + async setup({ sampleRate, inputQueue, outputQueue, atomicState, cmd, state }: { + sampleRate: number + inputQueue: FreeQueue + outputQueue: FreeQueue + atomicState: Int32Array + cmd: Uint8Array + state: Uint8Array + }) { + console.log('[dsp-worker] setup') + Object.setPrototypeOf(inputQueue, FreeQueue.prototype) + Object.setPrototypeOf(outputQueue, FreeQueue.prototype) + + const dsp = Dsp({ sampleRate }) + const sounds = Array.from({ length: 4 }, () => dsp.Sound()) + + let current = 0 + let sound = sounds[current++] + + // buffer for storing data pulled out from queue. + const input = getFloats(`${FRAME_SIZE}:${epoch++}`, FRAME_SIZE) + const output = getFloats(`${FRAME_SIZE}:${epoch++}`, FRAME_SIZE) + const decoder = new TextDecoder() + + let LR: number = -1 + + // loop for processing data. + while (Atomics.wait(atomicState, 0, 0) === 'ok') { + + // pull data out from inputQueue. + const didPull = inputQueue.pull([input], FRAME_SIZE) + + // If pulling data out was successfull, process it and push it to + // outputQueue + if (didPull) { + if (LR >= 0) { + wasm.fillSound( + sound.sound$, + sound.ops.ptr, + LR, + 0, + output.length, + output.ptr, + ) + } + + outputQueue.push([output], FRAME_SIZE) + } + + if (cmd[0]) { + const code = decoder.decode(state.slice(0, cmd[0])) + const tokens = Array.from(tokenize({ code })) + // sound = sounds[current++ % sounds.length] + const { out } = sound.process(tokens) + if (out.LR) { + LR = out.LR.getAudio() + } + cmd[0] = 0 + } + + Atomics.store(atomicState, 0, 0) + } + }, +} + +const host = rpc<{ isReady(): void }>(self as any, worker) +host.isReady() +console.debug('[dsp-worker] started') diff --git a/src/pages/DspNodeDemo/free-queue.ts b/src/pages/DspNodeDemo/free-queue.ts new file mode 100644 index 0000000..96e49d4 --- /dev/null +++ b/src/pages/DspNodeDemo/free-queue.ts @@ -0,0 +1,254 @@ +/** + * A shared storage for FreeQueue operation backed by SharedArrayBuffer. + * + * @typedef SharedRingBuffer + * @property {Uint32Array} states Backed by SharedArrayBuffer. + * @property {number} bufferLength The frame buffer length. Should be identical + * throughout channels. + * @property {Array} channelData The length must be > 0. + * @property {number} channelCount same with channelData.length + */ + +interface SharedRingBuffer { + states: Uint32Array + bufferLength: number + channelData: Float32Array[] + channelCount: number +} + +/** + * A single-producer/single-consumer lock-free FIFO backed by SharedArrayBuffer. + * In a typical pattern is that a worklet pulls the data from the queue and a + * worker renders audio data to fill in the queue. + */ + +export class FreeQueue { + states: Uint32Array + bufferLength: number + channelCount: number + channelData: Float32Array[] + + /** + * An index set for shared state fields. Requires atomic access. + * @enum {number} + */ + States = { + /** @type {number} A shared index for reading from the queue. (consumer) */ + READ: 0, + /** @type {number} A shared index for writing into the queue. (producer) */ + WRITE: 1, + }; + + /** + * FreeQueue constructor. A shared buffer created by this constuctor + * will be shared between two threads. + * + * @param {number} size Frame buffer length. + * @param {number} channelCount Total channel count. + */ + constructor(size: number, channelCount: number = 1) { + this.states = new Uint32Array( + new SharedArrayBuffer( + Object.keys(this.States).length * Uint32Array.BYTES_PER_ELEMENT + ) + ) + /** + * Use one extra bin to distinguish between the read and write indices + * when full. See Tim Blechmann's |boost::lockfree::spsc_queue| + * implementation. + */ + this.bufferLength = size + 1 + this.channelCount = channelCount + this.channelData = [] + for (let i = 0; i < channelCount; i++) { + this.channelData.push( + new Float32Array( + new SharedArrayBuffer( + this.bufferLength * Float32Array.BYTES_PER_ELEMENT + ) + ) + ) + } + } + + /** + * Helper function for creating FreeQueue from pointers. + * @param {FreeQueuePointers} queuePointers + * An object containing various pointers required to create FreeQueue + * + * interface FreeQueuePointers { + * memory: WebAssembly.Memory; // Reference to WebAssembly Memory + * bufferLengthPointer: number; + * channelCountPointer: number; + * statePointer: number; + * channelDataPointer: number; + * } + * @returns FreeQueue + */ + static fromPointers(queuePointers: { + memory: WebAssembly.Memory + bufferLengthPointer: number + channelCountPointer: number + statePointer: number + channelDataPointer: number + }): FreeQueue { + const queue = new FreeQueue(0, 0) + const HEAPU32 = new Uint32Array(queuePointers.memory.buffer) + const HEAPF32 = new Float32Array(queuePointers.memory.buffer) + const bufferLength = HEAPU32[queuePointers.bufferLengthPointer / 4] + const channelCount = HEAPU32[queuePointers.channelCountPointer / 4] + const states = HEAPU32.subarray( + HEAPU32[queuePointers.statePointer / 4] / 4, + HEAPU32[queuePointers.statePointer / 4] / 4 + 2 + ) + const channelData: Float32Array[] = [] + for (let i = 0; i < channelCount; i++) { + channelData.push( + HEAPF32.subarray( + HEAPU32[HEAPU32[queuePointers.channelDataPointer / 4] / 4 + i] / 4, + HEAPU32[HEAPU32[queuePointers.channelDataPointer / 4] / 4 + i] / 4 + + bufferLength + ) + ) + } + queue.bufferLength = bufferLength + queue.channelCount = channelCount + queue.states = states + queue.channelData = channelData + return queue + } + + /** + * Pushes the data into queue. Used by producer. + * + * @param {Float32Array[]} input Its length must match with the channel + * count of this queue. + * @param {number} blockLength Input block frame length. It must be identical + * throughout channels. + * @return {boolean} False if the operation fails. + */ + push(input: Float32Array[], blockLength: number): boolean { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + if (this._getAvailableWrite(currentRead, currentWrite) < blockLength) { + return false + } + let nextWrite = currentWrite + blockLength + if (this.bufferLength < nextWrite) { + nextWrite -= this.bufferLength + for (let channel = 0; channel < this.channelCount; channel++) { + if (!input[channel]) continue + const blockA = this.channelData[channel].subarray(currentWrite) + const blockB = this.channelData[channel].subarray(0, nextWrite) + blockA.set(input[channel].subarray(0, blockA.length)) + blockB.set(input[channel].subarray(blockA.length)) + } + } else { + for (let channel = 0; channel < this.channelCount; channel++) { + if (!input[channel]) continue + this.channelData[channel] + .subarray(currentWrite, nextWrite) + .set(input[channel].subarray(0, blockLength)) + } + if (nextWrite === this.bufferLength) nextWrite = 0 + } + Atomics.store(this.states, this.States.WRITE, nextWrite) + return true + } + + /** + * Pulls data out of the queue. Used by consumer. + * + * @param {Float32Array[]} output Its length must match with the channel + * count of this queue. + * @param {number} blockLength output block length. It must be identical + * throughout channels. + * @return {boolean} False if the operation fails. + */ + pull(output: Float32Array[], blockLength: number): boolean { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + if (this._getAvailableRead(currentRead, currentWrite) < blockLength) { + return false + } + let nextRead = currentRead + blockLength + if (this.bufferLength < nextRead) { + nextRead -= this.bufferLength + for (let channel = 0; channel < this.channelCount; channel++) { + const blockA = this.channelData[channel].subarray(currentRead) + const blockB = this.channelData[channel].subarray(0, nextRead) + output[channel].set(blockA) + output[channel].set(blockB, blockA.length) + } + } else { + for (let channel = 0; channel < this.channelCount; ++channel) { + output[channel].set( + this.channelData[channel].subarray(currentRead, nextRead) + ) + } + if (nextRead === this.bufferLength) { + nextRead = 0 + } + } + Atomics.store(this.states, this.States.READ, nextRead) + return true + } + + /** + * Helper function for debugging. + * Prints currently available read and write. + */ + printAvailableReadAndWrite() { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + console.log(this, { + availableRead: this._getAvailableRead(currentRead, currentWrite), + availableWrite: this._getAvailableWrite(currentRead, currentWrite), + }) + } + + /** + * + * @returns {number} number of samples available for read + */ + getAvailableSamples(): number { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + return this._getAvailableRead(currentRead, currentWrite) + } + + /** + * + * @param {number} size + * @returns boolean. if frame of given size is available or not. + */ + isFrameAvailable(size: number): boolean { + return this.getAvailableSamples() >= size + } + + /** + * @return {number} + */ + getBufferLength(): number { + return this.bufferLength - 1 + } + + private _getAvailableWrite(readIndex: number, writeIndex: number): number { + if (writeIndex >= readIndex) + return this.bufferLength - writeIndex + readIndex - 1 + return readIndex - writeIndex - 1 + } + + private _getAvailableRead(readIndex: number, writeIndex: number): number { + if (writeIndex >= readIndex) return writeIndex - readIndex + return writeIndex + this.bufferLength - readIndex + } + + private _reset() { + for (let channel = 0; channel < this.channelCount; channel++) { + this.channelData[channel].fill(0) + } + Atomics.store(this.states, this.States.READ, 0) + Atomics.store(this.states, this.States.WRITE, 0) + } +} diff --git a/src/pages/DspDemo.tsx b/src/pages/DspSyncDemo.tsx similarity index 83% rename from src/pages/DspDemo.tsx rename to src/pages/DspSyncDemo.tsx index aa82464..90f7a32 100644 --- a/src/pages/DspDemo.tsx +++ b/src/pages/DspSyncDemo.tsx @@ -7,7 +7,6 @@ import { DspEditor } from '~/src/comp/DspEditor.tsx' import type { AstNode } from '~/src/lang/interpreter.ts' import { tokenize } from '~/src/lang/tokenize.ts' import { Canvas } from '~/src/ui/Canvas.tsx' -import type { Editor } from '~/src/ui/Editor.tsx' import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' import { H2 } from '~/src/ui/Heading.tsx' @@ -16,7 +15,7 @@ const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Floa const getBuffer = Lru(20, (length: number) => wasmDsp.alloc(Float32Array, length), item => item.fill(0), item => item.free()) const getPointers = Lru(20, (length: number) => wasmDsp.alloc(Uint32Array, length), item => item.fill(0), item => item.free()) -export function DspDemo() { +export function DspSyncDemo() { using $ = Sigui() const info = $({ @@ -32,11 +31,9 @@ export function DspDemo() { const ctx = new AudioContext({ sampleRate: 48000 }) const dsp = Dsp({ sampleRate: ctx.sampleRate }) - const { clock } = dsp const sound = dsp.Sound() - const barsCount = .25 - const length = 8192 //Math.floor(barsCount * clock.sampleRate / clock.coeff) + const length = 8192 const canvas = as HTMLCanvasElement const gfx = Gfx({ canvas }) @@ -55,38 +52,32 @@ export function DspDemo() { queueMicrotask(() => { $.fx(() => { - const { pane } = dspEditor.info + const { pane } = dspEditor.editor.info const { codeVisual } = pane.buffer.info $() pane.draw.widgets.deco.clear() plot.info.floats.fill(0) - let nodeCount = 0 + try { const tokens = Array.from(tokenize({ code: codeVisual })) + sound.reset() - const { program, out } = sound.process(tokens, 6, false, 0) - if (!out.LR) { - throw new Error('No audio in the stack!') - } + const { program, out } = sound.process(tokens) + if (!out.LR) throw new Error('No audio in the stack!') + const floats = getFloats('floats', length) info.floats = floats - const notes = [] - const params = [] - const notesData = getBuffer(notes.length * 4) - const paramsData = getPointers(params.length * 2) + wasmDsp.fillSound( sound.sound$, sound.ops.ptr, - notesData.ptr, - notes.length, - paramsData.ptr, - params.length, out.LR.getAudio(), 0, floats.length, floats.ptr, ) + plot.info.floats.set(floats) program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => @@ -138,14 +129,14 @@ export function DspDemo() { }) }) - const dspEditor = as Editor + const dspEditor = DspEditor({ + width: info.$.width, + height: info.$.height, + code: info.$.code, + }) return
-

Dsp demo

+

Dsp Sync demo

{dspEditor} {canvas}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 20ca5af..2c508c3 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -28,7 +28,10 @@ export function Home() { Canvas WebGL Editor - Dsp + Dsp Sync + Dsp Async + Dsp Node + Worker-Worklet AssemblyScript QrCode About diff --git a/src/pages/WorkerWorklet/WorkerWorkletDemo.tsx b/src/pages/WorkerWorklet/WorkerWorkletDemo.tsx new file mode 100644 index 0000000..570bc32 --- /dev/null +++ b/src/pages/WorkerWorklet/WorkerWorkletDemo.tsx @@ -0,0 +1,58 @@ +import { Sigui } from 'sigui' +import basicProcessorUrl from '~/src/pages/WorkerWorklet/basic-processor.ts?url' +import { QUEUE_SIZE } from '~/src/pages/WorkerWorklet/constants.ts' +import { FreeQueue } from '~/src/pages/WorkerWorklet/free-queue.ts' +import WorkerFactory from '~/src/pages/WorkerWorklet/worker.ts?worker' +import { H2 } from '~/src/ui/Heading.tsx' +import { Input } from '~/src/ui/Input.tsx' + +export function WorkerWorkletDemo() { + using $ = Sigui() + + const inputQueue = new FreeQueue(QUEUE_SIZE, 1) + const outputQueue = new FreeQueue(QUEUE_SIZE, 1) + // Create an atomic state for synchronization between worker and AudioWorklet. + const atomicState = new Int32Array(new SharedArrayBuffer(1 * Int32Array.BYTES_PER_ELEMENT)) + const cmd = new Uint8Array(new SharedArrayBuffer(1 * Uint8Array.BYTES_PER_ELEMENT)) + const state = new Uint8Array(new SharedArrayBuffer(128 * Uint8Array.BYTES_PER_ELEMENT)) + + const worker = new WorkerFactory() + worker.postMessage({ + type: 'init', + data: { + inputQueue, + outputQueue, + atomicState, + cmd, + state, + } + }) + + const audioContext = new AudioContext({ latencyHint: 0.000001 }) + $.fx(() => { + $().then(async () => { + await audioContext.audioWorklet.addModule(basicProcessorUrl) + + const processorNode = new AudioWorkletNode(audioContext, 'basic-processor', { + processorOptions: { + inputQueue, + outputQueue, + atomicState, + } + }) + + const osc = new OscillatorNode(audioContext) + osc.connect(processorNode).connect(audioContext.destination) + }) + }) + + return
+

Worker-Worklet Demo

+ { + const hz = (e.target as HTMLInputElement).valueAsNumber + const encoded = new TextEncoder().encode(JSON.stringify({ hz })) + state.set(encoded) + cmd[0] = encoded.byteLength + }} /> +
+} diff --git a/src/pages/WorkerWorklet/basic-processor.ts b/src/pages/WorkerWorklet/basic-processor.ts new file mode 100644 index 0000000..88570b9 --- /dev/null +++ b/src/pages/WorkerWorklet/basic-processor.ts @@ -0,0 +1,53 @@ +import { FRAME_SIZE, RENDER_QUANTUM } from '~/src/pages/WorkerWorklet/constants.ts' +import { FreeQueue } from '~/src/pages/WorkerWorklet/free-queue.ts' + +/** + * A simple AudioWorkletProcessor node. + * + * @class BasicProcessor + * @extends AudioWorkletProcessor + */ +class BasicProcessor extends AudioWorkletProcessor { + inputQueue: FreeQueue + outputQueue: FreeQueue + atomicState: Int32Array + + /** + * Constructor to initialize, input and output FreeQueue instances + * and atomicState to synchronise Worker with AudioWorklet + * @param {Object} options AudioWorkletProcessor options + * to initialize inputQueue, outputQueue and atomicState + */ + constructor(options: AudioWorkletNodeOptions) { + super() + + this.inputQueue = options.processorOptions.inputQueue + this.outputQueue = options.processorOptions.outputQueue + this.atomicState = options.processorOptions.atomicState + Object.setPrototypeOf(this.inputQueue, FreeQueue.prototype) + Object.setPrototypeOf(this.outputQueue, FreeQueue.prototype) + } + + process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean { + const input = inputs[0] + const output = outputs[0] + + // Push data from input into inputQueue. + this.inputQueue.push(input, RENDER_QUANTUM) + + // Try to pull data out of outputQueue and store it in output. + const didPull = this.outputQueue.pull(output, RENDER_QUANTUM) + if (!didPull) { + // console.log("failed to pull.") + } + + // Wake up worker to process a frame of data. + if (this.inputQueue.isFrameAvailable(FRAME_SIZE)) { + Atomics.notify(this.atomicState, 0, 1) + } + + return true + } +} + +registerProcessor('basic-processor', BasicProcessor) diff --git a/src/pages/WorkerWorklet/constants.ts b/src/pages/WorkerWorklet/constants.ts new file mode 100644 index 0000000..88b1c9e --- /dev/null +++ b/src/pages/WorkerWorklet/constants.ts @@ -0,0 +1,4 @@ +export const KERNEL_LENGTH = 30 +export const RENDER_QUANTUM = 128 +export const FRAME_SIZE = KERNEL_LENGTH * RENDER_QUANTUM +export const QUEUE_SIZE = 4096 diff --git a/src/pages/WorkerWorklet/free-queue.ts b/src/pages/WorkerWorklet/free-queue.ts new file mode 100644 index 0000000..96e49d4 --- /dev/null +++ b/src/pages/WorkerWorklet/free-queue.ts @@ -0,0 +1,254 @@ +/** + * A shared storage for FreeQueue operation backed by SharedArrayBuffer. + * + * @typedef SharedRingBuffer + * @property {Uint32Array} states Backed by SharedArrayBuffer. + * @property {number} bufferLength The frame buffer length. Should be identical + * throughout channels. + * @property {Array} channelData The length must be > 0. + * @property {number} channelCount same with channelData.length + */ + +interface SharedRingBuffer { + states: Uint32Array + bufferLength: number + channelData: Float32Array[] + channelCount: number +} + +/** + * A single-producer/single-consumer lock-free FIFO backed by SharedArrayBuffer. + * In a typical pattern is that a worklet pulls the data from the queue and a + * worker renders audio data to fill in the queue. + */ + +export class FreeQueue { + states: Uint32Array + bufferLength: number + channelCount: number + channelData: Float32Array[] + + /** + * An index set for shared state fields. Requires atomic access. + * @enum {number} + */ + States = { + /** @type {number} A shared index for reading from the queue. (consumer) */ + READ: 0, + /** @type {number} A shared index for writing into the queue. (producer) */ + WRITE: 1, + }; + + /** + * FreeQueue constructor. A shared buffer created by this constuctor + * will be shared between two threads. + * + * @param {number} size Frame buffer length. + * @param {number} channelCount Total channel count. + */ + constructor(size: number, channelCount: number = 1) { + this.states = new Uint32Array( + new SharedArrayBuffer( + Object.keys(this.States).length * Uint32Array.BYTES_PER_ELEMENT + ) + ) + /** + * Use one extra bin to distinguish between the read and write indices + * when full. See Tim Blechmann's |boost::lockfree::spsc_queue| + * implementation. + */ + this.bufferLength = size + 1 + this.channelCount = channelCount + this.channelData = [] + for (let i = 0; i < channelCount; i++) { + this.channelData.push( + new Float32Array( + new SharedArrayBuffer( + this.bufferLength * Float32Array.BYTES_PER_ELEMENT + ) + ) + ) + } + } + + /** + * Helper function for creating FreeQueue from pointers. + * @param {FreeQueuePointers} queuePointers + * An object containing various pointers required to create FreeQueue + * + * interface FreeQueuePointers { + * memory: WebAssembly.Memory; // Reference to WebAssembly Memory + * bufferLengthPointer: number; + * channelCountPointer: number; + * statePointer: number; + * channelDataPointer: number; + * } + * @returns FreeQueue + */ + static fromPointers(queuePointers: { + memory: WebAssembly.Memory + bufferLengthPointer: number + channelCountPointer: number + statePointer: number + channelDataPointer: number + }): FreeQueue { + const queue = new FreeQueue(0, 0) + const HEAPU32 = new Uint32Array(queuePointers.memory.buffer) + const HEAPF32 = new Float32Array(queuePointers.memory.buffer) + const bufferLength = HEAPU32[queuePointers.bufferLengthPointer / 4] + const channelCount = HEAPU32[queuePointers.channelCountPointer / 4] + const states = HEAPU32.subarray( + HEAPU32[queuePointers.statePointer / 4] / 4, + HEAPU32[queuePointers.statePointer / 4] / 4 + 2 + ) + const channelData: Float32Array[] = [] + for (let i = 0; i < channelCount; i++) { + channelData.push( + HEAPF32.subarray( + HEAPU32[HEAPU32[queuePointers.channelDataPointer / 4] / 4 + i] / 4, + HEAPU32[HEAPU32[queuePointers.channelDataPointer / 4] / 4 + i] / 4 + + bufferLength + ) + ) + } + queue.bufferLength = bufferLength + queue.channelCount = channelCount + queue.states = states + queue.channelData = channelData + return queue + } + + /** + * Pushes the data into queue. Used by producer. + * + * @param {Float32Array[]} input Its length must match with the channel + * count of this queue. + * @param {number} blockLength Input block frame length. It must be identical + * throughout channels. + * @return {boolean} False if the operation fails. + */ + push(input: Float32Array[], blockLength: number): boolean { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + if (this._getAvailableWrite(currentRead, currentWrite) < blockLength) { + return false + } + let nextWrite = currentWrite + blockLength + if (this.bufferLength < nextWrite) { + nextWrite -= this.bufferLength + for (let channel = 0; channel < this.channelCount; channel++) { + if (!input[channel]) continue + const blockA = this.channelData[channel].subarray(currentWrite) + const blockB = this.channelData[channel].subarray(0, nextWrite) + blockA.set(input[channel].subarray(0, blockA.length)) + blockB.set(input[channel].subarray(blockA.length)) + } + } else { + for (let channel = 0; channel < this.channelCount; channel++) { + if (!input[channel]) continue + this.channelData[channel] + .subarray(currentWrite, nextWrite) + .set(input[channel].subarray(0, blockLength)) + } + if (nextWrite === this.bufferLength) nextWrite = 0 + } + Atomics.store(this.states, this.States.WRITE, nextWrite) + return true + } + + /** + * Pulls data out of the queue. Used by consumer. + * + * @param {Float32Array[]} output Its length must match with the channel + * count of this queue. + * @param {number} blockLength output block length. It must be identical + * throughout channels. + * @return {boolean} False if the operation fails. + */ + pull(output: Float32Array[], blockLength: number): boolean { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + if (this._getAvailableRead(currentRead, currentWrite) < blockLength) { + return false + } + let nextRead = currentRead + blockLength + if (this.bufferLength < nextRead) { + nextRead -= this.bufferLength + for (let channel = 0; channel < this.channelCount; channel++) { + const blockA = this.channelData[channel].subarray(currentRead) + const blockB = this.channelData[channel].subarray(0, nextRead) + output[channel].set(blockA) + output[channel].set(blockB, blockA.length) + } + } else { + for (let channel = 0; channel < this.channelCount; ++channel) { + output[channel].set( + this.channelData[channel].subarray(currentRead, nextRead) + ) + } + if (nextRead === this.bufferLength) { + nextRead = 0 + } + } + Atomics.store(this.states, this.States.READ, nextRead) + return true + } + + /** + * Helper function for debugging. + * Prints currently available read and write. + */ + printAvailableReadAndWrite() { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + console.log(this, { + availableRead: this._getAvailableRead(currentRead, currentWrite), + availableWrite: this._getAvailableWrite(currentRead, currentWrite), + }) + } + + /** + * + * @returns {number} number of samples available for read + */ + getAvailableSamples(): number { + const currentRead = Atomics.load(this.states, this.States.READ) + const currentWrite = Atomics.load(this.states, this.States.WRITE) + return this._getAvailableRead(currentRead, currentWrite) + } + + /** + * + * @param {number} size + * @returns boolean. if frame of given size is available or not. + */ + isFrameAvailable(size: number): boolean { + return this.getAvailableSamples() >= size + } + + /** + * @return {number} + */ + getBufferLength(): number { + return this.bufferLength - 1 + } + + private _getAvailableWrite(readIndex: number, writeIndex: number): number { + if (writeIndex >= readIndex) + return this.bufferLength - writeIndex + readIndex - 1 + return readIndex - writeIndex - 1 + } + + private _getAvailableRead(readIndex: number, writeIndex: number): number { + if (writeIndex >= readIndex) return writeIndex - readIndex + return writeIndex + this.bufferLength - readIndex + } + + private _reset() { + for (let channel = 0; channel < this.channelCount; channel++) { + this.channelData[channel].fill(0) + } + Atomics.store(this.states, this.States.READ, 0) + Atomics.store(this.states, this.States.WRITE, 0) + } +} diff --git a/src/pages/WorkerWorklet/worker.ts b/src/pages/WorkerWorklet/worker.ts new file mode 100644 index 0000000..1bc40e7 --- /dev/null +++ b/src/pages/WorkerWorklet/worker.ts @@ -0,0 +1,57 @@ +import { FRAME_SIZE } from "./constants.ts" +import { FreeQueue } from "./free-queue.ts" + +/** + * Worker message event handler. + * This will initialize worker with FreeQueue instance and set loop for audio + * processing. + */ +self.onmessage = (msg) => { + if (msg.data.type === "init") { + let { inputQueue, outputQueue, atomicState, cmd, state } = msg.data.data as { + inputQueue: FreeQueue + outputQueue: FreeQueue + atomicState: Int32Array + cmd: Uint8Array + state: Uint8Array + } + Object.setPrototypeOf(inputQueue, FreeQueue.prototype) + Object.setPrototypeOf(outputQueue, FreeQueue.prototype) + + // buffer for storing data pulled out from queue. + const input = new Float32Array(FRAME_SIZE) + + let hz = 300 + let phase = 0 + let t = 0 + const decoder = new TextDecoder() + // loop for processing data. + while (Atomics.wait(atomicState, 0, 0) === 'ok') { + + // pull data out from inputQueue. + const didPull = inputQueue.pull([input], FRAME_SIZE) + + if (didPull) { + // If pulling data out was successfull, process it and push it to + // outputQueue + const output = input.map(() => { + const s = Math.sin(phase) * 0.2 + phase += (1 / 48000) * hz * Math.PI * 2 + return s + }) + outputQueue.push([output], FRAME_SIZE) + } + + if (cmd[0]) { + const st = JSON.parse(decoder.decode(state.slice(0, cmd[0]))) as { hz: number } + hz = st.hz + // console.log('set hz', hz) + cmd[0] = 0 + } + + // } + + Atomics.store(atomicState, 0, 0) + } + } +} diff --git a/src/ui/editor/caret.ts b/src/ui/editor/caret.ts index 643ccad..969df1e 100644 --- a/src/ui/editor/caret.ts +++ b/src/ui/editor/caret.ts @@ -1,4 +1,4 @@ -import { beginOfLine, Point, type Buffer, type PaneInfo } from 'editor' +import { beginOfLine, Close, Open, Point, type Buffer, type PaneInfo } from 'editor' import { Sigui } from 'sigui' import { assign, clamp } from 'utils' @@ -16,8 +16,8 @@ export function Caret({ buffer }: { visual: $(Point()), visualXIntent: 0, blinkReset: 0, - isBlink: false, - isVisible: false, + isBlink: true, + isVisible: true, }) const caret = info @@ -52,7 +52,11 @@ export function Caret({ buffer }: { if (beginOfLine(lines[caret.y]) === caret.x && caret.x > 0) { chars = (2 - (caret.x % 2)) } - buffer.code = code.slice(0, caret.index - chars) + code.slice(caret.index) + const before = code[caret.index - 1] + const after = code[caret.index] + let index = caret.index + if (Open[before] && Close[after]) index++ + buffer.code = code.slice(0, caret.index - chars) + code.slice(index) moveByChars(-chars) $.flush() } diff --git a/src/ui/editor/kbd.tsx b/src/ui/editor/kbd.tsx index 9d83974..049ae60 100644 --- a/src/ui/editor/kbd.tsx +++ b/src/ui/editor/kbd.tsx @@ -1,4 +1,4 @@ -import { beginOfLine, Buffer, escapeRegExp, findMatchingBrackets, linecolToPoint, pointToLinecol, type Caret, type Dims, type History, type Misc, type PaneInfo, type Selection } from 'editor' +import { beginOfLine, Buffer, Close, escapeRegExp, findMatchingBrackets, linecolToPoint, Open, pointToLinecol, type Caret, type Dims, type History, type Misc, type PaneInfo, type Selection } from 'editor' import { Sigui } from 'sigui' import { assign } from 'utils' @@ -373,6 +373,12 @@ export function Kbd({ paneInfo, misc, dims, selection, buffer, caret, history }: // insert character withHistoryDebounced(() => withIntent(() => { + // auto-close brackets + if (Open[key] && buffer.code.at(caret.index) !== Open[key]) key += Open[key] + else if (Close[key] && buffer.code.at(caret.index) === key) { + caret.moveByChars(+1) + return + } caret.insert(key) caret.moveByChars(+1) }) diff --git a/src/ui/editor/widgets/error-sub.ts b/src/ui/editor/widgets/error-sub.ts new file mode 100644 index 0000000..89dfe7a --- /dev/null +++ b/src/ui/editor/widgets/error-sub.ts @@ -0,0 +1,31 @@ +import { Widget } from 'editor' +import { Sigui } from 'sigui' +import { drawText } from 'utils' + +export function ErrorSubWidget() { + using $ = Sigui() + + const info = $({ + color: '#f00', + error: null as null | Error + }) + + const widget = Widget() + + widget.draw = c => { + const { color, error } = info + if (!error) return + const { rect } = widget + const { x, y, w, h } = rect + c.beginPath() + c.moveTo(x, y - 2) + for (let sx = x; sx < x + w + 3; sx += 3) { + c.lineTo(sx, y - 2 + (sx % 2 ? 0 : 2)) + } + c.strokeStyle = color + c.stroke() + drawText(c, { x, y: y + 1 }, error.message, color, .025, color) + } + + return { info, widget } +} diff --git a/src/ui/editor/widgets/index.ts b/src/ui/editor/widgets/index.ts index f99fd25..af9295c 100644 --- a/src/ui/editor/widgets/index.ts +++ b/src/ui/editor/widgets/index.ts @@ -1,3 +1,4 @@ +export * from './error-sub.ts' export * from './hover-mark.ts' export * from './wave-canvas.ts' export * from './wave-gl.ts' diff --git a/vite.config.ts b/vite.config.ts index 80ad10f..242aed7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -109,6 +109,16 @@ export default ({ mode }) => { '--transform', './vendor/as-transform-update-dsp-gens.js', ] }), + ViteAssemblyScript({ + configFile: 'asconfig-dsp-nort.json', + projectRoot: '.', + srcMatch: 'as/assembly/dsp', + srcEntryFile: 'as/assembly/dsp/index.ts', + mapFile: './as/build/dsp.wasm.map', + extra: [ + '--transform', './vendor/as-transform-unroll.js', + ] + }), ViteAssemblyScript({ configFile: 'asconfig-pkg.json', projectRoot: '.', From 84facb6c2cbaf3ae5fad0fe7cc103252eb0fedc2 Mon Sep 17 00:00:00 2001 From: stagas Date: Sat, 19 Oct 2024 07:16:15 +0300 Subject: [PATCH 09/33] rename to DspWorkerDemo --- src/as/dsp/dsp.ts | 4 ++++ src/pages/App.tsx | 4 ++-- .../DspWorkerDemo.tsx} | 12 ++++++------ .../basic-processor.ts | 17 ++++++++++------- .../{DspNodeDemo => DspWorkerDemo}/constants.ts | 0 .../dsp-worker.ts | 14 +++++++++----- .../free-queue.ts | 12 ++++++++---- src/pages/Home.tsx | 2 +- 8 files changed, 40 insertions(+), 25 deletions(-) rename src/pages/{DspNodeDemo/DspNodeDemo.tsx => DspWorkerDemo/DspWorkerDemo.tsx} (93%) rename src/pages/{DspNodeDemo => DspWorkerDemo}/basic-processor.ts (79%) rename src/pages/{DspNodeDemo => DspWorkerDemo}/constants.ts (100%) rename src/pages/{DspNodeDemo => DspWorkerDemo}/dsp-worker.ts (84%) rename src/pages/{DspNodeDemo => DspWorkerDemo}/free-queue.ts (96%) diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index a13f7e4..4283e9f 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -79,6 +79,10 @@ export function Dsp({ sampleRate, core$ }: { const context = getContext() + // TODO: ops/setup_ops need to be created new + // on every new code + // all of the context is prepared + // and then sent in one byte to the worklet const ops = wasm.alloc(Int32Array, MAX_OPS) const vm = createVm(ops) const setup_ops = wasm.alloc(Int32Array, MAX_OPS) diff --git a/src/pages/App.tsx b/src/pages/App.tsx index d592ffd..b5c9960 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -11,8 +11,8 @@ import { AssemblyScript } from '~/src/pages/AssemblyScript.tsx' import { CanvasDemo } from '~/src/pages/CanvasDemo' import { Chat } from '~/src/pages/Chat/Chat.tsx' import { DspAsyncDemo } from '~/src/pages/DspAsyncDemo.tsx' -import { DspNodeDemo } from '~/src/pages/DspNodeDemo/DspNodeDemo.tsx' import { DspSyncDemo } from '~/src/pages/DspSyncDemo' +import { DspWorkerDemo } from '~/src/pages/DspWorkerDemo/DspWorkerDemo' import { EditorDemo } from '~/src/pages/EditorDemo.tsx' import { Home } from '~/src/pages/Home.tsx' import { OAuthRegister } from '~/src/pages/OAuthRegister.tsx' @@ -46,7 +46,7 @@ export function App() { '/editor': () => , '/dsp-sync': () => , '/dsp-async': () => , - '/dsp-node': () => , + '/dsp-worker': () => , '/worker-worklet': () => , '/asc': () => , '/qrcode': () => , diff --git a/src/pages/DspNodeDemo/DspNodeDemo.tsx b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx similarity index 93% rename from src/pages/DspNodeDemo/DspNodeDemo.tsx rename to src/pages/DspWorkerDemo/DspWorkerDemo.tsx index 28dab90..7445042 100644 --- a/src/pages/DspNodeDemo/DspNodeDemo.tsx +++ b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx @@ -3,18 +3,18 @@ import { Sigui } from 'sigui' import { assign, Lru, rpc } from 'utils' import { PreviewService } from '~/src/as/dsp/preview-service.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' -import basicProcessorUrl from '~/src/pages/DspNodeDemo/basic-processor.ts?url' -import { QUEUE_SIZE } from '~/src/pages/DspNodeDemo/constants.ts' -import { DspWorker } from '~/src/pages/DspNodeDemo/dsp-worker.ts' -import DspWorkerFactory from '~/src/pages/DspNodeDemo/dsp-worker.ts?worker' -import { FreeQueue } from '~/src/pages/DspNodeDemo/free-queue.ts' +import basicProcessorUrl from '~/src/pages/DspWorkerDemo/basic-processor.ts?url' +import { QUEUE_SIZE } from '~/src/pages/DspWorkerDemo/constants' +import { DspWorker } from '~/src/pages/DspWorkerDemo/dsp-worker' +import DspWorkerFactory from '~/src/pages/DspWorkerDemo/dsp-worker.ts?worker' +import { FreeQueue } from '~/src/pages/DspWorkerDemo/free-queue' import { Canvas } from '~/src/ui/Canvas.tsx' import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' import { H3 } from '~/src/ui/Heading.tsx' const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) -export function DspNodeDemo() { +export function DspWorkerDemo() { using $ = Sigui() const info = $({ diff --git a/src/pages/DspNodeDemo/basic-processor.ts b/src/pages/DspWorkerDemo/basic-processor.ts similarity index 79% rename from src/pages/DspNodeDemo/basic-processor.ts rename to src/pages/DspWorkerDemo/basic-processor.ts index 88570b9..3b9e5a4 100644 --- a/src/pages/DspNodeDemo/basic-processor.ts +++ b/src/pages/DspWorkerDemo/basic-processor.ts @@ -29,22 +29,25 @@ class BasicProcessor extends AudioWorkletProcessor { } process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean { - const input = inputs[0] + // const input = inputs[0] const output = outputs[0] // Push data from input into inputQueue. - this.inputQueue.push(input, RENDER_QUANTUM) + // this.inputQueue.push(input, RENDER_QUANTUM) // Try to pull data out of outputQueue and store it in output. - const didPull = this.outputQueue.pull(output, RENDER_QUANTUM) - if (!didPull) { - // console.log("failed to pull.") - } + // const didPull = + this.outputQueue.pull(output, RENDER_QUANTUM) + // if (!didPull) { + // // console.log("failed to pull.") + // } // Wake up worker to process a frame of data. - if (this.inputQueue.isFrameAvailable(FRAME_SIZE)) { + // if (this.inputQueue.isFrameAvailable(FRAME_SIZE)) { + if (this.outputQueue.getAvailableSamples() < FRAME_SIZE) { Atomics.notify(this.atomicState, 0, 1) } + // } return true } diff --git a/src/pages/DspNodeDemo/constants.ts b/src/pages/DspWorkerDemo/constants.ts similarity index 100% rename from src/pages/DspNodeDemo/constants.ts rename to src/pages/DspWorkerDemo/constants.ts diff --git a/src/pages/DspNodeDemo/dsp-worker.ts b/src/pages/DspWorkerDemo/dsp-worker.ts similarity index 84% rename from src/pages/DspNodeDemo/dsp-worker.ts rename to src/pages/DspWorkerDemo/dsp-worker.ts index 92640eb..a42b703 100644 --- a/src/pages/DspNodeDemo/dsp-worker.ts +++ b/src/pages/DspWorkerDemo/dsp-worker.ts @@ -7,8 +7,8 @@ self.document = { import { Dsp, wasm } from 'dsp' import { Lru, rpc } from 'utils' import { tokenize } from '~/src/lang/tokenize.ts' -import { FRAME_SIZE } from '~/src/pages/DspNodeDemo/constants.ts' -import { FreeQueue } from '~/src/pages/DspNodeDemo/free-queue.ts' +import { FRAME_SIZE } from '~/src/pages/DspWorkerDemo/constants' +import { FreeQueue } from '~/src/pages/DspWorkerDemo/free-queue' export type DspWorker = typeof worker @@ -46,11 +46,11 @@ const worker = { while (Atomics.wait(atomicState, 0, 0) === 'ok') { // pull data out from inputQueue. - const didPull = inputQueue.pull([input], FRAME_SIZE) + // const didPull = inputQueue.pull([input], FRAME_SIZE) // If pulling data out was successfull, process it and push it to // outputQueue - if (didPull) { + if (outputQueue.getAvailableSamples() < FRAME_SIZE) { if (LR >= 0) { wasm.fillSound( sound.sound$, @@ -76,7 +76,11 @@ const worker = { cmd[0] = 0 } - Atomics.store(atomicState, 0, 0) + if (outputQueue.getAvailableSamples() >= FRAME_SIZE) { + Atomics.store(atomicState, 0, 0) + } + + // outputQueue.printAvailableReadAndWrite() } }, } diff --git a/src/pages/DspNodeDemo/free-queue.ts b/src/pages/DspWorkerDemo/free-queue.ts similarity index 96% rename from src/pages/DspNodeDemo/free-queue.ts rename to src/pages/DspWorkerDemo/free-queue.ts index 96e49d4..006c5b0 100644 --- a/src/pages/DspNodeDemo/free-queue.ts +++ b/src/pages/DspWorkerDemo/free-queue.ts @@ -201,10 +201,14 @@ export class FreeQueue { printAvailableReadAndWrite() { const currentRead = Atomics.load(this.states, this.States.READ) const currentWrite = Atomics.load(this.states, this.States.WRITE) - console.log(this, { - availableRead: this._getAvailableRead(currentRead, currentWrite), - availableWrite: this._getAvailableWrite(currentRead, currentWrite), - }) + console.log( + this._getAvailableRead(currentRead, currentWrite), + this._getAvailableWrite(currentRead, currentWrite) + ) + // console.log(this, { + // availableRead: this._getAvailableRead(currentRead, currentWrite), + // availableWrite: this._getAvailableWrite(currentRead, currentWrite), + // }) } /** diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 2c508c3..f5b9e24 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -30,7 +30,7 @@ export function Home() { Editor Dsp Sync Dsp Async - Dsp Node + Dsp Worker Worker-Worklet AssemblyScript QrCode From d7c9a480f66980d7446a52a5b60b8031f16d5385 Mon Sep 17 00:00:00 2001 From: stagas Date: Sat, 19 Oct 2024 20:27:22 +0300 Subject: [PATCH 10/33] dsp node working --- as/assembly/dsp/constants.ts | 9 +- as/assembly/dsp/gen/nrate.ts | 1 - as/assembly/dsp/index.ts | 47 +- as/assembly/dsp/shared.ts | 13 +- as/assembly/dsp/vm/dsp-shared.ts | 7 - as/assembly/dsp/vm/dsp.ts | 5 +- as/assembly/dsp/vm/player.ts | 34 + as/assembly/dsp/vm/sound.ts | 131 ++-- as/assembly/player/player.ts | 39 -- asconfig-dsp-nort.json | 4 +- generated/assembly/dsp-offsets.ts | 6 +- generated/assembly/dsp-runner.ts | 56 +- generated/typescript/dsp-gens.ts | 2 +- scripts/generate-dsp-vm.ts | 3 +- src/as/dsp/dsp-node.ts | 608 ++++++++++++++++++ src/as/dsp/dsp-worklet.ts | 202 ++++++ src/as/dsp/dsp.ts | 7 +- src/as/dsp/preview-worker.ts | 2 +- src/as/dsp/shared.ts | 15 +- src/as/dsp/value.ts | 2 +- src/lang/interpreter.ts | 6 +- src/pages/App.tsx | 2 + .../{DspNodeDemo.tsx_ => DspNodeDemo.tsx} | 59 +- src/pages/DspWorkerDemo/DspWorkerDemo.tsx | 1 - src/pages/Home.tsx | 1 + 25 files changed, 1040 insertions(+), 222 deletions(-) create mode 100644 as/assembly/dsp/vm/player.ts delete mode 100644 as/assembly/player/player.ts create mode 100644 src/as/dsp/dsp-node.ts create mode 100644 src/as/dsp/dsp-worklet.ts rename src/pages/{DspNodeDemo.tsx_ => DspNodeDemo.tsx} (73%) diff --git a/as/assembly/dsp/constants.ts b/as/assembly/dsp/constants.ts index 2ea495b..7d7ace6 100644 --- a/as/assembly/dsp/constants.ts +++ b/as/assembly/dsp/constants.ts @@ -1,8 +1,9 @@ -export const MAX_SCALARS = 4096 -export const MAX_LITERALS = 4096 +export const BUFFER_SIZE = 8192 export const MAX_FLOATS = 4096 export const MAX_LISTS = 4096 -export const MAX_TRACKS = 16 +export const MAX_LITERALS = 4096 +export const MAX_OPS = 4096 export const MAX_RMSS = 1024 +export const MAX_SCALARS = 4096 export const MAX_SOUNDS = 16 -export const BUFFER_SIZE = 8192 +export const MAX_TRACKS = 16 diff --git a/as/assembly/dsp/gen/nrate.ts b/as/assembly/dsp/gen/nrate.ts index b2e4c37..0a33619 100644 --- a/as/assembly/dsp/gen/nrate.ts +++ b/as/assembly/dsp/gen/nrate.ts @@ -1,5 +1,4 @@ import { rateToPhaseStep } from '../../util' -import { Engine } from '../core/engine' import { Gen } from './gen' export class Nrate extends Gen { diff --git a/as/assembly/dsp/index.ts b/as/assembly/dsp/index.ts index 7d59b55..4c267b4 100644 --- a/as/assembly/dsp/index.ts +++ b/as/assembly/dsp/index.ts @@ -1,7 +1,8 @@ -import { Player } from '../player/player' +import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from './constants' import { Clock } from './core/clock' import { Core, Engine } from './core/engine' -import { Out, PlayerTrack } from './shared' +import { Out, Track } from './shared' +import { Player } from './vm/player' import { Sound } from './vm/sound' export * from '../../../generated/assembly/dsp-factory' @@ -20,12 +21,12 @@ export function getEngineClock(engine$: usize): usize { return changetype(changetype(engine$).clock) } -export function resetClock(clock$: usize): void { +export function clockReset(clock$: usize): void { const clock = changetype(clock$) clock.reset() } -export function updateClock(clock$: usize): void { +export function clockUpdate(clock$: usize): void { const clock = changetype(clock$) clock.update() } @@ -80,27 +81,39 @@ export function getSoundLists(sound$: usize): usize { return changetype(changetype(sound$).lists) } -export function createOut(): usize { - return changetype(new Out()) +export function createPlayer(sound$: usize, out$: usize): usize { + return changetype(new Player(sound$, out$)) } -export function createPlayer(): usize { - return changetype(new Player()) +export function getPlayerTrackOffset(): usize { + return offsetof('track$') } -export function getPlayerOut(player$: usize): usize { - return changetype(changetype(player$).out) +export function setPlayerTrack(player$: usize, track$: usize): void { + changetype(player$).track$ = track$ } -export function getPlayerTracks(player$: usize): usize { - return changetype(changetype(player$).tracks) +export function playerProcess(player$: usize, begin: u32, end: u32): void { + const player = changetype(player$) + player.process(begin, end) } -export function createPlayerTrack(): usize { - return changetype(new PlayerTrack()) +export function createOut(): usize { + return changetype(new Out()) } -export function playerProcess(player$: usize, begin: u32, end: u32): void { - const player = changetype(player$) - player.process(begin, end) +export function createTrack(): usize { + return changetype(new Track()) +} + +export function createOps(): usize { + return changetype(new StaticArray(MAX_OPS)) +} + +export function createLiterals(): usize { + return changetype(new StaticArray(MAX_LITERALS)) +} + +export function createLists(): usize { + return changetype(new StaticArray(MAX_LISTS)) } diff --git a/as/assembly/dsp/shared.ts b/as/assembly/dsp/shared.ts index f12ae12..d8736d6 100644 --- a/as/assembly/dsp/shared.ts +++ b/as/assembly/dsp/shared.ts @@ -1,13 +1,10 @@ @unmanaged -export class PlayerTrack { - sound$: usize = 0 - ops$: usize = 0 - notes$: usize = 0 - notesCount: u32 = 0 - params$: usize = 0 - paramsCount: u32 = 0 +export class Track { + run_ops$: usize = 0 + setup_ops$: usize = 0 + literals$: usize = 0 + lists$: usize = 0 audio_LR$: i32 = 0 - out$: usize = 0 } @unmanaged diff --git a/as/assembly/dsp/vm/dsp-shared.ts b/as/assembly/dsp/vm/dsp-shared.ts index a7fcadc..c528ea6 100644 --- a/as/assembly/dsp/vm/dsp-shared.ts +++ b/as/assembly/dsp/vm/dsp-shared.ts @@ -18,10 +18,3 @@ export enum DspBinaryOp { Div, Pow, } - -export class SoundData { - constructor() { } - begin: u32 = 0 - end: u32 = 0 - pan: f32 = 0 -} diff --git a/as/assembly/dsp/vm/dsp.ts b/as/assembly/dsp/vm/dsp.ts index 3737f89..7142e8c 100644 --- a/as/assembly/dsp/vm/dsp.ts +++ b/as/assembly/dsp/vm/dsp.ts @@ -14,13 +14,14 @@ export class Dsp { @inline CreateGen(snd: Sound, kind_index: i32): void { - const gen = Factory[kind_index](snd.engine) + const Gen = Factory[kind_index] + const gen = Gen(snd.engine) snd.gens.push(gen) snd.offsets.push(Offsets[kind_index]) } @inline CreateAudios(snd: Sound, count: i32): void { - for (let x = 0; x < count; x++) { + for (let x = 0; x <= count; x++) { snd.audios.push(new StaticArray(BUFFER_SIZE)) } } diff --git a/as/assembly/dsp/vm/player.ts b/as/assembly/dsp/vm/player.ts new file mode 100644 index 0000000..0d87178 --- /dev/null +++ b/as/assembly/dsp/vm/player.ts @@ -0,0 +1,34 @@ +import { run as dspRun } from '../../../../generated/assembly/dsp-runner' +import { Track } from '../shared' +import { Sound } from './sound' + +export class Player { + sound: Sound + track$: usize = 0 + lastTrack$: usize = 0 + + constructor(public sound$: usize, public out$: usize) { + this.sound = changetype(sound$) + } + + process(begin: u32, end: u32): void { + const sound = this.sound + + const track$ = this.track$ + + if (track$ !== this.lastTrack$) { + this.lastTrack$ = track$ + // sound.reset() // ?? + // ideally we should compare gens and move + // the reused gens to the new context + sound.clear() + + const track = changetype(track$) + sound.literals = changetype>(track.literals$) + sound.lists = changetype>(track.lists$) + dspRun(this.sound$, track.setup_ops$) + } + // console.log(`${track$}`) + sound.fillTrack(track$, begin, end, this.out$) + } +} diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index 060a50e..ec04b24 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -3,8 +3,16 @@ import { BUFFER_SIZE, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_SCALARS } from '. import { Clock } from '../core/clock' import { Engine } from '../core/engine' import { Gen } from '../gen/gen' +import { Out, Track } from '../shared' import { SoundValueKind } from './dsp-shared' +const enum Globals { + sr, + t, + rt, + co, +} + export function ntof(n: f32): f32 { return 440 * 2 ** ((n - 69) / 12) } @@ -36,6 +44,7 @@ export class Sound { values: SoundValue[] = [] + @inline reset(): void { this.gens.forEach(gen => { gen._reset() @@ -44,6 +53,7 @@ export class Sound { this.scalars.fill(0) } + @inline clear(): void { this.gens = [] this.offsets = [] @@ -57,6 +67,62 @@ export class Sound { this.scalars[Globals.rt] = f32(c.time) } + @inline + fillTrack(track$: usize, begin: u32, end: u32, out$: usize): void { + const out = changetype(out$) + const track = changetype(track$) + const run_ops$ = track.run_ops$ + const audio_LR$ = track.audio_LR$ + // console.log(`what? ${run_ops$} ${audio_LR$}`) + const CHUNK_SIZE = 64 + let chunkCount = 0 + + const c = this.engine.clock + this.scalars[Globals.sr] = f32(c.sampleRate) + this.scalars[Globals.co] = f32(c.coeff) + + let i = begin + this.begin = i + this.end = i + dspRun(changetype(this), run_ops$) + + let time = c.time + let barTime = c.barTime + for (let x = i; x < end; x += BUFFER_SIZE) { + const chunkEnd = x + BUFFER_SIZE > end ? end - x : BUFFER_SIZE + + for (let i: u32 = 0; i < chunkEnd; i += CHUNK_SIZE) { + this.updateScalars(c) + + this.begin = i + this.end = i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE + dspRun(changetype(this), run_ops$) + + chunkCount++ + + c.time = time + f64(chunkCount * CHUNK_SIZE) * c.timeStep + c.barTime = barTime + f64(chunkCount * CHUNK_SIZE) * c.barTimeStep + } + time = c.time + barTime = c.barTime + + const audio = this.audios[audio_LR$] + const p = x << 2 + memory.copy( + out.L$ + p, + changetype(audio), + chunkEnd << 2 + ) + // copy left to right for now + memory.copy( + out.R$ + p, + out.L$ + p, + chunkEnd << 2 + ) + } + } + + @inline fill(ops$: usize, audio_LR$: i32, begin: u32, end: u32, out$: usize): void { const CHUNK_SIZE = 64 let chunkCount = 0 @@ -94,73 +160,10 @@ export class Sound { const audio = this.audios[audio_LR$] memory.copy( out$ + (x << 2), - changetype(audio) + (x << 2), + changetype(audio), chunkEnd << 2 ) } } } } - -const NOTE_SCALE_X: f64 = 16 - -const enum Globals { - sr, - t, - rt, - co, -} - -const MAX_VOICES = 6 - -const enum Voices { - n0 = 4, - n1 = 8, - n2 = 12, - n3 = 16, - n4 = 20, - n5 = 24, -} - -const enum Voice { - n, - f, - t, - v, -} - -const voices: i32[][] = [ - [Voices.n0, Voices.n0 + 1, Voices.n0 + 2, Voices.n0 + 3], - [Voices.n1, Voices.n1 + 1, Voices.n1 + 2, Voices.n1 + 3], - [Voices.n2, Voices.n2 + 1, Voices.n2 + 2, Voices.n2 + 3], - [Voices.n3, Voices.n3 + 1, Voices.n3 + 2, Voices.n3 + 3], - [Voices.n4, Voices.n4 + 1, Voices.n4 + 2, Voices.n4 + 3], - [Voices.n5, Voices.n5 + 1, Voices.n5 + 2, Voices.n5 + 3], -] - -const MAX_PARAMS = 6 - -const enum Params { - p0 = 28, - p1, - p2, - p3, - p4, - p5, -} - -const enum Param { - t, - l, - s, - a, -} - -const params: i32[][] = [ - [Params.p0, Params.p0 + 1, Params.p0 + 2, Params.p0 + 3], - [Params.p1, Params.p1 + 1, Params.p1 + 2, Params.p1 + 3], - [Params.p2, Params.p2 + 1, Params.p2 + 2, Params.p2 + 3], - [Params.p3, Params.p3 + 1, Params.p3 + 2, Params.p3 + 3], - [Params.p4, Params.p4 + 1, Params.p4 + 2, Params.p4 + 3], - [Params.p5, Params.p5 + 1, Params.p5 + 2, Params.p5 + 3], -] diff --git a/as/assembly/player/player.ts b/as/assembly/player/player.ts deleted file mode 100644 index f869270..0000000 --- a/as/assembly/player/player.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MAX_TRACKS } from '../dsp/constants' -import { add_audio_audio } from '../dsp/graph/math' -import { Out, PlayerTrack } from '../dsp/shared' -import { Sound } from '../dsp/vm/sound' - -export class Player { - constructor() { } - tracks: StaticArray = new StaticArray(MAX_TRACKS) - out: Out = new Out() - process(begin: u32, end: u32): void { - let sound: Sound - let track: PlayerTrack | null - for (let i = 0; i < this.tracks.length; i++) { - track = this.tracks[i] - if (track === null) break - sound = changetype(track.sound$) - sound.fill( - track.ops$, - track.audio_LR$, - begin, - end, - track.out$, - ) - // console.log(`${f32.load(track.ops$ + 16)}`) - add_audio_audio( - track.out$, - this.out.L$, - begin, - end, - this.out.L$ - ) - memory.copy( - this.out.R$ + (begin << 2), - this.out.L$ + (begin << 2), - (end - begin) << 2 - ) - } - } -} diff --git a/asconfig-dsp-nort.json b/asconfig-dsp-nort.json index 877b793..6ce91cc 100644 --- a/asconfig-dsp-nort.json +++ b/asconfig-dsp-nort.json @@ -30,8 +30,6 @@ "maximumMemory": 2000, "bindings": "raw", "runtime": false, - "exportRuntime": false, - "zeroFilledMemory": true, - "uncheckedBehavior": "always" + "exportRuntime": false } } diff --git a/generated/assembly/dsp-offsets.ts b/generated/assembly/dsp-offsets.ts index 21a294d..a0a0d65 100644 --- a/generated/assembly/dsp-offsets.ts +++ b/generated/assembly/dsp-offsets.ts @@ -55,7 +55,7 @@ import { Zero } from '../../as/assembly/dsp/gen/zero' export const Offsets: usize[][] = [ [offsetof('gain'),offsetof('attack'),offsetof('decay'),offsetof('sustain'),offsetof('release'),offsetof('on'),offsetof('off')], [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], - [offsetof('gain'),offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('in')], [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], @@ -102,8 +102,8 @@ export const Offsets: usize[][] = [ [offsetof('gain'),offsetof('in'),offsetof('cut'),offsetof('q')], [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], [offsetof('gain'),offsetof('in')], - [offsetof('gain'),offsetof('gain'),offsetof('in')], - [offsetof('gain'),offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('in')], + [offsetof('gain'),offsetof('in')], [offsetof('gain'),offsetof('ms'),offsetof('in')], [offsetof('gain'),offsetof('hz'),offsetof('trig'),offsetof('offset')], [offsetof('gain')] diff --git a/generated/assembly/dsp-runner.ts b/generated/assembly/dsp-runner.ts index d8d8d28..0bce961 100644 --- a/generated/assembly/dsp-runner.ts +++ b/generated/assembly/dsp-runner.ts @@ -21,38 +21,38 @@ export function run(sound$: usize, ops$: usize): void { snd, changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.CreateAudios: dsp.CreateAudios( snd, changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.CreateValues: dsp.CreateValues( snd, changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.AudioToScalar: dsp.AudioToScalar( snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.LiteralToAudio: dsp.LiteralToAudio( snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.Pick: dsp.Pick( snd, @@ -61,15 +61,15 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.Pan: dsp.Pan( snd, changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.SetValue: dsp.SetValue( snd, @@ -77,8 +77,8 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.SetValueDynamic: dsp.SetValueDynamic( snd, @@ -86,8 +86,8 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.SetProperty: dsp.SetProperty( snd, @@ -96,23 +96,23 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.UpdateGen: dsp.UpdateGen( snd, changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.ProcessAudio: dsp.ProcessAudio( snd, changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.ProcessAudioStereo: dsp.ProcessAudioStereo( snd, @@ -120,8 +120,8 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + case Op.BinaryOp: dsp.BinaryOp( snd, @@ -130,8 +130,8 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])), changetype(unchecked(ops[i++])) ) - continue - + continue + } // end switch } // end while } diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index fcfa179..24f5d6b 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Fri Oct 18 2024 21:53:09 GMT+0300 (Eastern European Summer Time) +// auto-generated Sat Oct 19 2024 20:05:07 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/scripts/generate-dsp-vm.ts b/scripts/generate-dsp-vm.ts index 1a82308..d2441a8 100644 --- a/scripts/generate-dsp-vm.ts +++ b/scripts/generate-dsp-vm.ts @@ -124,8 +124,7 @@ ${indent(4, args.map(([, type]) => `changetype<${type}>(unchecked(ops[i++]))` ).join(',\n'))} ) -continue -`).join('\n'))} + continue`).join('\n\n') + '\n')} } // end switch } // end while } diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/dsp-node.ts new file mode 100644 index 0000000..606cc71 --- /dev/null +++ b/src/as/dsp/dsp-node.ts @@ -0,0 +1,608 @@ +import { DEBUG, getAllProps, Value as ValueBase, type Value } from 'dsp' +import { Sigui } from 'sigui' +import { fromEntries, getMemoryView, keys, rpc, shallowCopy, type MemoryView } from 'utils' +import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' +import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' +import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' +import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' +import { DspWorklet, type DspProcessorOptions } from '~/src/as/dsp/dsp-worklet.ts' +import dspWorkletUrl from '~/src/as/dsp/dsp-worklet.ts?url' +import { DspWorkletMode, Track } from '~/src/as/dsp/shared.ts' +import { AstNode, interpret } from '~/src/lang/interpreter.ts' +import { Token, tokenize } from '~/src/lang/tokenize.ts' +import { parseNumber } from '~/src/lang/util.ts' + +const dspGensKeys = keys(dspGens) + +export class DspNode extends AudioWorkletNode { + constructor( + context: AudioContext, + public mode = new Uint8Array(new SharedArrayBuffer(1)) + ) { + super(context, 'dsp', { + numberOfInputs: 1, + numberOfOutputs: 1, + outputChannelCount: [2], + channelCount: 2, + processorOptions: { + mode, + } satisfies DspProcessorOptions + }) + } + get isPlaying() { + return this.mode[0] === DspWorkletMode.Play + } + get isPaused() { + return this.mode[0] === DspWorkletMode.Pause + } + reset() { + this.mode[0] = DspWorkletMode.Reset + } + stop() { + this.mode[0] = DspWorkletMode.Stop + } + play() { + if (this.context.state === 'suspended') { + (this.context as any).resume() + } + this.mode[0] = DspWorkletMode.Play + } + pause() { + this.mode[0] = DspWorkletMode.Pause + } +} + +const registeredContexts = new Set() + +type GenApi = { + (opt: Record): Value | [Value, Value] | void +} + +type BinaryOp = { + (lhs: number, rhs: number): number + (lhs: number, rhs: Value): Value + (lhs: Value, rhs: number): Value + (lhs: Value, rhs: Value): Value +} + +export interface DspApi { + globals: Record + math: Record + gen: Record + gen_st: Record + pan(value: Value | number): void + pick(values: T[], index: Value | number): Value +} + +const commutative = new Set([DspBinaryOp.Add, DspBinaryOp.Mul]) +const audioProps = new Set(['in', 'sidechain']) +const textProps = new Set(['text', 'id']) + +export function createDspNode(ctx: AudioContext) { + using $ = Sigui() + + const info = $({ + node: null as null | DspNode, + dsp: null as null | Awaited>, + view: null as null | MemoryView, + tracks: null as null | Track[], + code: null as null | string, + currTrack: 0, + nextTrack: 1, + isPlaying: false, + isPaused: false, + }) + + async function createNode() { + const node = new DspNode(ctx) + const worklet = rpc(node.port) + const sourcemapUrl = new URL('/as/build/dsp-nort.wasm.map', location.origin).href + const dsp = await worklet.setup({ sourcemapUrl }) + $.batch(() => { + info.node = node + info.dsp = dsp + }) + node.connect(ctx.destination) + } + + if (!registeredContexts.has(ctx)) { + registeredContexts.add(ctx) + ctx.audioWorklet + .addModule(dspWorkletUrl) + .then(createNode) + } + else { + createNode() + } + + const preTokens = Array.from(tokenize({ + // we implicit call [nrate 1] before our code + // so that the sample rate is reset. + code: `[nrate 1]` + // some builtin procedures + // + ` { .5* .5+ } norm= ` + // + ` { at= p= sp= 1 [inc sp co* at] clip - p^ } dec= ` + + ` [zero] ` + })) + + const postTokens = Array.from(tokenize({ + code: `@` + })) + + function getTrackContext(track: Track) { + const { view } = $.of(info) + const literalsf = view.getF32(track.literals$, MAX_LITERALS) + const listsi = view.getI32(track.lists$, MAX_LISTS) + return { + gens: 0, + values: 0, + floats: 0, + scalars: 0, + audios: 0, + literals: 0, + literalsf, + lists: 0, + listsi, + } + } + + const vms: Map = new Map() + const contexts: Map> = new Map() + const builds: Map> = new Map() + const hashes: Map = new Map() + + function TrackBuild(track: Track) { + const { runVm, setupVm } = vms.get(track)! + const context = contexts.get(track)! + const Value = ValueBase.Factory({ context, vm: runVm }) + + function binaryOp( + BinOp: DspBinaryOp, + LiteralLiteral: (a: number, b: number) => number + ): BinaryOp { + return function binaryOp(lhs: Value | number, rhs: Value | number) { + if (typeof lhs === 'number' && typeof rhs === 'number') { + return LiteralLiteral(lhs, rhs) + } + + let out = Value.Dynamic.create() + + let l: Value + let r: Value + + if (commutative.has(BinOp)) { + if (typeof lhs === 'number') { + if (typeof rhs === 'number') throw 'unreachable' + + l = rhs + r = Value.Literal.create(lhs) + } + else if (typeof rhs === 'number') { + l = lhs + r = Value.Literal.create(rhs) + } + else { + l = lhs + r = rhs + } + } + else { + if (typeof lhs === 'number') { + if (typeof rhs === 'number') throw 'unreachable' + + l = Value.Literal.create(lhs) + r = rhs + } + else if (typeof rhs === 'number') { + l = lhs + r = Value.Literal.create(rhs) + } + else { + l = lhs + r = rhs + } + } + + if (!l) { + throw new Error('Missing left operand.') + } + + if (!r) { + throw new Error('Missing right operand.') + } + + runVm.BinaryOp( + BinOp, + l.value$, + r.value$, + out.value$ + ) + + return out + } as BinaryOp + } + + function defineGen(name: keyof Gen, stereo: boolean) { + const props = getAllProps(name) + const Gen = dspGens[name] + const kind_index = dspGensKeys.indexOf(name) + + function handle(opt: Record) { + const gen$ = context.gens++ + setupVm.CreateGen(kind_index) + DEBUG && console.log('CreateGen', name, gen$) + + for (let p in opt) { + const prop = `${name}.${p}` + const prop$ = props.indexOf(p) + DEBUG && console.log('Property', prop, opt[p]) + + if (prop$ >= 0) { + let value: number | undefined + outer: { + if (opt[p] instanceof ValueBase) { + const v: Value = opt[p] + + // Audio + if (v.kind === ValueBase.Kind.Audio) { + if (audioProps.has(p)) { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, v.value$) + } + else { + const scalar = Value.Scalar.create() + runVm.AudioToScalar(v.ptr, scalar.ptr) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, scalar.value$) + } + break outer + } + else { + if (audioProps.has(p)) { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, v.value$) + } + else { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, v.value$) + } + break outer + } + } + // Text + else if (typeof opt[p] === 'string') { + if (name === 'say' && p === 'text') { + const floats = Value.Floats.create() + const text = opt[p] + // loadSayText(floats, text) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Floats, floats.value$) + break outer + } + if (name === 'freesound' && p === 'id') { + const floats = Value.Floats.create() + const text = opt[p] + // loadFreesound(floats, text) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Floats, floats.value$) + break outer + } + value = 0 + } + // Literal + else if (typeof opt[p] === 'number') { + value = opt[p] + } + else { + throw new TypeError( + `Invalid type for property "${prop}": ${typeof opt[p]}`) + } + + if (typeof value !== 'number') { + throw new TypeError( + `Invalid value for property "${prop}": ${value}`) + } + + const literal = Value.Literal.create(value) + if (audioProps.has(p)) { + const audio = Value.Audio.create() + runVm.LiteralToAudio(literal.ptr, audio.ptr) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, audio.value$) + } + else { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, literal.value$) + } + } + } + } + + return gen$ + } + + function processMono(opt: Record): Value | void { + const gen$ = handle(opt) + + if ('hasAudioOut' in Gen && Gen.hasAudioOut) { + const audio = Value.Audio.create() + runVm.ProcessAudio(gen$, audio.ptr) + return audio + } + else { + runVm.UpdateGen(gen$) + } + } + + function processStereo(opt: Record): [Value, Value] | void { + const gen$ = handle(opt) + + if ('hasStereoOut' in Gen && Gen.hasStereoOut) { + const out_0 = Value.Audio.create() + const out_1 = Value.Audio.create() + runVm.ProcessAudioStereo(gen$, out_0.ptr, out_1.ptr) + return [out_0, out_1] + } + else { + runVm.UpdateGen(gen$) + } + } + + return stereo + ? processStereo + : processMono + } + + const globals = {} as { + sr: Value.Scalar + t: Value.Scalar + rt: Value.Scalar + co: Value.Scalar + } + + const math = { + add: binaryOp(DspBinaryOp.Add, (a, b) => a + b), + mul: binaryOp(DspBinaryOp.Mul, (a, b) => a * b), + sub: binaryOp(DspBinaryOp.Sub, (a, b) => a - b), + div: binaryOp(DspBinaryOp.Div, (a, b) => a / b), + pow: binaryOp(DspBinaryOp.Pow, (a, b) => a ** b), + } + + const api = { + globals, + math: { + ...math, + '+': math.add, + '*': math.mul, + '-': math.sub, + '/': math.div, + '^': math.pow, + }, + pan(value: Value | number): void { + if (typeof value === 'number') { + value = Value.Literal.create(value) + } + runVm.Pan(value.value$) + }, + pick(values: T[], index: Value | number): Value { + const list$ = context.lists + + const length = values.length + context.lists += length + + let i = list$ + for (let v of values) { + if (typeof v === 'number') { + const literal = Value.Literal.create(v) + context.listsi[i++] = literal.value$ + } + else { + context.listsi[i++] = v.value$ + } + } + + if (typeof index === 'number') { + index = Value.Literal.create(index) + } + + const out = Value.Dynamic.create() + runVm.Pick(list$, length, index.value$, out.value$) + return out + }, + gen: fromEntries( + dspGensKeys.map(name => + [name, defineGen(name, false)] + ) + ), + gen_st: fromEntries( + dspGensKeys.map(name => + [name, defineGen(name, true)] + ) + ) + } + + function begin(scope: Record) { + runVm.Begin() + setupVm.Begin() + + context.gens = + context.audios = + context.literals = + context.floats = + context.scalars = + context.lists = + context.values = + 0 + + globals.sr = Value.Scalar.create() + globals.t = Value.Scalar.create() + globals.rt = Value.Scalar.create() + globals.co = Value.Scalar.create() + + const { sr, t, rt, co } = globals + scope.sr = new AstNode(AstNode.Type.Result, { value: sr }) + scope.t = new AstNode(AstNode.Type.Result, { value: t }) + scope.rt = new AstNode(AstNode.Type.Result, { value: rt }) + scope.co = new AstNode(AstNode.Type.Result, { value: co }) + } + + function buildTrack(tokensCopy: Token[]) { + const scope: Record = {} + const literals: AstNode[] = [] + + // replace number tokens with literal references ids + // and populate initial scope for those ids. + for (const t of tokensCopy) { + if (t.type === Token.Type.Number) { + const id = `lit_${literals.length}` + const literal = new AstNode(AstNode.Type.Literal, parseNumber(t.text), [t]) + literals.push(literal) + scope[id] = literal + t.type = Token.Type.Id + t.text = id + } + } + + begin(scope) + + const program = interpret(api, scope, tokensCopy) + + runVm.End() + + setupVm.CreateAudios(context.audios) + setupVm.CreateValues(context.values) + setupVm.End() + + return program + } + + return buildTrack + } + + function build(code: string) { + const { tracks, currTrack, nextTrack } = $.of(info) + let track = tracks[currTrack] + + const tokens = Array.from(tokenize({ code })) + + // fix invisible tokens bounds to the + // last visible token for errors + const last = tokens.at(-1) + function fixToken(x: Token) { + if (!last) return x + x.line = last.line + x.col = last.col + x.right = last.right + x.bottom = last.bottom + x.index = last.index + x.length = last.length + return x + } + + const tokensCopy = [ + ...preTokens.map(fixToken), + ...tokens, + ...postTokens.map(fixToken), + ].filter(t => t.type !== Token.Type.Comment).map(shallowCopy) + + // create hash id from tokens. We compare this afterwards to determine + // if we should make a new sound or update the old one. + const hashId = + [tokens.filter(t => t.type === Token.Type.Number).length].join('') + + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') + + const prevHashId = hashes.get(track) + const isNew = hashId !== prevHashId + + if (isNew) { + track = tracks[nextTrack] + hashes.set(track, hashId) + } + + const buildTrack = builds.get(track)! + + const program = buildTrack(tokensCopy) + + let L = program.scope.vars['L'] + let R = program.scope.vars['R'] + let LR = program.scope.vars['LR'] + + const slice = program.scope.stack.slice(-2) + if (slice.length === 2) { + R ??= slice.pop()! + L ??= slice.pop()! + } + else if (slice.length === 1) { + LR ??= slice.pop()! + } + + const out = { + L: L?.value as Value.Audio | undefined, + R: R?.value as Value.Audio | undefined, + LR: LR?.value as Value.Audio | undefined, + } + + track.audio_LR$ = out.LR?.audio$ ?? out.LR?.ptr ?? 0 + + info.nextTrack = (nextTrack + 1) % tracks.length + + return { track$: track.ptr } + } + + $.fx(() => { + const { dsp } = $.of(info) + $() + const view = info.view = getMemoryView(dsp.memory) + + const tracks = info.tracks = dsp.tracks$$.map(ptr => + Track(dsp.memory.buffer, ptr) + ) + + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i] + track.run_ops$ = dsp.run_ops$$[i] + track.setup_ops$ = dsp.setup_ops$$[i] + track.literals$ = dsp.literals$$[i] + track.lists$ = dsp.lists$$[i] + + const run_ops = view.getI32(track.run_ops$, MAX_OPS) + const setup_ops = view.getI32(track.setup_ops$, MAX_OPS) + vms.set(track, { + runVm: createVm(run_ops), + setupVm: createVm(setup_ops), + }) + + contexts.set(track, getTrackContext(track)) + builds.set(track, TrackBuild(track)) + } + }) + + $.fx(() => { + const { dsp, view, code } = $.of(info) + $() + const { track$ } = build(code) + view.heapI32[dsp.player_track$ >> 2] = track$ + }) + + function updateInfo() { + info.isPlaying = info.node?.isPlaying ?? false + info.isPaused = info.node?.isPaused ?? false + } + + function play() { + info.node?.play() + updateInfo() + } + + function pause() { + info.node?.pause() + updateInfo() + } + + function stop() { + if (info.node?.isPlaying) { + info.node?.stop() + } + updateInfo() + } + + function dispose() { + stop() + info.node?.disconnect() + info.node = null + } + + return { info, play, pause, stop, dispose } +} diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts new file mode 100644 index 0000000..71609ac --- /dev/null +++ b/src/as/dsp/dsp-worklet.ts @@ -0,0 +1,202 @@ +import { getMemoryView, omit, rpc, toRing, wasmSourceMap } from 'utils' +import { BUFFER_SIZE, MAX_TRACKS } from '~/as/assembly/dsp/constants.ts' +import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' +import hex from '~/as/build/dsp-nort.wasm?raw-hex' +import dspConfig from '~/asconfig-dsp-nort.json' +import { Clock, DspWorkletMode, Out } from '~/src/as/dsp/shared.ts' + +type AudioProcess = (inputs: Float32Array[], outputs: Float32Array[]) => void + +export interface DspProcessorOptions { + mode: Uint8Array +} + +interface SetupOptions { + sourcemapUrl: string +} + +type Setup = Awaited> +async function setup({ sourcemapUrl }: SetupOptions) { + const fromHexString = (hexString: string) => Uint8Array.from( + hexString.match(/.{1,2}/g)!.map(byte => + parseInt(byte, 16) + ) + ) + const uint8 = fromHexString(hex) + const buffer = wasmSourceMap.setSourceMapURL(uint8.buffer, sourcemapUrl) + const binary = new Uint8Array(buffer) + + const memory = new WebAssembly.Memory({ + initial: dspConfig.options.initialMemory, + maximum: dspConfig.options.maximumMemory, + shared: dspConfig.options.sharedMemory, + }) + const mod = await WebAssembly.compile(binary) + const instance = await WebAssembly.instantiate(mod, { + env: { + memory, + abort: console.warn, + log: console.log, + 'console.log': (textPtr: number) => { + console.log(__liftString(textPtr)) + } + } + }) + function __liftString(pointer: number) { + if (!pointer) return null + const + end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1, + memoryU16 = new Uint16Array(memory.buffer) + let + start = pointer >>> 1, + string = "" + while (end - start > 1024) string += String.fromCharCode(...memoryU16.subarray(start, start += 1024)) + return string + String.fromCharCode(...memoryU16.subarray(start, end)) + } + + const wasm: typeof WasmExports = instance.exports as any + + const view = getMemoryView(memory) + + const core$ = wasm.createCore(sampleRate) + const engine$ = wasm.createEngine(sampleRate, core$) + const clock$ = wasm.getEngineClock(engine$) + + const sound$ = wasm.createSound(engine$) + + const out$ = wasm.createOut() + const out = Out(memory.buffer, out$) + const L$ = wasm.allocF32(BUFFER_SIZE) + const R$ = wasm.allocF32(BUFFER_SIZE) + out.L$ = L$ + out.R$ = R$ + const L = view.getF32(out.L$, BUFFER_SIZE) + const R = view.getF32(out.R$, BUFFER_SIZE) + + const player$ = wasm.createPlayer(sound$, out$) + const player_track$ = player$ + wasm.getPlayerTrackOffset() + + const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) + const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) + const setup_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) + const literals$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLiterals()) + const lists$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLists()) + + // TODO: preallocate audios and return here their pointers + return { wasm, memory, clock$, L, R, player$, player_track$, tracks$$, run_ops$$, setup_ops$$, literals$$, lists$$ } +} + +async function createDspKernel(processor: DspProcessor, setup: Setup) { + const { mode } = processor.options.processorOptions + const { wasm, memory, clock$, L, R, player$ } = setup + const clock = Clock(memory.buffer, clock$) + + const chunkSize = 128 + + const ring_L = toRing(L, chunkSize) + const ring_R = toRing(R, chunkSize) + + let begin: number = 0 + let end: number = chunkSize + + const next = () => { + clock.ringPos = clock.nextRingPos + clock.nextRingPos = (clock.ringPos + 1) % ring_L.length + begin = clock.ringPos * chunkSize + end = (clock.ringPos + 1) * chunkSize + ring_L[clock.ringPos].fill(0) + ring_R[clock.ringPos].fill(0) + + wasm.clockUpdate(clock$) + } + + let inputs: Float32Array[] + let outputs: Float32Array[] + + const writeOutput = () => { + outputs[0]?.set(ring_L[clock.ringPos]) + outputs[1]?.set(ring_R[clock.ringPos]) + } + + function setMode(m: DspWorkletMode) { + mode[0] = m + } + + const modes: { [K in DspWorkletMode]: () => void } = { + [DspWorkletMode.Idle]() { + // wasm.playerProcess(player$, 0, 0) + }, + [DspWorkletMode.Reset]() { + next() + wasm.playerProcess(player$, begin, end) + writeOutput() + wasm.clockReset(clock$) + setMode(DspWorkletMode.Play) + }, + [DspWorkletMode.Stop]() { + next() + wasm.playerProcess(player$, begin, end) + writeOutput() + wasm.clockReset(clock$) + setMode(DspWorkletMode.Idle) + }, + [DspWorkletMode.Play]() { + next() + wasm.playerProcess(player$, begin, end) + writeOutput() + }, + [DspWorkletMode.Pause]() { + next() + wasm.playerProcess(player$, begin, end) + writeOutput() + setMode(DspWorkletMode.Idle) + }, + } + + const controller: { player$: number, process: AudioProcess } = { + player$, + process: (_inputs, _outputs) => { + inputs = _inputs + outputs = _outputs + modes[mode[0]]() + } + } + + return controller +} + +export class DspWorklet { + kernel?: Awaited> + + constructor(public processor: DspProcessor) { } + + process: AudioProcess = () => { } + + async setup(options: SetupOptions) { + const dspSetup = await setup(options) + await this.init(dspSetup) + console.log('[dsp-worklet] ready') + return omit(dspSetup, ['wasm']) + } + + async init(dspSetup: Setup) { + this.kernel = await createDspKernel(this.processor, dspSetup) + this.process = this.kernel.process + } +} + +export class DspProcessor extends AudioWorkletProcessor { + worklet: DspWorklet = new DspWorklet(this) + + constructor(public options: { processorOptions: DspProcessorOptions }) { + super() + rpc(this.port, this.worklet) + } + + process(inputs: Float32Array[][], outputs: Float32Array[][]) { + this.worklet.process(inputs[0], outputs[0]) + return true + } +} + +registerProcessor('dsp', DspProcessor) diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index 4283e9f..0352c56 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -1,6 +1,6 @@ import { Sigui } from 'sigui' import { fromEntries, getMemoryView, keys } from 'utils' -import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS } from '~/as/assembly/dsp/constants.ts' +import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { Op } from '~/generated/assembly/dsp-op.ts' import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts' @@ -12,11 +12,10 @@ import { Clock } from './shared.ts' import { getAllProps } from './util.ts' import { Value } from './value.ts' import { wasm } from './wasm.ts' +import type { DspApi } from '~/src/as/dsp/dsp-node.ts' const DEBUG = false -const MAX_OPS = 4096 - const dspGensKeys = keys(dspGens) export type Dsp = ReturnType @@ -499,7 +498,7 @@ export function Dsp({ sampleRate, core$ }: { scope[name] = new AstNode(AstNode.Type.Result, { value }) } - const program = interpret(sound, scope, tokensCopy) + const program = interpret(sound.api, scope, tokensCopy) let L = program.scope.vars['L'] let R = program.scope.vars['R'] diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index b97bf9f..70a2976 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -103,7 +103,7 @@ const worker = { clock.barTime = 0 clock.bpm ||= 144 - wasm.updateClock(clock.ptr) + wasm.clockUpdate(clock.ptr) const { LR, floats, waves } = await this.build(sound$, code) diff --git a/src/as/dsp/shared.ts b/src/as/dsp/shared.ts index 772bfb4..ba197e6 100644 --- a/src/as/dsp/shared.ts +++ b/src/as/dsp/shared.ts @@ -28,17 +28,14 @@ export const Clock = Struct({ nextRingPos: 'u32', }) -export type PlayerTrack = typeof PlayerTrack.type +export type Track = typeof Track.type -export const PlayerTrack = Struct({ - sound$: 'usize', - ops$: 'usize', - notes$: 'usize', - notesCount: 'u32', - params$: 'usize', - paramsCount: 'u32', +export const Track = Struct({ + run_ops$: 'usize', + setup_ops$: 'usize', + literals$: 'usize', + lists$: 'usize', audio_LR$: 'i32', - out$: 'usize', }) export const Out = Struct({ diff --git a/src/as/dsp/value.ts b/src/as/dsp/value.ts index 980787b..76aea66 100644 --- a/src/as/dsp/value.ts +++ b/src/as/dsp/value.ts @@ -46,7 +46,7 @@ export class Value { sound.vm.SetValueDynamic(this.value$, this.scalar$, this.audio$) } else { - sound.vm.SetValue(this.value$, kind as number, this.ptr) + sound.vm.SetValue(this.value$, kind, this.ptr) } } getAudio() { diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts index e3f3bd8..804004d 100644 --- a/src/lang/interpreter.ts +++ b/src/lang/interpreter.ts @@ -1,5 +1,6 @@ -import { getAllPropsReverse, Sound } from 'dsp' +import { getAllPropsReverse } from 'dsp' import { dspGens } from '~/generated/typescript/dsp-gens.ts' +import type { DspApi } from '~/src/as/dsp/dsp-node.ts' import type { Value } from '~/src/as/dsp/value.ts' import { Token } from '~/src/lang/tokenize.ts' import { parseNumber, type NumberFormat, type NumberInfo } from '~/src/lang/util.ts' @@ -158,8 +159,7 @@ const ScopeSpecial = { const BinOps = new Set('+ * - / ^'.split(' ')) const AssignOps = new Set('= += *= -= /= ^='.split(' ')) -export function interpret(sound: Sound, data: Record, tokens: Token[]) { - const g = sound.api +export function interpret(g: DspApi, data: Record, tokens: Token[]) { const scope: Scope = new Scope(null, { ...ScopeNatives, ...ScopeSpecial, ...data }) const results: ProgramValueResult[] = [] const tokensAstNode: Map = new Map() diff --git a/src/pages/App.tsx b/src/pages/App.tsx index b5c9960..bfc335b 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -11,6 +11,7 @@ import { AssemblyScript } from '~/src/pages/AssemblyScript.tsx' import { CanvasDemo } from '~/src/pages/CanvasDemo' import { Chat } from '~/src/pages/Chat/Chat.tsx' import { DspAsyncDemo } from '~/src/pages/DspAsyncDemo.tsx' +import { DspNodeDemo } from '~/src/pages/DspNodeDemo.tsx' import { DspSyncDemo } from '~/src/pages/DspSyncDemo' import { DspWorkerDemo } from '~/src/pages/DspWorkerDemo/DspWorkerDemo' import { EditorDemo } from '~/src/pages/EditorDemo.tsx' @@ -47,6 +48,7 @@ export function App() { '/dsp-sync': () => , '/dsp-async': () => , '/dsp-worker': () => , + '/dsp-node': () => , '/worker-worklet': () => , '/asc': () => , '/qrcode': () => , diff --git a/src/pages/DspNodeDemo.tsx_ b/src/pages/DspNodeDemo.tsx similarity index 73% rename from src/pages/DspNodeDemo.tsx_ rename to src/pages/DspNodeDemo.tsx index 427acd3..14b2dc3 100644 --- a/src/pages/DspNodeDemo.tsx_ +++ b/src/pages/DspNodeDemo.tsx @@ -1,9 +1,8 @@ import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' import { Sigui } from 'sigui' import { assign, Lru } from 'utils' -import { DspNode } from '~/src/as/dsp/node.ts' -import { DspService } from '~/src/as/dsp/service.ts' -import { PlayerTrack } from '~/src/as/dsp/shared.ts' +import { createDspNode } from '~/src/as/dsp/dsp-node.ts' +import { PreviewService } from '~/src/as/dsp/preview-service.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' import { Button } from '~/src/ui/Button.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' @@ -18,26 +17,40 @@ export function DspNodeDemo() { const info = $({ width: 400, height: 300, - code: `[sin 42.11 303 -[exp 2.00] 6.66^ * +] -[exp 1.00] 9.99^ * + code: `t 4* x= +[sin 67.51 413 +[exp 1.00 x trig=] 65.78^ * + x trig=] +[exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .44] + +[saw 42 x trig=] [clip .4] .55* [slp 523 1000 [exp 15 x trig=] 5.5^ * + .99] [exp 8 x trig=] .2^ * [lp 2293] +[sno 8739] + +[noi 6.66] [exp 6 x trig=] 2 +[sin .2] 1.9 + .4^ * ^ +[sin 2 x trig=] * * [sbp 7542 .9] `, - validCode: '', + codeWorking: null as null | string, floats: new Float32Array(), - previewSound$: null as null | number, - track: null as null | PlayerTrack, + sound$: null as null | number, error: null as null | Error, }) const ctx = new AudioContext({ sampleRate: 48000 }) $.fx(() => () => ctx.close()) - const dspService = DspService(ctx) - $.fx(() => dspService.dispose) + const preview = PreviewService(ctx) + $.fx(() => preview.dispose) - const dspNode = DspNode(ctx, dspService) + const dspNode = createDspNode(ctx) $.fx(() => dspNode.dispose) + $.fx(() => { + const { codeWorking } = info + $() + dspNode.info.code = codeWorking + dspNode.play() + }) + const length = 8192 const canvas = as HTMLCanvasElement @@ -56,27 +69,25 @@ export function DspNodeDemo() { const waveWidgets: WaveGlWidget[] = [] $.fx(() => { - const { dsp } = $.of(dspService.info) - if (dsp instanceof Error) return + const { isReady } = $.of(preview.info) $().then(async () => { - info.sound$ = await dspService.service.createSound() + info.sound$ = await preview.service.createSound() }) }) queueMicrotask(() => { $.fx(() => { - const { previewSound$ } = $.of(info) + const { sound$ } = $.of(info) const { pane } = dspEditor.editor.info const { codeVisual } = pane.buffer.info - $().then(async () => { - let result: Awaited> + queueMicrotask(async () => { + const { codeVisual } = pane.buffer.info + + let result: Awaited> let nodeCount = 0 try { - result = await dspService.service.renderSource( - previewSound$, - codeVisual, - ) + result = await preview.service.renderSource(sound$, codeVisual) pane.draw.widgets.deco.clear() plot.info.floats.fill(0) @@ -89,7 +100,7 @@ export function DspNodeDemo() { } info.error = null - info.validCode = codeVisual + info.codeWorking = codeVisual plot.info.floats.set(result.floats) for (const waveData of result.waves) { @@ -133,7 +144,7 @@ export function DspNodeDemo() { $.fx(() => { const { error } = $.of(info) if (!error) return - console.error(error) + console.warn(error) dspEditor.info.error = error return () => dspEditor.info.error = null }) diff --git a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx index 7445042..f0413d9 100644 --- a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx +++ b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx @@ -113,7 +113,6 @@ export function DspWorkerDemo() { }) }) - queueMicrotask(() => { $.fx(() => { const { sound$ } = $.of(info) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f5b9e24..7fb09a2 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -31,6 +31,7 @@ export function Home() { Dsp Sync Dsp Async Dsp Worker + Dsp Node Worker-Worklet AssemblyScript QrCode From d9a3c2502b9db6e69361bd4fedce799ab920daf8 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 06:35:52 +0300 Subject: [PATCH 11/33] wip --- as/assembly/dsp/vm/sound.ts | 2 ++ generated/assembly/dsp-factory.ts | 3 ++- generated/assembly/dsp-runner.ts | 28 ++++++++++++++-------------- generated/typescript/dsp-gens.ts | 2 +- scripts/update-dsp-factory.ts | 3 +++ src/as/dsp/dsp-node.ts | 14 ++++++++------ src/as/dsp/dsp-worklet.ts | 13 +++++++++---- src/pages/DspNodeDemo.tsx | 2 +- 8 files changed, 40 insertions(+), 27 deletions(-) diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index ec04b24..4e95699 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -34,6 +34,7 @@ export class Sound { pan: f32 = 0 gens: Gen[] = [] + prevGens: Gen[] = [] offsets: usize[][] = [] audios: Array | null> = [] @@ -55,6 +56,7 @@ export class Sound { @inline clear(): void { + this.prevGens = this.gens this.gens = [] this.offsets = [] this.values = [] diff --git a/generated/assembly/dsp-factory.ts b/generated/assembly/dsp-factory.ts index 83f9107..3bcc227 100644 --- a/generated/assembly/dsp-factory.ts +++ b/generated/assembly/dsp-factory.ts @@ -105,4 +105,5 @@ import { Tri } from '../../as/assembly/dsp/gen/tri' function createTri(engine: Engine): Tri { return new Tri(engine) } import { Zero } from '../../as/assembly/dsp/gen/zero' function createZero(engine: Engine): Zero { return new Zero(engine) } -export const Factory: ((engine: Engine) => Gen)[] = [createAdsr,createAosc,createAtan,createBap,createBbp,createBhp,createBhs,createBiquad,createBlp,createBls,createBno,createBpk,createClamp,createClip,createComp,createDaverb,createDcc,createDclip,createDclipexp,createDcliplin,createDelay,createDiode,createExp,createFreesound,createZero,createGendy,createGrain,createInc,createLp,createMhp,createMlp,createMoog,createNoi,createNrate,createZero,createRamp,createRate,createSap,createSaw,createSay,createSbp,createShp,createSin,createSlp,createSmp,createSno,createSpk,createSqr,createSvf,createTanh,createTanha,createTap,createTri,createZero] \ No newline at end of file +export const Factory: ((engine: Engine) => Gen)[] = [createAdsr,createAosc,createAtan,createBap,createBbp,createBhp,createBhs,createBiquad,createBlp,createBls,createBno,createBpk,createClamp,createClip,createComp,createDaverb,createDcc,createDclip,createDclipexp,createDcliplin,createDelay,createDiode,createExp,createFreesound,createZero,createGendy,createGrain,createInc,createLp,createMhp,createMlp,createMoog,createNoi,createNrate,createZero,createRamp,createRate,createSap,createSaw,createSay,createSbp,createShp,createSin,createSlp,createSmp,createSno,createSpk,createSqr,createSvf,createTanh,createTanha,createTap,createTri,createZero] +export const Ctors: string[] = ['Adsr','Aosc','Atan','Bap','Bbp','Bhp','Bhs','Biquad','Blp','Bls','Bno','Bpk','Clamp','Clip','Comp','Daverb','Dcc','Dclip','Dclipexp','Dcliplin','Delay','Diode','Exp','Freesound','Gen','Gendy','Grain','Inc','Lp','Mhp','Mlp','Moog','Noi','Nrate','Osc','Ramp','Rate','Sap','Saw','Say','Sbp','Shp','Sin','Slp','Smp','Sno','Spk','Sqr','Svf','Tanh','Tanha','Tap','Tri','Zero'] \ No newline at end of file diff --git a/generated/assembly/dsp-runner.ts b/generated/assembly/dsp-runner.ts index 0bce961..a9d3203 100644 --- a/generated/assembly/dsp-runner.ts +++ b/generated/assembly/dsp-runner.ts @@ -22,21 +22,21 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.CreateAudios: dsp.CreateAudios( snd, changetype(unchecked(ops[i++])) ) continue - + case Op.CreateValues: dsp.CreateValues( snd, changetype(unchecked(ops[i++])) ) continue - + case Op.AudioToScalar: dsp.AudioToScalar( snd, @@ -44,7 +44,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.LiteralToAudio: dsp.LiteralToAudio( snd, @@ -52,7 +52,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.Pick: dsp.Pick( snd, @@ -62,14 +62,14 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.Pan: dsp.Pan( snd, changetype(unchecked(ops[i++])) ) continue - + case Op.SetValue: dsp.SetValue( snd, @@ -78,7 +78,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.SetValueDynamic: dsp.SetValueDynamic( snd, @@ -87,7 +87,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.SetProperty: dsp.SetProperty( snd, @@ -97,14 +97,14 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.UpdateGen: dsp.UpdateGen( snd, changetype(unchecked(ops[i++])) ) continue - + case Op.ProcessAudio: dsp.ProcessAudio( snd, @@ -112,7 +112,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.ProcessAudioStereo: dsp.ProcessAudioStereo( snd, @@ -121,7 +121,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + case Op.BinaryOp: dsp.BinaryOp( snd, @@ -131,7 +131,7 @@ export function run(sound$: usize, ops$: usize): void { changetype(unchecked(ops[i++])) ) continue - + } // end switch } // end while } diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 24f5d6b..b9d4553 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sat Oct 19 2024 20:05:07 GMT+0300 (Eastern European Summer Time) +// auto-generated Sun Oct 20 2024 06:34:15 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/scripts/update-dsp-factory.ts b/scripts/update-dsp-factory.ts index a985f84..f552bb8 100644 --- a/scripts/update-dsp-factory.ts +++ b/scripts/update-dsp-factory.ts @@ -10,12 +10,14 @@ const extendsRegExp = /extends\s([^\s]+)/ let out: string[] = [] out.push(`import { Engine } from '../../as/assembly/dsp/core/engine'`) const factories: string[] = [] +const ctors: string[] = [] for (const file of files) { const base = basename(file, '.ts') const filename = join(gensRoot, file) const text = fs.readFileSync(filename, 'utf-8') const parentCtor = text.match(extendsRegExp)?.[1] const ctor = capitalize(base) + ctors.push(`'${ctor}'`) const factory = `create${ctor}` out.push(`import { ${ctor} } from '../.${gensRoot}/${base}'`) if (['osc', 'gen'].includes(base)) { @@ -27,6 +29,7 @@ for (const file of files) { } out.push(`export const Factory: ((engine: Engine) => Gen)[] = [${factories}]`) +out.push(`export const Ctors: string[] = [${ctors}]`) const targetPath = './generated/assembly/dsp-factory.ts' const text = out.join('\n') diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/dsp-node.ts index 606cc71..8be6652 100644 --- a/src/as/dsp/dsp-node.ts +++ b/src/as/dsp/dsp-node.ts @@ -499,15 +499,17 @@ export function createDspNode(ctx: AudioContext) { // create hash id from tokens. We compare this afterwards to determine // if we should make a new sound or update the old one. - const hashId = - [tokens.filter(t => t.type === Token.Type.Number).length].join('') + const hashId = '' + + [tokens.filter(t => t.type === Token.Type.Number).length].join('') + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') const prevHashId = hashes.get(track) const isNew = hashId !== prevHashId if (isNew) { - track = tracks[nextTrack] + console.log('is new') + track = tracks[info.currTrack = nextTrack] + info.nextTrack = (nextTrack + 1) % tracks.length hashes.set(track, hashId) } @@ -534,13 +536,12 @@ export function createDspNode(ctx: AudioContext) { LR: LR?.value as Value.Audio | undefined, } - track.audio_LR$ = out.LR?.audio$ ?? out.LR?.ptr ?? 0 - - info.nextTrack = (nextTrack + 1) % tracks.length + track.audio_LR$ = out.LR?.audio$ || out.LR?.ptr || 0 return { track$: track.ptr } } + // setup tracks $.fx(() => { const { dsp } = $.of(info) $() @@ -569,6 +570,7 @@ export function createDspNode(ctx: AudioContext) { } }) + // update player track $.fx(() => { const { dsp, view, code } = $.of(info) $() diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts index 71609ac..1ee5508 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/dsp-worklet.ts @@ -35,8 +35,13 @@ async function setup({ sourcemapUrl }: SetupOptions) { const instance = await WebAssembly.instantiate(mod, { env: { memory, - abort: console.warn, - log: console.log, + abort(message$: number, fileName$: number, lineNumber$: number, columnNumber$: number) { + const message = __liftString(message$ >>> 0) + const fileName = __liftString(fileName$ >>> 0) + const lineNumber = lineNumber$ >>> 0 + const columnNumber = columnNumber$ >>> 0 + throw new Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`) + }, 'console.log': (textPtr: number) => { console.log(__liftString(textPtr)) } @@ -153,7 +158,7 @@ async function createDspKernel(processor: DspProcessor, setup: Setup) { }, } - const controller: { player$: number, process: AudioProcess } = { + const kernel: { player$: number, process: AudioProcess } = { player$, process: (_inputs, _outputs) => { inputs = _inputs @@ -162,7 +167,7 @@ async function createDspKernel(processor: DspProcessor, setup: Setup) { } } - return controller + return kernel } export class DspWorklet { diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index 14b2dc3..3fcacdd 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -27,7 +27,7 @@ export function DspNodeDemo() { [noi 6.66] [exp 6 x trig=] 2 [sin .2] 1.9 + .4^ * ^ -[sin 2 x trig=] * * [sbp 7542 .9] +[sin 2 x trig=] * * [sbp 7542 .9] @ [slp 5420] `, codeWorking: null as null | string, floats: new Float32Array(), From 0c578185ddcd6522e2c752492085f7f5c19e933d Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 18:23:21 +0300 Subject: [PATCH 12/33] dsp node stable --- as/assembly/dsp/constants.ts | 2 +- as/assembly/dsp/gen/adsr.ts | 1 + as/assembly/dsp/gen/atan.ts | 2 + as/assembly/dsp/gen/bap.ts | 1 + as/assembly/dsp/gen/bbp.ts | 1 + as/assembly/dsp/gen/bhp.ts | 1 + as/assembly/dsp/gen/bhs.ts | 1 + as/assembly/dsp/gen/blp.ts | 1 + as/assembly/dsp/gen/bls.ts | 1 + as/assembly/dsp/gen/clip.ts | 1 + as/assembly/dsp/gen/dclipexp.ts | 1 + as/assembly/dsp/gen/exp.ts | 1 + as/assembly/dsp/gen/gen.ts | 1 + as/assembly/dsp/gen/lp.ts | 1 + as/assembly/dsp/gen/noi.ts | 1 + as/assembly/dsp/gen/nrate.ts | 1 + as/assembly/dsp/gen/saw.ts | 1 + as/assembly/dsp/gen/sbp.ts | 1 + as/assembly/dsp/gen/shp.ts | 1 + as/assembly/dsp/gen/sin.ts | 1 + as/assembly/dsp/gen/slp.ts | 1 + as/assembly/dsp/gen/sno.ts | 1 + as/assembly/dsp/vm/dsp.ts | 18 +- as/assembly/dsp/vm/sound.ts | 21 +- asconfig-gfx.json | 4 +- generated/typescript/dsp-gens.ts | 2 +- src/as/dsp/dsp-node.ts | 5 +- src/as/dsp/dsp-worklet.ts | 20 +- src/as/dsp/preview-worker.ts | 13 +- src/as/init-wasm.ts | 2 +- src/comp/DspEditor.tsx | 38 ++-- src/pages/DspAsyncDemo.tsx | 5 +- src/pages/DspNodeDemo.tsx | 225 ++++++++++++++-------- src/pages/DspSyncDemo.tsx | 5 +- src/pages/DspWorkerDemo/DspWorkerDemo.tsx | 5 +- src/state.ts | 12 +- src/ui/Button.tsx | 4 +- src/ui/editor/draw.ts | 25 ++- src/ui/editor/input.ts | 4 +- src/ui/editor/view.tsx | 2 +- src/ui/editor/widgets/hover-mark.ts | 19 +- src/ui/editor/widgets/wave-gl.ts | 5 + src/util/copy-ring-into.ts | 27 +++ src/util/stabilizer.ts | 63 ++++++ 44 files changed, 387 insertions(+), 161 deletions(-) create mode 100644 src/util/copy-ring-into.ts create mode 100644 src/util/stabilizer.ts diff --git a/as/assembly/dsp/constants.ts b/as/assembly/dsp/constants.ts index 7d7ace6..8fbc192 100644 --- a/as/assembly/dsp/constants.ts +++ b/as/assembly/dsp/constants.ts @@ -1,4 +1,4 @@ -export const BUFFER_SIZE = 8192 +export const BUFFER_SIZE = 2048 export const MAX_FLOATS = 4096 export const MAX_LISTS = 4096 export const MAX_LITERALS = 4096 diff --git a/as/assembly/dsp/gen/adsr.ts b/as/assembly/dsp/gen/adsr.ts index 8b62f3a..b2512b0 100644 --- a/as/assembly/dsp/gen/adsr.ts +++ b/as/assembly/dsp/gen/adsr.ts @@ -8,6 +8,7 @@ enum AdsrState { } export class Adsr extends Gen { + _name: string = 'Adsr' attack: f32 = 1 // ms decay: f32 = 200 sustain: f32 = 0.1 diff --git a/as/assembly/dsp/gen/atan.ts b/as/assembly/dsp/gen/atan.ts index 76cb27a..86f4cdd 100644 --- a/as/assembly/dsp/gen/atan.ts +++ b/as/assembly/dsp/gen/atan.ts @@ -1,6 +1,8 @@ import { Gen } from './gen' export class Atan extends Gen { + _name: string = 'Atan'; + in: u32 = 0 _audio(begin: u32, end: u32, out: usize): void { diff --git a/as/assembly/dsp/gen/bap.ts b/as/assembly/dsp/gen/bap.ts index 27d4f51..4dac212 100644 --- a/as/assembly/dsp/gen/bap.ts +++ b/as/assembly/dsp/gen/bap.ts @@ -1,6 +1,7 @@ import { Biquad } from './biquad' export class Bap extends Biquad { + _name: string = 'Bap' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/bbp.ts b/as/assembly/dsp/gen/bbp.ts index c238ca5..58a7a08 100644 --- a/as/assembly/dsp/gen/bbp.ts +++ b/as/assembly/dsp/gen/bbp.ts @@ -1,6 +1,7 @@ import { Biquad } from './biquad' export class Bbp extends Biquad { + _name: string = 'Bbp' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/bhp.ts b/as/assembly/dsp/gen/bhp.ts index 13a2b90..4a68707 100644 --- a/as/assembly/dsp/gen/bhp.ts +++ b/as/assembly/dsp/gen/bhp.ts @@ -1,6 +1,7 @@ import { Biquad } from './biquad' export class Bhp extends Biquad { + _name: string = 'Bhp' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/bhs.ts b/as/assembly/dsp/gen/bhs.ts index 344ebe6..583f0ff 100644 --- a/as/assembly/dsp/gen/bhs.ts +++ b/as/assembly/dsp/gen/bhs.ts @@ -1,6 +1,7 @@ import { Biquad } from './biquad' export class Bhs extends Biquad { + _name: string = 'Bhs' cut: f32 = 500 q: f32 = 0.5 amt: f32 = 1 diff --git a/as/assembly/dsp/gen/blp.ts b/as/assembly/dsp/gen/blp.ts index fd83eea..e5a90f1 100644 --- a/as/assembly/dsp/gen/blp.ts +++ b/as/assembly/dsp/gen/blp.ts @@ -1,6 +1,7 @@ import { Biquad } from './biquad' export class Blp extends Biquad { + _name: string = 'Blp' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/bls.ts b/as/assembly/dsp/gen/bls.ts index a5ad099..f42370d 100644 --- a/as/assembly/dsp/gen/bls.ts +++ b/as/assembly/dsp/gen/bls.ts @@ -1,6 +1,7 @@ import { Biquad } from './biquad' export class Bls extends Biquad { + _name: string = 'Bls' cut: f32 = 500 q: f32 = 0.5 amt: f32 = 1 diff --git a/as/assembly/dsp/gen/clip.ts b/as/assembly/dsp/gen/clip.ts index d84cd4c..47a077d 100644 --- a/as/assembly/dsp/gen/clip.ts +++ b/as/assembly/dsp/gen/clip.ts @@ -1,6 +1,7 @@ import { Gen } from './gen' export class Clip extends Gen { + _name: string = 'Clip' threshold: f32 = 1.0; in: u32 = 0 diff --git a/as/assembly/dsp/gen/dclipexp.ts b/as/assembly/dsp/gen/dclipexp.ts index 0cede66..83ac756 100644 --- a/as/assembly/dsp/gen/dclipexp.ts +++ b/as/assembly/dsp/gen/dclipexp.ts @@ -1,6 +1,7 @@ import { Gen } from './gen' export class Dclipexp extends Gen { + _name: string = 'Dclipexp' factor: f32 = 1.0; in: u32 = 0 diff --git a/as/assembly/dsp/gen/exp.ts b/as/assembly/dsp/gen/exp.ts index a5f3aaa..416a311 100644 --- a/as/assembly/dsp/gen/exp.ts +++ b/as/assembly/dsp/gen/exp.ts @@ -1,6 +1,7 @@ import { Osc } from './osc' export class Exp extends Osc { + _name: string = 'Exp' get _table(): StaticArray { return this._engine.wavetable.exp } diff --git a/as/assembly/dsp/gen/gen.ts b/as/assembly/dsp/gen/gen.ts index bd87ff7..e51d9d2 100644 --- a/as/assembly/dsp/gen/gen.ts +++ b/as/assembly/dsp/gen/gen.ts @@ -1,6 +1,7 @@ import { Engine } from '../core/engine' export abstract class Gen { + _name: string = 'Gen' gain: f32 = 1 constructor(public _engine: Engine) { } _update(): void { } diff --git a/as/assembly/dsp/gen/lp.ts b/as/assembly/dsp/gen/lp.ts index ec82d9b..3aa5825 100644 --- a/as/assembly/dsp/gen/lp.ts +++ b/as/assembly/dsp/gen/lp.ts @@ -1,6 +1,7 @@ import { Gen } from './gen' export class Lp extends Gen { + _name: string = 'Lp' cut: f32 = 500; in: u32 = 0 diff --git a/as/assembly/dsp/gen/noi.ts b/as/assembly/dsp/gen/noi.ts index f45ccea..4991520 100644 --- a/as/assembly/dsp/gen/noi.ts +++ b/as/assembly/dsp/gen/noi.ts @@ -1,6 +1,7 @@ import { Osc } from './osc' export class Noi extends Osc { + _name: string = 'Noi' get _table(): StaticArray { return this._engine.wavetable.noise } diff --git a/as/assembly/dsp/gen/nrate.ts b/as/assembly/dsp/gen/nrate.ts index 0a33619..8f0d305 100644 --- a/as/assembly/dsp/gen/nrate.ts +++ b/as/assembly/dsp/gen/nrate.ts @@ -2,6 +2,7 @@ import { rateToPhaseStep } from '../../util' import { Gen } from './gen' export class Nrate extends Gen { + _name: string = 'Nrate' normal: f32 = 1.0 _reset(): void { this.normal = 1.0 diff --git a/as/assembly/dsp/gen/saw.ts b/as/assembly/dsp/gen/saw.ts index 6ec9419..103d3b6 100644 --- a/as/assembly/dsp/gen/saw.ts +++ b/as/assembly/dsp/gen/saw.ts @@ -1,6 +1,7 @@ import { Aosc } from './aosc' export class Saw extends Aosc { + _name: string = 'Saw' get _tables(): StaticArray> { return this._engine.wavetable.antialias.saw } diff --git a/as/assembly/dsp/gen/sbp.ts b/as/assembly/dsp/gen/sbp.ts index 0d8a860..b540a27 100644 --- a/as/assembly/dsp/gen/sbp.ts +++ b/as/assembly/dsp/gen/sbp.ts @@ -1,6 +1,7 @@ import { Svf } from './svf' export class Sbp extends Svf { + _name: string = 'Sbp' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/shp.ts b/as/assembly/dsp/gen/shp.ts index 3a91dea..c568394 100644 --- a/as/assembly/dsp/gen/shp.ts +++ b/as/assembly/dsp/gen/shp.ts @@ -1,6 +1,7 @@ import { Svf } from './svf' export class Shp extends Svf { + _name: string = 'Shp' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/sin.ts b/as/assembly/dsp/gen/sin.ts index 58199d0..2ef9d4e 100644 --- a/as/assembly/dsp/gen/sin.ts +++ b/as/assembly/dsp/gen/sin.ts @@ -1,6 +1,7 @@ import { Osc } from './osc' export class Sin extends Osc { + _name: string = 'Sin' get _table(): StaticArray { return this._engine.wavetable.sine } diff --git a/as/assembly/dsp/gen/slp.ts b/as/assembly/dsp/gen/slp.ts index e6887e0..e571c2a 100644 --- a/as/assembly/dsp/gen/slp.ts +++ b/as/assembly/dsp/gen/slp.ts @@ -1,6 +1,7 @@ import { Svf } from './svf' export class Slp extends Svf { + _name: string = 'Slp' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/gen/sno.ts b/as/assembly/dsp/gen/sno.ts index b15d5ad..8790387 100644 --- a/as/assembly/dsp/gen/sno.ts +++ b/as/assembly/dsp/gen/sno.ts @@ -1,6 +1,7 @@ import { Svf } from './svf' export class Sno extends Svf { + _name: string = 'Sno' cut: f32 = 500 q: f32 = 0.5 diff --git a/as/assembly/dsp/vm/dsp.ts b/as/assembly/dsp/vm/dsp.ts index 7142e8c..b216c81 100644 --- a/as/assembly/dsp/vm/dsp.ts +++ b/as/assembly/dsp/vm/dsp.ts @@ -1,3 +1,4 @@ +import { Gen } from '../gen/gen' import { Factory } from '../../../../generated/assembly/dsp-factory' import { Offsets } from '../../../../generated/assembly/dsp-offsets' import { modWrap } from '../../util' @@ -15,15 +16,24 @@ export class Dsp { @inline CreateGen(snd: Sound, kind_index: i32): void { const Gen = Factory[kind_index] - const gen = Gen(snd.engine) + let gen = Gen(snd.engine) + for (let i = 0; i < snd.prevGens.length; i++) { + const prevGen: Gen = snd.prevGens[i] + const isSameClass: boolean = prevGen._name === gen._name + if (isSameClass) { + gen = prevGen + snd.prevGens.splice(i, 1) + break + } + } snd.gens.push(gen) snd.offsets.push(Offsets[kind_index]) } @inline CreateAudios(snd: Sound, count: i32): void { - for (let x = 0; x <= count; x++) { - snd.audios.push(new StaticArray(BUFFER_SIZE)) - } + // for (let x = 0; x <= count; x++) { + // snd.audios.push(new StaticArray(BUFFER_SIZE)) + // } } @inline CreateValues(snd: Sound, count: i32): void { diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index 4e95699..3b1637d 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -1,5 +1,5 @@ import { run as dspRun } from '../../../../generated/assembly/dsp-runner' -import { BUFFER_SIZE, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_SCALARS } from '../constants' +import { BUFFER_SIZE, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_RMSS, MAX_SCALARS } from '../constants' import { Clock } from '../core/clock' import { Engine } from '../core/engine' import { Gen } from '../gen/gen' @@ -37,7 +37,7 @@ export class Sound { prevGens: Gen[] = [] offsets: usize[][] = [] - audios: Array | null> = [] + audios: Array> = new Array>(MAX_RMSS).map(() => new StaticArray(BUFFER_SIZE)) floats: StaticArray = new StaticArray(MAX_FLOATS) lists: StaticArray = new StaticArray(MAX_LISTS) literals: StaticArray = new StaticArray(MAX_LITERALS) @@ -60,7 +60,7 @@ export class Sound { this.gens = [] this.offsets = [] this.values = [] - this.audios = [] + // this.audios = [] } @inline @@ -96,8 +96,8 @@ export class Sound { for (let i: u32 = 0; i < chunkEnd; i += CHUNK_SIZE) { this.updateScalars(c) - this.begin = i - this.end = i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE + this.begin = x + i + this.end = x + (i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE) dspRun(changetype(this), run_ops$) chunkCount++ @@ -112,7 +112,7 @@ export class Sound { const p = x << 2 memory.copy( out.L$ + p, - changetype(audio), + changetype(audio) + p, chunkEnd << 2 ) // copy left to right for now @@ -146,8 +146,8 @@ export class Sound { for (let i: u32 = 0; i < chunkEnd; i += CHUNK_SIZE) { this.updateScalars(c) - this.begin = i - this.end = i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE + this.begin = x + i + this.end = x + (i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE) dspRun(changetype(this), ops$) chunkCount++ @@ -160,9 +160,10 @@ export class Sound { if (audio_LR$ < this.audios.length) { const audio = this.audios[audio_LR$] + const p = (x << 2) memory.copy( - out$ + (x << 2), - changetype(audio), + out$ + p, + changetype(audio) + p, chunkEnd << 2 ) } diff --git a/asconfig-gfx.json b/asconfig-gfx.json index 71574bf..b1b449b 100644 --- a/asconfig-gfx.json +++ b/asconfig-gfx.json @@ -26,8 +26,8 @@ ], "sharedMemory": true, "importMemory": false, - "initialMemory": 1000, - "maximumMemory": 1000, + "initialMemory": 2000, + "maximumMemory": 2000, "bindings": "raw", "runtime": "incremental", "exportRuntime": true diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index b9d4553..2682deb 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sun Oct 20 2024 06:34:15 GMT+0300 (Eastern European Summer Time) +// auto-generated Sun Oct 20 2024 12:28:11 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/dsp-node.ts index 8be6652..526fd5f 100644 --- a/src/as/dsp/dsp-node.ts +++ b/src/as/dsp/dsp-node.ts @@ -7,7 +7,7 @@ import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' import { DspWorklet, type DspProcessorOptions } from '~/src/as/dsp/dsp-worklet.ts' import dspWorkletUrl from '~/src/as/dsp/dsp-worklet.ts?url' -import { DspWorkletMode, Track } from '~/src/as/dsp/shared.ts' +import { Clock, DspWorkletMode, Track } from '~/src/as/dsp/shared.ts' import { AstNode, interpret } from '~/src/lang/interpreter.ts' import { Token, tokenize } from '~/src/lang/tokenize.ts' import { parseNumber } from '~/src/lang/util.ts' @@ -85,6 +85,7 @@ export function createDspNode(ctx: AudioContext) { node: null as null | DspNode, dsp: null as null | Awaited>, view: null as null | MemoryView, + clock: null as null | Clock, tracks: null as null | Track[], code: null as null | string, currTrack: 0, @@ -101,6 +102,7 @@ export function createDspNode(ctx: AudioContext) { $.batch(() => { info.node = node info.dsp = dsp + info.clock = Clock(dsp.memory.buffer, dsp.clock$) }) node.connect(ctx.destination) } @@ -507,7 +509,6 @@ export function createDspNode(ctx: AudioContext) { const isNew = hashId !== prevHashId if (isNew) { - console.log('is new') track = tracks[info.currTrack = nextTrack] info.nextTrack = (nextTrack + 1) % tracks.length hashes.set(track, hashId) diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts index 1ee5508..b3fa0ed 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/dsp-worklet.ts @@ -1,5 +1,5 @@ import { getMemoryView, omit, rpc, toRing, wasmSourceMap } from 'utils' -import { BUFFER_SIZE, MAX_TRACKS } from '~/as/assembly/dsp/constants.ts' +import { BUFFER_SIZE, MAX_RMSS, MAX_TRACKS } from '~/as/assembly/dsp/constants.ts' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import hex from '~/as/build/dsp-nort.wasm?raw-hex' import dspConfig from '~/asconfig-dsp-nort.json' @@ -80,7 +80,7 @@ async function setup({ sourcemapUrl }: SetupOptions) { const player$ = wasm.createPlayer(sound$, out$) const player_track$ = player$ + wasm.getPlayerTrackOffset() - + const player_audios$$ = Array.from({ length: MAX_RMSS }, (_, index) => wasm.getSoundAudio(sound$, index)) const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) const setup_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) @@ -88,7 +88,21 @@ async function setup({ sourcemapUrl }: SetupOptions) { const lists$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLists()) // TODO: preallocate audios and return here their pointers - return { wasm, memory, clock$, L, R, player$, player_track$, tracks$$, run_ops$$, setup_ops$$, literals$$, lists$$ } + return { + wasm, + memory, + clock$, + L, + R, + player$, + player_track$, + player_audios$$, + tracks$$, + run_ops$$, + setup_ops$$, + literals$$, + lists$$, + } } async function createDspKernel(processor: DspProcessor, setup: Setup) { diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index 70a2976..04d25b3 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -51,8 +51,8 @@ const worker = { ) let last: AstNode | null = null - const waves = new Map() - const waveWidgets = [] as { floats: Float32Array | null, bounds: Token.Bounds }[] + const waves = new Map() + const waveWidgets = [] as { index: number, floats: Float32Array | null, bounds: Token.Bounds }[] let nodeCount = 0 for (const node of program.value.results) { if ('genId' in node) { @@ -61,8 +61,9 @@ const worker = { last.bounds.right = bounds.col - 1 waves.get(last)!.bounds.right = bounds.col - 1 } - const wave = (waveWidgets[nodeCount] ??= { floats: null, bounds }) - wave.floats = sound.getAudio((node.result.value as Value.Audio).ptr) + const wave = (waveWidgets[nodeCount] ??= { index: -1, floats: null, bounds }) + wave.index = (node.result.value as Value.Audio).getAudio() + wave.floats = sound.getAudio(wave.index) assign(wave.bounds, bounds) waves.set(node.result, wave) last = node.result @@ -83,7 +84,7 @@ const worker = { ops$: sound.ops.ptr, LR, floats, - waves: waveWidgets as { floats: Float32Array, bounds: Token.Bounds }[], + waves: waveWidgets as { index: number, floats: Float32Array, bounds: Token.Bounds }[], } }, async renderSource(sound$: number, code: string) { @@ -116,7 +117,7 @@ const worker = { floats.ptr, ) - return { floats, waves } + return { LR, floats, waves } } catch (e) { if (e instanceof Error) { diff --git a/src/as/init-wasm.ts b/src/as/init-wasm.ts index f5cb6b3..b9855e3 100644 --- a/src/as/init-wasm.ts +++ b/src/as/init-wasm.ts @@ -40,7 +40,7 @@ export function initWasm(wasm: Wasm) { let lru = new Set() const TRIES = 16 - const GC_EVERY = 81920 + const GC_EVERY = 819200 let allocs = 0 const funcs = new Map([ diff --git a/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx index 7e78c59..0e1d894 100644 --- a/src/comp/DspEditor.tsx +++ b/src/comp/DspEditor.tsx @@ -31,8 +31,6 @@ export function DspEditor({ code, width, height }: { let digits: number let isDot = false - const hoverMark = HoverMarkWidget() - function getHoveringNumber(pane: Pane) { if (!pane.mouse.info.linecol.hoverLine) return @@ -65,11 +63,13 @@ export function DspEditor({ code, width, height }: { bottom: linecol.line, } ) + hoverMark.box.visible = true pane.draw.widgets.mark.add(hoverMark.widget) pane.draw.info.triggerUpdateTokenDrawInfo++ pane.view.anim.info.epoch++ } else { + hoverMark.box.visible = false pane.draw.widgets.mark.delete(hoverMark.widget) pane.view.anim.info.epoch++ pane.view.el.style.cursor = pane.view.info.cursor @@ -273,22 +273,24 @@ export function DspEditor({ code, width, height }: { inputHandlers, }) - // const errorSub = ErrorSubWidget() - // $.fx(() => { - // const { error } = $.of(info) - // const { pane } = editor.info - // $() - // pane.draw.widgets.subs.add(errorSub.widget) - // errorSub.info.error = error - // errorSub.widget.bounds = Token.bounds((error as any).cause?.nodes ?? [] as Token[]) - // pane.draw.info.triggerUpdateTokenDrawInfo++ - // pane.view.anim.info.epoch++ - // return () => { - // pane.draw.widgets.subs.delete(errorSub.widget) - // pane.draw.info.triggerUpdateTokenDrawInfo++ - // pane.view.anim.info.epoch++ - // } - // }) + const hoverMark = HoverMarkWidget(editor.info.pane.draw.shapes) + + const errorSub = ErrorSubWidget() + $.fx(() => { + const { error } = $.of(info) + const { pane } = editor.info + $() + pane.draw.widgets.subs.add(errorSub.widget) + errorSub.info.error = error + errorSub.widget.bounds = Token.bounds((error as any).cause?.nodes ?? [] as Token[]) + pane.draw.info.triggerUpdateTokenDrawInfo++ + pane.view.anim.info.epoch++ + return () => { + pane.draw.widgets.subs.delete(errorSub.widget) + pane.draw.info.triggerUpdateTokenDrawInfo++ + pane.view.anim.info.epoch++ + } + }) return { info, el: editor.el, editor } } diff --git a/src/pages/DspAsyncDemo.tsx b/src/pages/DspAsyncDemo.tsx index 0a0bbed..4620e20 100644 --- a/src/pages/DspAsyncDemo.tsx +++ b/src/pages/DspAsyncDemo.tsx @@ -1,6 +1,7 @@ import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' import { Sigui } from 'sigui' import { assign, Lru } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import { PreviewService } from '~/src/as/dsp/preview-service' import { DspEditor } from '~/src/comp/DspEditor.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' @@ -29,7 +30,7 @@ export function DspAsyncDemo() { const preview = PreviewService(ctx) $.fx(() => preview.dispose) - const length = 8192 + const length = BUFFER_SIZE const canvas = as HTMLCanvasElement const gfx = Gfx({ canvas }) @@ -82,7 +83,7 @@ export function DspAsyncDemo() { const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, 8192) + : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) wave.info.floats.set(waveData.floats) assign(wave.widget.bounds, waveData.bounds) pane.draw.widgets.deco.add(wave.widget) diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index 3fcacdd..62452ff 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -1,41 +1,41 @@ import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' import { Sigui } from 'sigui' -import { assign, Lru } from 'utils' +import { assign, Lru, throttle } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import { createDspNode } from '~/src/as/dsp/dsp-node.ts' import { PreviewService } from '~/src/as/dsp/preview-service.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' +import { state } from '~/src/state.ts' import { Button } from '~/src/ui/Button.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' -import { H3 } from '~/src/ui/Heading.tsx' +import { copyRingInto } from '~/src/util/copy-ring-into.ts' -const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) +const getFloatsGfx = Lru(1024, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) export function DspNodeDemo() { using $ = Sigui() const info = $({ - width: 400, - height: 300, + get width() { return state.containerWidth / 2 }, + height: state.$.containerHeight, code: `t 4* x= -[sin 67.51 413 -[exp 1.00 x trig=] 65.78^ * + x trig=] -[exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .44] - -[saw 42 x trig=] [clip .4] .55* [slp 523 1000 [exp 15 x trig=] 5.5^ * + .99] [exp 8 x trig=] .2^ * [lp 2293] -[sno 8739] - -[noi 6.66] [exp 6 x trig=] 2 -[sin .2] 1.9 + .4^ * ^ -[sin 2 x trig=] * * [sbp 7542 .9] @ [slp 5420] +[sin 100.00 409 +[exp 1.00 x trig=] 31.88^ * + x trig=] +[exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] +[saw 92 [sin 1] 9^ 61* + x trig=] [clip .4] .7* [slp 756 5000 [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [lp 9999] [sno 1000] [delay 14 .65] +[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 +[sin .3] 1.0 + .9^ * ^ +[sin 2 x trig=] * * [shp 7090 .7] .21* `, codeWorking: null as null | string, + audios: [] as Float32Array[], floats: new Float32Array(), sound$: null as null | number, error: null as null | Error, }) - const ctx = new AudioContext({ sampleRate: 48000 }) + const ctx = new AudioContext({ sampleRate: 48000, latencyHint: 0.00001 }) $.fx(() => () => ctx.close()) const preview = PreviewService(ctx) @@ -48,23 +48,63 @@ export function DspNodeDemo() { const { codeWorking } = info $() dspNode.info.code = codeWorking - dspNode.play() }) - const length = 8192 + $.fx(() => { + const { dsp, view } = $.of(dspNode.info) + $() + info.audios = dsp?.player_audios$$.map(ptr => view.getF32(ptr, BUFFER_SIZE)) + }) + + $.fx(() => { + const { audios } = info + const { isPlaying, clock } = $.of(dspNode.info) + $() + if (isPlaying) { + const { pane } = dspEditor.editor.info + let animFrame: any + const tick = () => { + for (const wave of [...waveWidgets, plot]) { + copyRingInto( + wave.info.stabilizerTemp, + audios[wave.info.index + 1], + clock.ringPos, + wave.info.stabilizerTemp.length, + 15 + ) + const startIndex = wave.info.stabilizer.findStartingPoint(wave.info.stabilizerTemp) + wave.info.floats.set(wave.info.stabilizerTemp.subarray(startIndex)) + wave.info.floats.set(wave.info.stabilizerTemp.subarray(0, startIndex), startIndex) + } + pane.view.anim.info.epoch++ + c.meshes.draw() + animFrame = requestAnimationFrame(tick) + } + tick() + return () => { + for (const wave of [...waveWidgets, plot]) { + wave.info.floats.set(wave.info.previewFloats) + } + pane.view.anim.info.epoch++ + cancelAnimationFrame(animFrame) + } + } + }) const canvas = as HTMLCanvasElement const gfx = Gfx({ canvas }) - const view = Rect(0, 0, 500, 500) + const view = Rect(0, 0, info.$.width, info.$.height) const matrix = Matrix() const c = gfx.createContext(view, matrix) const shapes = c.createShapes() c.sketch.scene.add(shapes) const plot = WaveGlWidget(shapes) - plot.widget.rect.w = 400 - plot.widget.rect.h = 300 - plot.info.floats = wasmGfx.alloc(Float32Array, length) + plot.widget.rect.w = view.width + plot.widget.rect.h = view.height + plot.info.stabilizerTemp = getFloatsGfx('s:LR', BUFFER_SIZE) + plot.info.previewFloats = getFloatsGfx('p:LR', BUFFER_SIZE) + plot.info.floats = getFloatsGfx(`LR`, BUFFER_SIZE) const waveWidgets: WaveGlWidget[] = [] @@ -75,63 +115,89 @@ export function DspNodeDemo() { }) }) + async function build() { + const { sound$ } = info + if (!sound$) return + + const { pane } = dspEditor.editor.info + const { codeVisual } = pane.buffer.info + + let result: Awaited> + let nodeCount = 0 + + try { + result = await preview.service.renderSource(sound$, codeVisual) + + pane.draw.widgets.deco.clear() + plot.info.floats.fill(0) + + if (result.error) { + throw new Error(result.error.message, { cause: result.error.cause }) + } + if (!result?.floats) { + throw new Error('Could not render.') + } + + info.error = null + info.codeWorking = codeVisual + plot.info.index = result.LR + plot.info.previewFloats.set(result.floats) + if (!dspNode.info.isPlaying) plot.info.floats.set(result.floats) + + for (const waveData of result.waves) { + const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + + wave.info.floats = wave.info.floats.length + ? wave.info.floats + : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) + + wave.info.previewFloats = wave.info.previewFloats.length + ? wave.info.previewFloats + : getFloatsGfx(`p:${nodeCount}`, BUFFER_SIZE) + + wave.info.stabilizerTemp = wave.info.stabilizerTemp.length + ? wave.info.stabilizerTemp + : getFloatsGfx(`s:${nodeCount}`, BUFFER_SIZE) + + wave.info.index = waveData.index + wave.info.previewFloats.set(waveData.floats) + + if (!dspNode.info.isPlaying) wave.info.floats.set(waveData.floats) + + assign(wave.widget.bounds, waveData.bounds) + pane.draw.widgets.deco.add(wave.widget) + nodeCount++ + } + } + catch (err) { + if (err instanceof Error) { + info.error = err + } + else { + throw err + } + } + + let delta = waveWidgets.length - nodeCount + while (delta-- > 0) waveWidgets.pop()?.dispose() + + pane.draw.info.triggerUpdateTokenDrawInfo++ + + c.meshes.draw() + pane.view.anim.info.epoch++ + pane.draw.widgets.update() + } + + const buildThrottled = throttle(16, build) + queueMicrotask(() => { $.fx(() => { const { sound$ } = $.of(info) const { pane } = dspEditor.editor.info const { codeVisual } = pane.buffer.info - queueMicrotask(async () => { - const { codeVisual } = pane.buffer.info - - let result: Awaited> - let nodeCount = 0 - - try { - result = await preview.service.renderSource(sound$, codeVisual) - - pane.draw.widgets.deco.clear() - plot.info.floats.fill(0) - - if (result.error) { - throw new Error(result.error.message, { cause: result.error.cause }) - } - if (!result?.floats) { - throw new Error('Could not render.') - } - - info.error = null - info.codeWorking = codeVisual - plot.info.floats.set(result.floats) - - for (const waveData of result.waves) { - const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) - wave.info.floats = wave.info.floats.length - ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, 8192) - wave.info.floats.set(waveData.floats) - assign(wave.widget.bounds, waveData.bounds) - pane.draw.widgets.deco.add(wave.widget) - nodeCount++ - } - } - catch (err) { - if (err instanceof Error) { - info.error = err - } - else { - throw err - } - } - - let delta = waveWidgets.length - nodeCount - while (delta-- > 0) waveWidgets.pop()?.dispose() - - pane.draw.info.triggerUpdateTokenDrawInfo++ - - c.meshes.draw() - pane.view.anim.info.epoch++ - pane.draw.widgets.update() - }) + const { isPlaying } = dspNode.info + $() + queueMicrotask(isPlaying ? buildThrottled : build) }) }) @@ -149,13 +215,10 @@ export function DspNodeDemo() { return () => dspEditor.info.error = null }) - return
-

- DspNode demo - -

+ return
+ {dspEditor} {canvas}
diff --git a/src/pages/DspSyncDemo.tsx b/src/pages/DspSyncDemo.tsx index 90f7a32..bd85b4d 100644 --- a/src/pages/DspSyncDemo.tsx +++ b/src/pages/DspSyncDemo.tsx @@ -2,6 +2,7 @@ import { Dsp, wasm as wasmDsp } from 'dsp' import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' import { Sigui } from 'sigui' import { assign, Lru } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import type { Value } from '~/src/as/dsp/value.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' import type { AstNode } from '~/src/lang/interpreter.ts' @@ -33,7 +34,7 @@ export function DspSyncDemo() { const dsp = Dsp({ sampleRate: ctx.sampleRate }) const sound = dsp.Sound() - const length = 8192 + const length = BUFFER_SIZE const canvas = as HTMLCanvasElement const gfx = Gfx({ canvas }) @@ -99,7 +100,7 @@ export function DspSyncDemo() { const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, 8192) + : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) wave.info.floats.set(sound.getAudio((node.result.value as Value.Audio).ptr)) assign(wave.widget.bounds, bounds) pane.draw.widgets.deco.add(wave.widget) diff --git a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx index f0413d9..57ae76c 100644 --- a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx +++ b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx @@ -1,6 +1,7 @@ import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' import { Sigui } from 'sigui' import { assign, Lru, rpc } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import { PreviewService } from '~/src/as/dsp/preview-service.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' import basicProcessorUrl from '~/src/pages/DspWorkerDemo/basic-processor.ts?url' @@ -89,7 +90,7 @@ export function DspWorkerDemo() { const preview = PreviewService(audioContext) $.fx(() => preview.dispose) - const length = 8192 + const length = BUFFER_SIZE const canvas = as HTMLCanvasElement const gfx = Gfx({ canvas }) @@ -145,7 +146,7 @@ export function DspWorkerDemo() { const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, 8192) + : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) wave.info.floats.set(waveData.floats) assign(wave.widget.bounds, waveData.bounds) pane.draw.widgets.deco.add(wave.widget) diff --git a/src/state.ts b/src/state.ts index 9e5fa60..962fdf7 100644 --- a/src/state.ts +++ b/src/state.ts @@ -34,12 +34,12 @@ class State { const { height } = screen const { container } = this if (!container) return height - let h = 0 - const tagNames = ['header', 'article'] - tagNames.forEach(tagName => { - const el = container.getElementsByTagName(tagName)[0] as HTMLElement - h += el.getBoundingClientRect().height - }) + const header = container.getElementsByTagName('header')[0] as HTMLElement + const article = container.getElementsByTagName('article')[0] as HTMLElement + const articleStyle = window.getComputedStyle(article) + const h = header.getBoundingClientRect().height + + parseFloat(articleStyle.paddingTop) + + parseFloat(articleStyle.paddingBottom) return height - h } diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx index e29a0be..2b2c106 100644 --- a/src/ui/Button.tsx +++ b/src/ui/Button.tsx @@ -1,6 +1,8 @@ +import { cn } from '~/lib/cn.ts' + export function Button(props: Record) { return
as HTMLDivElement diff --git a/src/ui/editor/widgets/hover-mark.ts b/src/ui/editor/widgets/hover-mark.ts index 8d697ed..285cf49 100644 --- a/src/ui/editor/widgets/hover-mark.ts +++ b/src/ui/editor/widgets/hover-mark.ts @@ -1,21 +1,22 @@ -import { Widget } from 'editor' +import { hexToInt, Widget } from 'editor' +import type { Shapes } from 'gfx' import { Sigui } from 'sigui' -export function HoverMarkWidget() { +export function HoverMarkWidget(shapes: Shapes) { using $ = Sigui() const info = $({ - color: '#fff3', + color: '#fff', }) const widget = Widget() + const box = shapes.Box(widget.rect) + box.view.color = hexToInt(info.color) + box.view.alpha = .2 - widget.draw = c => { - const { rect } = widget - const { x, y, w, h } = rect - c.fillStyle = info.color - c.fillRect(x, y, w, h) + function dispose() { + box.remove() } - return { info, widget } + return { info, widget, box, dispose } } diff --git a/src/ui/editor/widgets/wave-gl.ts b/src/ui/editor/widgets/wave-gl.ts index 2f879eb..990d8e9 100644 --- a/src/ui/editor/widgets/wave-gl.ts +++ b/src/ui/editor/widgets/wave-gl.ts @@ -2,6 +2,7 @@ import { Widget } from 'editor' import { wasm, type Shapes } from 'gfx' import { Sigui } from 'sigui' import { hexToInt } from '~/src/ui/editor/util/rgb.ts' +import { Stabilizer } from '~/src/util/stabilizer.ts' export type WaveGlWidget = ReturnType @@ -9,7 +10,11 @@ export function WaveGlWidget(shapes: Shapes) { using $ = Sigui() const info = $({ + index: -1, + previewFloats: wasm.alloc(Float32Array, 0), floats: wasm.alloc(Float32Array, 0), + stabilizer: new Stabilizer(), + stabilizerTemp: wasm.alloc(Float32Array, 0), color: '#fff', }) diff --git a/src/util/copy-ring-into.ts b/src/util/copy-ring-into.ts new file mode 100644 index 0000000..62e0181 --- /dev/null +++ b/src/util/copy-ring-into.ts @@ -0,0 +1,27 @@ +import { memoize } from 'utils' + +export const getRingOffsets = memoize(function getRingOffsets(ringPos: number, length: number, max: number) { + const a = (max - ringPos) * 128 + const b = length + const c = 0 + const d = (ringPos + 1) * 128 + + const e = 0 + const f = a + const g = d + const h = length + + return [a, b, c, d, e, f, g, h] +}) + +export function copyRingInto(target: Float32Array, source: Float32Array, ringPos: number, length: number, max: number) { + const [a, b, c, d, e, f, g, h] = getRingOffsets(ringPos, length, max) + target.set( + source.subarray(c, d), + a + ) + target.set( + source.subarray(g, h), + e + ) +} diff --git a/src/util/stabilizer.ts b/src/util/stabilizer.ts new file mode 100644 index 0000000..39052e4 --- /dev/null +++ b/src/util/stabilizer.ts @@ -0,0 +1,63 @@ +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' + +// based on https://github.com/fenomas/webaudio-viz/blob/master/src/index.js +export class Stabilizer { + // overly magical ad-hoc routine to choose where to start drawing data + numOffsets = 40 + offsetsSpan = 200 + offsets: Uint32Array = Uint32Array.from({ length: this.numOffsets }).map((_, i) => + ((this.offsetsSpan / this.numOffsets) * ((i / this.numOffsets) ** 9) * this.numOffsets) | 0 + ) + // numOffsets = 10 + // offsetsSpan = 320 + // offsets: number[] = Array.from({ length: this.numOffsets }).map((_, i) => + // ((this.offsetsSpan / this.numOffsets) * ((i / this.numOffsets) ** 1.3) * this.numOffsets) | 0 + // ) + prev = new Float32Array(this.numOffsets) + points = new Float32Array(this.numOffsets) + + findStartingPoint(data: Float32Array) { + let count = 0 + let p = 0 + + // console.log(this.offsets) + while (count < this.numOffsets && p >= 0) { + p = this.findUpwardsZeroCrossing(data, p, BUFFER_SIZE >> 4) + if (p > 0) + this.points[count++] = p + } + if (count < 2) return 0 + // if (count < 2) return this.points[0] || 0 + + // try to find a starting point similar to the previous one + p = 0 + let bestScore = 999999.9 + for (let i = 0, np = 0; i < count; i++) { + np = this.points[i]! + const score = this.scorePoint(np, data) + if (score > bestScore) continue + bestScore = score * 0.98 - 1 + p = np + } + + for (let i = 0; i < this.numOffsets; i++) + this.prev[i] = data[this.offsets[i]! + p]! + + return p + -(p > 1) + } + + scorePoint(pt: number, data: Float32Array) { + let acc = 0 + for (let i = 0; i < this.numOffsets; i++) + acc += Math.abs(data[pt + this.offsets[i]!]! - this.prev[i]!) + return acc + } + + findUpwardsZeroCrossing(data: Float32Array, start: number, end: number) { + for (let ct = 0, i = start; i < end; i++) { + if (data[i]! < 0) ct++ + if (data[i]! > 0 && ct > 0) return i + } + return -1 + } +} From ab0d7f846b95f2623a4bfa784be3f36f5c87e1ca Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 21:09:22 +0300 Subject: [PATCH 13/33] good --- as/assembly/dsp/core/clock.ts | 2 +- as/assembly/dsp/vm/sound.ts | 3 +- as/assembly/gfx/draw.ts | 5 +- as/assembly/rms.ts | 9 ++ as/assembly/util.ts | 12 +++ asconfig-rms.json | 35 +++++++ generated/typescript/dsp-gens.ts | 2 +- src/as/dsp/preview-worker.ts | 63 ++++++++++--- src/as/dsp/rms.ts | 20 ++++ src/pages/DspAsyncDemo.tsx | 8 +- src/pages/DspNodeDemo.tsx | 91 +++++++++++++------ src/pages/DspSyncDemo.tsx | 10 +- src/pages/DspWorkerDemo/DspWorkerDemo.tsx | 8 +- src/pages/EditorDemo.tsx | 9 +- src/ui/editor/widgets/index.ts | 7 +- src/ui/editor/widgets/rms-deco.ts | 56 ++++++++++++ .../{wave-canvas.ts => wave-canvas-deco.ts} | 0 .../widgets/{wave-gl.ts => wave-gl-deco.ts} | 4 +- .../{wave-svg.tsx => wave-svg-deco.tsx} | 0 tsconfig.json | 3 + vite.config.ts | 38 +++++--- 21 files changed, 302 insertions(+), 83 deletions(-) create mode 100644 as/assembly/rms.ts create mode 100644 asconfig-rms.json create mode 100644 src/as/dsp/rms.ts create mode 100644 src/ui/editor/widgets/rms-deco.ts rename src/ui/editor/widgets/{wave-canvas.ts => wave-canvas-deco.ts} (100%) rename src/ui/editor/widgets/{wave-gl.ts => wave-gl-deco.ts} (88%) rename src/ui/editor/widgets/{wave-svg.tsx => wave-svg-deco.tsx} (100%) diff --git a/as/assembly/dsp/core/clock.ts b/as/assembly/dsp/core/clock.ts index 4577625..06ed8af 100644 --- a/as/assembly/dsp/core/clock.ts +++ b/as/assembly/dsp/core/clock.ts @@ -4,7 +4,7 @@ export class Clock { timeStep: f64 = 0 prevTime: f64 = -1 startTime: f64 = 0 - endTime: f64 = 1 + endTime: f64 = 16 bpm: f64 = 60 coeff: f64 = 0 barTime: f64 = 0 diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index 3b1637d..4ac0d58 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -75,7 +75,7 @@ export class Sound { const track = changetype(track$) const run_ops$ = track.run_ops$ const audio_LR$ = track.audio_LR$ - // console.log(`what? ${run_ops$} ${audio_LR$}`) + const CHUNK_SIZE = 64 let chunkCount = 0 @@ -115,6 +115,7 @@ export class Sound { changetype(audio) + p, chunkEnd << 2 ) + // TODO: stereo // copy left to right for now memory.copy( out.R$ + p, diff --git a/as/assembly/gfx/draw.ts b/as/assembly/gfx/draw.ts index 86a7cfa..584b590 100644 --- a/as/assembly/gfx/draw.ts +++ b/as/assembly/gfx/draw.ts @@ -1,3 +1,4 @@ +import { clamp11 } from '../util' import { Sketch } from './sketch' import { Box, Line, Matrix, Note, Notes, Params, ParamValue, Shape, ShapeOpts, Wave } from './sketch-shared' import { lineIntersectsRect } from './util' @@ -588,7 +589,7 @@ export function draw( let p = i32(wave.floats$) let x_step: f32 = .5 - let s: f32 = f32.load(p + (i32(nx) << 2)) + let s: f32 = clamp11(f32.load(p + (i32(nx) << 2))) let n_step = wave.coeff / (1.0 / x_step) let cx = x @@ -607,7 +608,7 @@ export function draw( nx += n_step if (cx >= right) break - s = f32.load(p + (i32(nx) << 2)) + s = clamp11(f32.load(p + (i32(nx) << 2))) x1 = cx y1 = y + h * (s * 0.5 + 0.5) diff --git a/as/assembly/rms.ts b/as/assembly/rms.ts new file mode 100644 index 0000000..63eb14c --- /dev/null +++ b/as/assembly/rms.ts @@ -0,0 +1,9 @@ +import { BUFFER_SIZE } from './dsp/constants' +import { rms } from './dsp/graph/rms' +import { clamp01 } from './util' + +export const floats = changetype(new StaticArray(BUFFER_SIZE)) + +export function run(): f32 { + return clamp01(rms(changetype(floats), 0, BUFFER_SIZE)) +} diff --git a/as/assembly/util.ts b/as/assembly/util.ts index 1a58004..fba15a3 100644 --- a/as/assembly/util.ts +++ b/as/assembly/util.ts @@ -6,6 +6,18 @@ export function clamp255(x: f32): i32 { return i32(x) } +export function clamp01(x: f32): f32 { + if (x > 1) x = 1 + else if (x < 0) x = 0 + return x +} + +export function clamp11(x: f32): f32 { + if (x > 1) x = 1 + else if (x < -1) x = -1 + return x +} + export function rgbToInt(r: f32, g: f32, b: f32): i32 { return (clamp255(r * 255) << 16) | (clamp255(g * 255) << 8) | clamp255(b * 255) } diff --git a/asconfig-rms.json b/asconfig-rms.json new file mode 100644 index 0000000..ab7db41 --- /dev/null +++ b/asconfig-rms.json @@ -0,0 +1,35 @@ +{ + "targets": { + "debug": { + "outFile": "./as/build/rms.wasm", + "textFile": "./as/build/rms.wat", + "sourceMap": true, + "debug": true, + "noAssert": true + }, + "release": { + "outFile": "./as/build/rms.wasm", + "textFile": "./as/build/rms.wat", + "sourceMap": true, + "debug": false, + "optimizeLevel": 0, + "shrinkLevel": 0, + "converge": false, + "noAssert": true + } + }, + "options": { + "enable": [ + "simd", + "relaxed-simd", + "threads" + ], + "sharedMemory": true, + "importMemory": false, + "initialMemory": 1, + "maximumMemory": 1, + "bindings": "raw", + "runtime": false, + "exportRuntime": false + } +} diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 2682deb..3e76622 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sun Oct 20 2024 12:28:11 GMT+0300 (Eastern European Summer Time) +// auto-generated Sun Oct 20 2024 20:31:38 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index 04d25b3..c911887 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -19,6 +19,17 @@ const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Ar let epoch = 0 +interface WidgetInfoPartial { + index: number + floats: Float32Array | null + bounds: Token.Bounds +} + +type WidgetInfo = (WidgetInfoPartial & { floats: WidgetInfoPartial['floats'] & {} }) + +const waveWidgets: WidgetInfoPartial[] = [] +const rmsWidgets: WidgetInfoPartial[] = [] + const worker = { dsp: null as null | Dsp, error: null as null | Error, @@ -51,29 +62,50 @@ const worker = { ) let last: AstNode | null = null - const waves = new Map() - const waveWidgets = [] as { index: number, floats: Float32Array | null, bounds: Token.Bounds }[] - let nodeCount = 0 + const nodes = new Map() + + let waveCount = 0 + let rmsCount = 0 + for (const node of program.value.results) { - if ('genId' in node) { + if ('genId' in node || 'op' in node) { const bounds = node.result.bounds + if (last && last.bounds.line === bounds.line && last.bounds.right > bounds.col) { last.bounds.right = bounds.col - 1 - waves.get(last)!.bounds.right = bounds.col - 1 + nodes.get(last)!.bounds.right = bounds.col - 1 + } + + let info: WidgetInfoPartial + + if ('genId' in node) { + info = (waveWidgets[waveCount] ??= { index: -1, floats: null, bounds }) + waveCount++ + } + else if ('op' in node) { + info = (rmsWidgets[rmsCount] ??= { index: -1, floats: null, bounds }) + rmsCount++ + } + else { + throw new Error('unreachable') } - const wave = (waveWidgets[nodeCount] ??= { index: -1, floats: null, bounds }) - wave.index = (node.result.value as Value.Audio).getAudio() - wave.floats = sound.getAudio(wave.index) - assign(wave.bounds, bounds) - waves.set(node.result, wave) + + info.index = (node.result.value as Value.Audio).getAudio() + info.floats = sound.getAudio(info.index) + assign(info.bounds, bounds) + + nodes.set(node.result, info) + last = node.result - nodeCount++ } } - let delta = waveWidgets.length - nodeCount + let delta = waveWidgets.length - waveCount while (delta-- > 0) waveWidgets.pop() + delta = rmsWidgets.length - rmsCount + while (delta-- > 0) rmsWidgets.pop() + const LR = out.LR.getAudio() const length = BUFFER_SIZE @@ -84,7 +116,8 @@ const worker = { ops$: sound.ops.ptr, LR, floats, - waves: waveWidgets as { index: number, floats: Float32Array, bounds: Token.Bounds }[], + waves: waveWidgets as WidgetInfo[], + rmss: rmsWidgets as WidgetInfo[], } }, async renderSource(sound$: number, code: string) { @@ -106,7 +139,7 @@ const worker = { wasm.clockUpdate(clock.ptr) - const { LR, floats, waves } = await this.build(sound$, code) + const { LR, floats, waves, rmss } = await this.build(sound$, code) wasm.fillSound( sound.sound$, @@ -117,7 +150,7 @@ const worker = { floats.ptr, ) - return { LR, floats, waves } + return { LR, floats, waves, rmss } } catch (e) { if (e instanceof Error) { diff --git a/src/as/dsp/rms.ts b/src/as/dsp/rms.ts new file mode 100644 index 0000000..d800996 --- /dev/null +++ b/src/as/dsp/rms.ts @@ -0,0 +1,20 @@ +import { instantiate } from '~/as/build/rms.js' +import url from '~/as/build/rms.wasm?url' +import { hexToBinary } from '~/src/as/init-wasm.ts' + +let mod: WebAssembly.Module + +if (import.meta.env && import.meta.env.MODE !== 'production') { + const hex = (await import('~/as/build/rms.wasm?raw-hex')).default + const wasmMapUrl = new URL('/as/build/rms.wasm.map', location.origin).href + const binary = hexToBinary(hex, wasmMapUrl) + mod = await WebAssembly.compile(binary) +} +else { + mod = await WebAssembly.compileStreaming(fetch(new URL(url, location.href))) +} + +export const wasm = await instantiate(mod, { + env: { + } +}) diff --git a/src/pages/DspAsyncDemo.tsx b/src/pages/DspAsyncDemo.tsx index 4620e20..9695f31 100644 --- a/src/pages/DspAsyncDemo.tsx +++ b/src/pages/DspAsyncDemo.tsx @@ -5,7 +5,7 @@ import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import { PreviewService } from '~/src/as/dsp/preview-service' import { DspEditor } from '~/src/comp/DspEditor.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' import { H2 } from '~/src/ui/Heading.tsx' const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) @@ -40,12 +40,12 @@ export function DspAsyncDemo() { const shapes = c.createShapes() c.sketch.scene.add(shapes) - const plot = WaveGlWidget(shapes) + const plot = WaveGlDecoWidget(shapes) plot.widget.rect.w = 400 plot.widget.rect.h = 300 plot.info.floats = wasmGfx.alloc(Float32Array, length) - const waveWidgets: WaveGlWidget[] = [] + const waveWidgets: WaveGlDecoWidget[] = [] $.fx(() => { const { isReady } = $.of(preview.info) @@ -80,7 +80,7 @@ export function DspAsyncDemo() { plot.info.floats.set(result.floats) for (const waveData of result.waves) { - const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + const wave = (waveWidgets[nodeCount] ??= WaveGlDecoWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index 62452ff..9eedc41 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -8,9 +8,22 @@ import { DspEditor } from '~/src/comp/DspEditor.tsx' import { state } from '~/src/state.ts' import { Button } from '~/src/ui/Button.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { WaveGlDecoWidget, RmsDecoWidget } from '~/src/ui/editor/widgets/index.ts' import { copyRingInto } from '~/src/util/copy-ring-into.ts' +/* + +t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] +[saw (92 353 50 218) t 12* ? [sin 1 x trig=] 9^ 61* + x trig=] [clip .4] .7* [slp 156 22k [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [sno 516 2181 [sin .2 co * t .5 - trig=] * + ] [delay 15 .73] .59* +[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* +[noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* + +t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] +[saw (92 202 50 300) t 2* ? [sin 1 x trig=] 9^ 61* + x trig=] [clip .4] .7* [slp 156 22k [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [sno 516 14400 [sin .2 co * t .5 - trig=] * + ] [delay 14 .73] .59* +[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* +[noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* + +*/ const getFloatsGfx = Lru(1024, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) export function DspNodeDemo() { @@ -19,14 +32,10 @@ export function DspNodeDemo() { const info = $({ get width() { return state.containerWidth / 2 }, height: state.$.containerHeight, - code: `t 4* x= -[sin 100.00 409 -[exp 1.00 x trig=] 31.88^ * + x trig=] -[exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] -[saw 92 [sin 1] 9^ 61* + x trig=] [clip .4] .7* [slp 756 5000 [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [lp 9999] [sno 1000] [delay 14 .65] -[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 -[sin .3] 1.0 + .9^ * ^ -[sin 2 x trig=] * * [shp 7090 .7] .21* + code: `t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] +[saw (92 353 50 218) t 12* ? [sin 1 x trig=] 9^ 61* + x trig=] [clip .4] .7* [slp 156 22k [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [sno 516 2181 [sin .2 co * t .5 - trig=] * + ] [delay 15 .73] .59* +[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* +[noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* `, codeWorking: null as null | string, audios: [] as Float32Array[], @@ -76,6 +85,9 @@ export function DspNodeDemo() { wave.info.floats.set(wave.info.stabilizerTemp.subarray(startIndex)) wave.info.floats.set(wave.info.stabilizerTemp.subarray(0, startIndex), startIndex) } + for (const rms of rmsWidgets) { + rms.update(audios[rms.info.index + 1]) + } pane.view.anim.info.epoch++ c.meshes.draw() animFrame = requestAnimationFrame(tick) @@ -99,14 +111,15 @@ export function DspNodeDemo() { const shapes = c.createShapes() c.sketch.scene.add(shapes) - const plot = WaveGlWidget(shapes) + const plot = WaveGlDecoWidget(shapes) plot.widget.rect.w = view.width plot.widget.rect.h = view.height plot.info.stabilizerTemp = getFloatsGfx('s:LR', BUFFER_SIZE) plot.info.previewFloats = getFloatsGfx('p:LR', BUFFER_SIZE) plot.info.floats = getFloatsGfx(`LR`, BUFFER_SIZE) - const waveWidgets: WaveGlWidget[] = [] + const waveWidgets: WaveGlDecoWidget[] = [] + const rmsWidgets: RmsDecoWidget[] = [] $.fx(() => { const { isReady } = $.of(preview.info) @@ -123,11 +136,14 @@ export function DspNodeDemo() { const { codeVisual } = pane.buffer.info let result: Awaited> - let nodeCount = 0 + let waveCount = 0 + let rmsCount = 0 try { result = await preview.service.renderSource(sound$, codeVisual) + const { isPlaying } = dspNode.info + pane.draw.widgets.deco.clear() plot.info.floats.fill(0) @@ -138,36 +154,54 @@ export function DspNodeDemo() { throw new Error('Could not render.') } - info.error = null - info.codeWorking = codeVisual + $.batch(() => { + info.error = null + info.codeWorking = codeVisual + }) + + const end = $.batch() + plot.info.index = result.LR plot.info.previewFloats.set(result.floats) - if (!dspNode.info.isPlaying) plot.info.floats.set(result.floats) + if (!isPlaying) plot.info.floats.set(result.floats) - for (const waveData of result.waves) { - const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + for (const waveInfo of result.waves) { + const wave = (waveWidgets[waveCount] ??= WaveGlDecoWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) + : getFloatsGfx(`${waveCount}`, BUFFER_SIZE) wave.info.previewFloats = wave.info.previewFloats.length ? wave.info.previewFloats - : getFloatsGfx(`p:${nodeCount}`, BUFFER_SIZE) + : getFloatsGfx(`p:${waveCount}`, BUFFER_SIZE) wave.info.stabilizerTemp = wave.info.stabilizerTemp.length ? wave.info.stabilizerTemp - : getFloatsGfx(`s:${nodeCount}`, BUFFER_SIZE) + : getFloatsGfx(`s:${waveCount}`, BUFFER_SIZE) - wave.info.index = waveData.index - wave.info.previewFloats.set(waveData.floats) + wave.info.index = waveInfo.index + wave.info.previewFloats.set(waveInfo.floats) + if (!isPlaying) wave.info.floats.set(waveInfo.floats) - if (!dspNode.info.isPlaying) wave.info.floats.set(waveData.floats) - - assign(wave.widget.bounds, waveData.bounds) + assign(wave.widget.bounds, waveInfo.bounds) pane.draw.widgets.deco.add(wave.widget) - nodeCount++ + waveCount++ + } + + for (const rmsInfo of result.rmss) { + const rms = (rmsWidgets[rmsCount] ??= RmsDecoWidget(pane.draw.shapes)) + + rms.info.index = rmsInfo.index + + if (!isPlaying) rms.update(rmsInfo.floats) + + assign(rms.widget.bounds, rmsInfo.bounds) + pane.draw.widgets.deco.add(rms.widget) + rmsCount++ } + + end() } catch (err) { if (err instanceof Error) { @@ -178,9 +212,12 @@ export function DspNodeDemo() { } } - let delta = waveWidgets.length - nodeCount + let delta = waveWidgets.length - waveCount while (delta-- > 0) waveWidgets.pop()?.dispose() + delta = rmsWidgets.length - rmsCount + while (delta-- > 0) rmsWidgets.pop()?.dispose() + pane.draw.info.triggerUpdateTokenDrawInfo++ c.meshes.draw() diff --git a/src/pages/DspSyncDemo.tsx b/src/pages/DspSyncDemo.tsx index bd85b4d..02288b8 100644 --- a/src/pages/DspSyncDemo.tsx +++ b/src/pages/DspSyncDemo.tsx @@ -8,7 +8,7 @@ import { DspEditor } from '~/src/comp/DspEditor.tsx' import type { AstNode } from '~/src/lang/interpreter.ts' import { tokenize } from '~/src/lang/tokenize.ts' import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' import { H2 } from '~/src/ui/Heading.tsx' const getFloats = Lru(20, (key: string, length: number) => wasmDsp.alloc(Float32Array, length), item => item.fill(0), item => item.free()) @@ -44,12 +44,12 @@ export function DspSyncDemo() { const shapes = c.createShapes() c.sketch.scene.add(shapes) - const plot = WaveGlWidget(shapes) + const plot = WaveGlDecoWidget(shapes) plot.widget.rect.w = 400 plot.widget.rect.h = 300 plot.info.floats = wasmGfx.alloc(Float32Array, length) - const waveWidgets: WaveGlWidget[] = [] + const waveWidgets: WaveGlDecoWidget[] = [] queueMicrotask(() => { $.fx(() => { @@ -88,7 +88,7 @@ export function DspSyncDemo() { ) let last: AstNode | null = null - const waves = new Map() + const waves = new Map() for (const node of program.value.results) { if ('genId' in node) { @@ -97,7 +97,7 @@ export function DspSyncDemo() { last.bounds.right = bounds.col - 1 waves.get(last)!.widget.bounds.right = bounds.col - 1 } - const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + const wave = (waveWidgets[nodeCount] ??= WaveGlDecoWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) diff --git a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx index 57ae76c..9e339c1 100644 --- a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx +++ b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx @@ -10,7 +10,7 @@ import { DspWorker } from '~/src/pages/DspWorkerDemo/dsp-worker' import DspWorkerFactory from '~/src/pages/DspWorkerDemo/dsp-worker.ts?worker' import { FreeQueue } from '~/src/pages/DspWorkerDemo/free-queue' import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' +import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' import { H3 } from '~/src/ui/Heading.tsx' const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) @@ -100,12 +100,12 @@ export function DspWorkerDemo() { const shapes = c.createShapes() c.sketch.scene.add(shapes) - const plot = WaveGlWidget(shapes) + const plot = WaveGlDecoWidget(shapes) plot.widget.rect.w = 400 plot.widget.rect.h = 300 plot.info.floats = wasmGfx.alloc(Float32Array, length) - const waveWidgets: WaveGlWidget[] = [] + const waveWidgets: WaveGlDecoWidget[] = [] $.fx(() => { const { isReady } = $.of(preview.info) @@ -143,7 +143,7 @@ export function DspWorkerDemo() { plot.info.floats.set(result.floats) for (const waveData of result.waves) { - const wave = (waveWidgets[nodeCount] ??= WaveGlWidget(pane.draw.shapes)) + const wave = (waveWidgets[nodeCount] ??= WaveGlDecoWidget(pane.draw.shapes)) wave.info.floats = wave.info.floats.length ? wave.info.floats : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) diff --git a/src/pages/EditorDemo.tsx b/src/pages/EditorDemo.tsx index 6b61b90..9d9352f 100644 --- a/src/pages/EditorDemo.tsx +++ b/src/pages/EditorDemo.tsx @@ -9,8 +9,8 @@ import { theme } from '~/src/theme.ts' import { Editor } from '~/src/ui/Editor.tsx' import { makeWaveform, waveform } from '~/src/ui/editor/util/waveform.ts' import { HoverMarkWidget, WaveCanvasWidget } from '~/src/ui/editor/widgets/index.ts' -import { WaveGlWidget } from '~/src/ui/editor/widgets/wave-gl.ts' -import { WaveSvgWidget } from '~/src/ui/editor/widgets/wave-svg.tsx' +import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' +import { WaveSvgWidget } from '~/src/ui/editor/widgets/wave-svg-deco' import { H2 } from '~/src/ui/Heading.tsx' export function EditorDemo({ width, height }: { @@ -40,7 +40,6 @@ export function EditorDemo({ width, height }: { let value: number let digits: number let isDot = false - const hoverMark = HoverMarkWidget() function getHoveringNumber(pane: Pane) { const word = pane.buffer.wordUnderLinecol(pane.mouse.info.linecol) @@ -244,6 +243,8 @@ export function EditorDemo({ width, height }: { inputHandlers, }) + const hoverMark = HoverMarkWidget(editor.info.pane.draw.shapes) + // const pane2Info = $({ // code: `[hello] // [world] @@ -301,7 +302,7 @@ export function EditorDemo({ width, height }: { Object.assign(d.widget.bounds, Token.bounds(gens[0])) pane.draw.widgets.deco.add(d.widget) - const d2 = WaveGlWidget(pane.draw.shapes) + const d2 = WaveGlDecoWidget(pane.draw.shapes) d2.info.floats = floats Object.assign(d2.widget.bounds, Token.bounds(gens[1])) pane.draw.widgets.deco.add(d2.widget) diff --git a/src/ui/editor/widgets/index.ts b/src/ui/editor/widgets/index.ts index af9295c..3631ed8 100644 --- a/src/ui/editor/widgets/index.ts +++ b/src/ui/editor/widgets/index.ts @@ -1,5 +1,6 @@ export * from './error-sub.ts' export * from './hover-mark.ts' -export * from './wave-canvas.ts' -export * from './wave-gl.ts' -export * from './wave-svg.tsx' +export * from './rms-deco.ts' +export * from './wave-canvas-deco.ts' +export * from './wave-gl-deco.ts' +export * from './wave-svg-deco.tsx' diff --git a/src/ui/editor/widgets/rms-deco.ts b/src/ui/editor/widgets/rms-deco.ts new file mode 100644 index 0000000..d468d7d --- /dev/null +++ b/src/ui/editor/widgets/rms-deco.ts @@ -0,0 +1,56 @@ +import { hexToInt, Widget } from 'editor' +import { Rect, type Shapes } from 'gfx' +import { wasm } from 'rms' +import { Sigui } from 'sigui' +import { getMemoryView } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' +import { ShapeOpts } from '~/as/assembly/gfx/sketch-shared.ts' + +export type RmsDecoWidget = ReturnType + +export function RmsDecoWidget(shapes: Shapes) { + using $ = Sigui() + + const info = $({ + rect: Rect(), + index: -1, + color: '#fff', + peak: 0, + value: 0, + }) + + const rmsFloats = getMemoryView(wasm.memory).getF32(+wasm.floats, BUFFER_SIZE) + const widget = Widget() + const box = shapes.Box(info.rect) + box.view.opts |= ShapeOpts.Collapse | ShapeOpts.NoMargin + box.view.color = hexToInt(info.color) + + $.fx(() => { + const { rect } = info + const { pr, x, y, w, h } = widget.rect + $() + rect.x = x + 3 + rect.y = y + rect.w = 4 + rect.h = h + }) + + $.fx(() => { + const { rect, value } = info + const { y, h } = widget.rect + $() + rect.h = value * h + rect.y = y + h - rect.h + }) + + function update(floats: Float32Array) { + rmsFloats.set(floats) + info.value = wasm.run() + } + + function dispose() { + box.remove() + } + + return { info, widget, box, update, dispose } +} diff --git a/src/ui/editor/widgets/wave-canvas.ts b/src/ui/editor/widgets/wave-canvas-deco.ts similarity index 100% rename from src/ui/editor/widgets/wave-canvas.ts rename to src/ui/editor/widgets/wave-canvas-deco.ts diff --git a/src/ui/editor/widgets/wave-gl.ts b/src/ui/editor/widgets/wave-gl-deco.ts similarity index 88% rename from src/ui/editor/widgets/wave-gl.ts rename to src/ui/editor/widgets/wave-gl-deco.ts index 990d8e9..dd9773c 100644 --- a/src/ui/editor/widgets/wave-gl.ts +++ b/src/ui/editor/widgets/wave-gl-deco.ts @@ -4,9 +4,9 @@ import { Sigui } from 'sigui' import { hexToInt } from '~/src/ui/editor/util/rgb.ts' import { Stabilizer } from '~/src/util/stabilizer.ts' -export type WaveGlWidget = ReturnType +export type WaveGlDecoWidget = ReturnType -export function WaveGlWidget(shapes: Shapes) { +export function WaveGlDecoWidget(shapes: Shapes) { using $ = Sigui() const info = $({ diff --git a/src/ui/editor/widgets/wave-svg.tsx b/src/ui/editor/widgets/wave-svg-deco.tsx similarity index 100% rename from src/ui/editor/widgets/wave-svg.tsx rename to src/ui/editor/widgets/wave-svg-deco.tsx diff --git a/tsconfig.json b/tsconfig.json index 0d5c4d0..a338ada 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,6 +49,9 @@ "dsp": [ "src/as/dsp/index.ts" ], + "rms": [ + "src/as/dsp/rms.ts" + ], "gfx": [ "./src/as/gfx/index.ts" ], diff --git a/vite.config.ts b/vite.config.ts index 242aed7..c350a3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -119,22 +119,32 @@ export default ({ mode }) => { '--transform', './vendor/as-transform-unroll.js', ] }), + // ViteAssemblyScript({ + // configFile: 'asconfig-pkg.json', + // projectRoot: '.', + // srcMatch: 'as/assembly/pkg', + // srcEntryFile: 'as/assembly/pkg/index.ts', + // mapFile: './as/build/pkg.wasm.map', + // extra: [ + // '--transform', './vendor/as-transform-unroll.js', + // ] + // }), + // ViteAssemblyScript({ + // configFile: 'asconfig-pkg-nort.json', + // projectRoot: '.', + // srcMatch: 'as/assembly/pkg', + // srcEntryFile: 'as/assembly/pkg/index.ts', + // mapFile: './as/build/pkg-nort.wasm.map', + // extra: [ + // '--transform', './vendor/as-transform-unroll.js', + // ] + // }), ViteAssemblyScript({ - configFile: 'asconfig-pkg.json', + configFile: 'asconfig-rms.json', projectRoot: '.', - srcMatch: 'as/assembly/pkg', - srcEntryFile: 'as/assembly/pkg/index.ts', - mapFile: './as/build/pkg.wasm.map', - extra: [ - '--transform', './vendor/as-transform-unroll.js', - ] - }), - ViteAssemblyScript({ - configFile: 'asconfig-pkg-nort.json', - projectRoot: '.', - srcMatch: 'as/assembly/pkg', - srcEntryFile: 'as/assembly/pkg/index.ts', - mapFile: './as/build/pkg-nort.wasm.map', + srcMatch: 'as/assembly', + srcEntryFile: 'as/assembly/rms.ts', + mapFile: './as/build/rms.wasm.map', extra: [ '--transform', './vendor/as-transform-unroll.js', ] From 4828e24c0a65d3fa289e1645c36b703360960c6e Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:26:39 +0300 Subject: [PATCH 14/33] fix build --- as/assembly/common/env.ts | 3 +++ as/assembly/dsp/vm/dsp.ts | 3 +-- generated/typescript/dsp-gens.ts | 2 +- index.html | 6 +----- public/as-interop.js | 5 +++++ src/as/dsp/dsp-worklet.ts | 1 + src/as/dsp/rms.ts | 1 + src/as/dsp/wasm.ts | 1 + src/as/gfx/wasm.ts | 1 + src/as/pkg/wasm.ts | 1 + src/client.tsx | 2 ++ vendor/vite-plugin-assemblyscript.ts | 3 +++ vendor/vite-plugin-bundle-url.ts | 1 + vite.config.ts | 12 +++++++----- 14 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 as/assembly/common/env.ts create mode 100644 public/as-interop.js diff --git a/as/assembly/common/env.ts b/as/assembly/common/env.ts new file mode 100644 index 0000000..db4096b --- /dev/null +++ b/as/assembly/common/env.ts @@ -0,0 +1,3 @@ +// @ts-ignore +@external('env', 'log') +export declare function log(x: i32): void diff --git a/as/assembly/dsp/vm/dsp.ts b/as/assembly/dsp/vm/dsp.ts index b216c81..14a5a81 100644 --- a/as/assembly/dsp/vm/dsp.ts +++ b/as/assembly/dsp/vm/dsp.ts @@ -1,8 +1,7 @@ -import { Gen } from '../gen/gen' import { Factory } from '../../../../generated/assembly/dsp-factory' import { Offsets } from '../../../../generated/assembly/dsp-offsets' import { modWrap } from '../../util' -import { BUFFER_SIZE } from '../constants' +import { Gen } from '../gen/gen' import { fill } from '../graph/fill' import { BinOpAudioAudio, BinOpAudioScalar, BinOpScalarAudio, BinOpScalarScalar } from './bin-op' import { DspBinaryOp, SoundValueKind } from './dsp-shared' diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 3e76622..a91146e 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sun Oct 20 2024 20:31:38 GMT+0300 (Eastern European Summer Time) +// auto-generated Sun Oct 20 2024 22:25:25 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/index.html b/index.html index 64b9c9e..bbe1d8f 100644 --- a/index.html +++ b/index.html @@ -21,13 +21,9 @@ Vasi -
+ - diff --git a/public/as-interop.js b/public/as-interop.js new file mode 100644 index 0000000..bf9dc94 --- /dev/null +++ b/public/as-interop.js @@ -0,0 +1,5 @@ +// KEEP: required for AssemblyScript interop. +globalThis.unmanaged = () => { + // noop + setTimeout(() => { }, 1) +} diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts index b3fa0ed..698449f 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/dsp-worklet.ts @@ -42,6 +42,7 @@ async function setup({ sourcemapUrl }: SetupOptions) { const columnNumber = columnNumber$ >>> 0 throw new Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`) }, + log: console.log, 'console.log': (textPtr: number) => { console.log(__liftString(textPtr)) } diff --git a/src/as/dsp/rms.ts b/src/as/dsp/rms.ts index d800996..83098c5 100644 --- a/src/as/dsp/rms.ts +++ b/src/as/dsp/rms.ts @@ -16,5 +16,6 @@ else { export const wasm = await instantiate(mod, { env: { + log: console.log, } }) diff --git a/src/as/dsp/wasm.ts b/src/as/dsp/wasm.ts index 0530c80..764a61d 100644 --- a/src/as/dsp/wasm.ts +++ b/src/as/dsp/wasm.ts @@ -16,6 +16,7 @@ else { const wasmInstance = await instantiate(mod, { env: { + log: console.warn, } }) diff --git a/src/as/gfx/wasm.ts b/src/as/gfx/wasm.ts index 291365d..994a55a 100644 --- a/src/as/gfx/wasm.ts +++ b/src/as/gfx/wasm.ts @@ -23,6 +23,7 @@ function setFlushSketchFn(fn: (count: number) => void) { const wasmInstance = await instantiate(mod, { env: { + log: console.log, flushSketch(count: number) { DEBUG && console.debug('[flush]', count) flushSketchFn(count) diff --git a/src/as/pkg/wasm.ts b/src/as/pkg/wasm.ts index f1f7880..066e5d3 100644 --- a/src/as/pkg/wasm.ts +++ b/src/as/pkg/wasm.ts @@ -16,6 +16,7 @@ else { const wasm = await instantiate(mod, { env: { + log: console.log, } }) diff --git a/src/client.tsx b/src/client.tsx index 5c70bd9..1037066 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -1,3 +1,5 @@ +import '~/lib/watcher.ts' + import { cleanup, hmr, mount } from 'sigui' import { App } from '~/src/pages/App.tsx' import { setState, state } from '~/src/state.ts' diff --git a/vendor/vite-plugin-assemblyscript.ts b/vendor/vite-plugin-assemblyscript.ts index 0132ff4..d53c444 100644 --- a/vendor/vite-plugin-assemblyscript.ts +++ b/vendor/vite-plugin-assemblyscript.ts @@ -66,6 +66,7 @@ export function ViteAssemblyScript( const matchPath = resolve(join(options.projectRoot, options.srcMatch)) let handledTimestamp: any + let didBuild = false return { name: 'vite-plugin-assemblyscript', @@ -77,6 +78,8 @@ export function ViteAssemblyScript( } }, async buildStart() { + if (didBuild) return + didBuild = true await compile(entryFile, 'release', options) }, } diff --git a/vendor/vite-plugin-bundle-url.ts b/vendor/vite-plugin-bundle-url.ts index 713dfbd..75645ab 100644 --- a/vendor/vite-plugin-bundle-url.ts +++ b/vendor/vite-plugin-bundle-url.ts @@ -23,6 +23,7 @@ export function ViteBundleUrl({ plugins }: { plugins: (Plugin | Plugin[])[] }): configFile: false, clearScreen: false, customLogger: quietLogger, + plugins: [], build: { ...viteConfig?.build, lib: { diff --git a/vite.config.ts b/vite.config.ts index c350a3b..39f0be5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,7 +23,9 @@ type Plugins = (Plugin | Plugin[])[] export default ({ mode }) => { loadEnv(mode, root) - const https = mode === 'development' ? { + const IS_DEV = mode === 'development' + + const https = IS_DEV ? { key: fs.readFileSync(path.join(homedir, '.ssl-certs', 'devito.test-key.pem')), cert: fs.readFileSync(path.join(homedir, '.ssl-certs', 'devito.test.pem')), } : undefined @@ -142,7 +144,7 @@ export default ({ mode }) => { ViteAssemblyScript({ configFile: 'asconfig-rms.json', projectRoot: '.', - srcMatch: 'as/assembly', + srcMatch: 'as/assembly/dsp', srcEntryFile: 'as/assembly/rms.ts', mapFile: './as/build/rms.wasm.map', extra: [ @@ -159,7 +161,7 @@ export default ({ mode }) => { '--transform', './vendor/as-transform-unroll.js', ] }), - watchAndRun([ + IS_DEV && watchAndRun([ { name: 'scripts', watchKind: ['add', 'change', 'unlink'], @@ -168,7 +170,7 @@ export default ({ mode }) => { delay: 100 } ]) as Plugin, - ] + ].filter(Boolean) as Plugins return defineConfig({ clearScreen: false, @@ -208,7 +210,7 @@ export default ({ mode }) => { admin: path.join(root, 'admin/index.html'), }, treeshake: { propertyReadSideEffects: 'always' }, - plugins: buildPlugins + plugins //: buildPlugins }, }, }) From 69c0c874d28e87c38d49597622fc620b1a6d4ce5 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:30:25 +0300 Subject: [PATCH 15/33] fix admin --- admin/client.tsx | 4 +++- admin/index.html | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/admin/client.tsx b/admin/client.tsx index d88d793..4f63820 100644 --- a/admin/client.tsx +++ b/admin/client.tsx @@ -1,9 +1,11 @@ +import '~/lib/watcher.ts' + import { cleanup, hmr, mount } from 'sigui' import { Admin } from '~/admin/Admin.tsx' import { setState, state } from '~/src/state.ts' export const start = mount('#container', target => { - target.replaceChildren() + target.replaceChildren( as HTMLElement) return cleanup }) diff --git a/admin/index.html b/admin/index.html index 478ee11..179b73b 100644 --- a/admin/index.html +++ b/admin/index.html @@ -9,12 +9,8 @@ Vasi - Admin -
+ - From d89d9db3314c4db9b6692a14b52af8185d93dccc Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:32:14 +0300 Subject: [PATCH 16/33] build --- generated/typescript/dsp-gens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index a91146e..9a2ae33 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sun Oct 20 2024 22:25:25 GMT+0300 (Eastern European Summer Time) +// auto-generated Sun Oct 20 2024 22:30:57 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' From bed6608a3f61423bf87515c06de57c817f71201b Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:40:41 +0300 Subject: [PATCH 17/33] use pnpm --- .github/workflows/deploy.yml | 2 +- .github/workflows/preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 393829d..31fed9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: node-version: 22.x - name: Install dependencies - run: npm install --ignore-scripts --force + run: npx pnpm i - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 9913e26..82e8d3b 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -52,7 +52,7 @@ jobs: node-version: 22.x - name: Install dependencies - run: npm install --ignore-scripts --force + run: npx pnpm i - name: Build run: bun run build From 6557b4e7446662f1cbd362028ab51cd3f35002a2 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:46:37 +0300 Subject: [PATCH 18/33] dont run postinstall scripts --- .github/workflows/deploy.yml | 2 +- .github/workflows/preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 31fed9e..0d70145 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: node-version: 22.x - name: Install dependencies - run: npx pnpm i + run: npx pnpm i --enable-pre-post-scripts=false - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 82e8d3b..8679fee 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -52,7 +52,7 @@ jobs: node-version: 22.x - name: Install dependencies - run: npx pnpm i + run: npx pnpm i --enable-pre-post-scripts=false - name: Build run: bun run build From fa2d2af9e0dec8eb076ce6f99e9008a6fded8b0d Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:49:16 +0300 Subject: [PATCH 19/33] dont run any scripts --- .github/workflows/deploy.yml | 2 +- .github/workflows/preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0d70145..cbbcbed 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: node-version: 22.x - name: Install dependencies - run: npx pnpm i --enable-pre-post-scripts=false + run: npx pnpm i --ignore-scripts=true - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 8679fee..37613a4 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -52,7 +52,7 @@ jobs: node-version: 22.x - name: Install dependencies - run: npx pnpm i --enable-pre-post-scripts=false + run: npx pnpm i --ignore-scripts=true - name: Build run: bun run build From 1eca99c9c70b8d010b3744150a8ee7cd394686d5 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:51:48 +0300 Subject: [PATCH 20/33] add missing dev deps --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 1fd16b0..4792d7a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "open-in-editor": "^2.2.0", "postcss": "^8.4.47", "qrcode-terminal": "^0.12.0", + "sharp": "0.32.6", + "sharp-ico": "0.1.5", "tailwindcss": "^3.4.13", "tailwindcss-image-rendering": "^1.0.2", "utils": "github:stagas/utils", From 40f76561413de1fc4b974c0cbad87d0b2efbb8e1 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 22:56:03 +0300 Subject: [PATCH 21/33] another attempt --- .github/workflows/deploy.yml | 3 +++ .github/workflows/preview.yml | 3 +++ package.json | 2 -- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cbbcbed..d84ed89 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,6 +34,9 @@ jobs: - name: Install dependencies run: npx pnpm i --ignore-scripts=true + - name: Install dev deps with scripts + run: npx pnpm add @vite-pwa/assets-generator -D + - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 37613a4..d974b07 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -54,6 +54,9 @@ jobs: - name: Install dependencies run: npx pnpm i --ignore-scripts=true + - name: Install dev deps with scripts + run: npx pnpm add @vite-pwa/assets-generator -D + - name: Build run: bun run build diff --git a/package.json b/package.json index 4792d7a..1fd16b0 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,6 @@ "open-in-editor": "^2.2.0", "postcss": "^8.4.47", "qrcode-terminal": "^0.12.0", - "sharp": "0.32.6", - "sharp-ico": "0.1.5", "tailwindcss": "^3.4.13", "tailwindcss-image-rendering": "^1.0.2", "utils": "github:stagas/utils", From 152d1403f3c8687bd65e79afb96c8f0fc7699f39 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 23:00:15 +0300 Subject: [PATCH 22/33] remove then install again --- .github/workflows/deploy.yml | 2 +- .github/workflows/preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d84ed89..2724ead 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,7 +35,7 @@ jobs: run: npx pnpm i --ignore-scripts=true - name: Install dev deps with scripts - run: npx pnpm add @vite-pwa/assets-generator -D + run: npx pnpm rm @vite-pwa/assets-generator -D && npx pnpm add @vite-pwa/assets-generator -D - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index d974b07..d3f31b0 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -55,7 +55,7 @@ jobs: run: npx pnpm i --ignore-scripts=true - name: Install dev deps with scripts - run: npx pnpm add @vite-pwa/assets-generator -D + run: npx pnpm rm @vite-pwa/assets-generator -D && npx pnpm add @vite-pwa/assets-generator -D - name: Build run: bun run build From dfa64d14b129bec91080ea4be320717beff2322a Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 23:03:30 +0300 Subject: [PATCH 23/33] remove before --- .github/workflows/deploy.yml | 5 ++++- .github/workflows/preview.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2724ead..96038a1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,11 +31,14 @@ jobs: with: node-version: 22.x + - name: Uninstall dev deps with scripts + run: npx pnpm rm @vite-pwa/assets-generator + - name: Install dependencies run: npx pnpm i --ignore-scripts=true - name: Install dev deps with scripts - run: npx pnpm rm @vite-pwa/assets-generator -D && npx pnpm add @vite-pwa/assets-generator -D + run: npx pnpm add @vite-pwa/assets-generator - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index d3f31b0..da8ec68 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -51,11 +51,14 @@ jobs: with: node-version: 22.x + - name: Uninstall dev deps with scripts + run: npx pnpm rm @vite-pwa/assets-generator + - name: Install dependencies run: npx pnpm i --ignore-scripts=true - name: Install dev deps with scripts - run: npx pnpm rm @vite-pwa/assets-generator -D && npx pnpm add @vite-pwa/assets-generator -D + run: npx pnpm add @vite-pwa/assets-generator - name: Build run: bun run build From c6cebf023c9ce930e3ffd85b6db6cdbd448b44bc Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 23:05:07 +0300 Subject: [PATCH 24/33] ignore scripts again --- .github/workflows/deploy.yml | 2 +- .github/workflows/preview.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 96038a1..30f3836 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: node-version: 22.x - name: Uninstall dev deps with scripts - run: npx pnpm rm @vite-pwa/assets-generator + run: npx pnpm rm @vite-pwa/assets-generator --ignore-scripts=true - name: Install dependencies run: npx pnpm i --ignore-scripts=true diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index da8ec68..0f4422d 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -52,7 +52,7 @@ jobs: node-version: 22.x - name: Uninstall dev deps with scripts - run: npx pnpm rm @vite-pwa/assets-generator + run: npx pnpm rm @vite-pwa/assets-generator --ignore-scripts=true - name: Install dependencies run: npx pnpm i --ignore-scripts=true From 338ca983d28d83088a1c19cd2694792601912238 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 23:07:33 +0300 Subject: [PATCH 25/33] one more time --- .github/workflows/deploy.yml | 6 +++--- .github/workflows/preview.yml | 6 +++--- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 30f3836..6e7bb93 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,13 +32,13 @@ jobs: node-version: 22.x - name: Uninstall dev deps with scripts - run: npx pnpm rm @vite-pwa/assets-generator --ignore-scripts=true + run: npm remove @vite-pwa/assets-generator --ignore-scripts=true - name: Install dependencies - run: npx pnpm i --ignore-scripts=true + run: npm i --ignore-scripts=true - name: Install dev deps with scripts - run: npx pnpm add @vite-pwa/assets-generator + run: npm i @vite-pwa/assets-generator -D - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 0f4422d..856c5d2 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -52,13 +52,13 @@ jobs: node-version: 22.x - name: Uninstall dev deps with scripts - run: npx pnpm rm @vite-pwa/assets-generator --ignore-scripts=true + run: npm remove @vite-pwa/assets-generator --ignore-scripts=true - name: Install dependencies - run: npx pnpm i --ignore-scripts=true + run: npm i --ignore-scripts=true - name: Install dev deps with scripts - run: npx pnpm add @vite-pwa/assets-generator + run: npm i @vite-pwa/assets-generator -D - name: Build run: bun run build diff --git a/package.json b/package.json index 1fd16b0..523135d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "models": "kysely-zod-codegen --env-file=.env.development --camel-case --out-file=api/models.ts", "scripts": "bun run scripts/generate-dsp-vm.ts && bun run scripts/update-dsp-factory.ts && bun run scripts/update-gens-offsets.ts", "pwa": "pwa-assets-generator --preset minimal-2023 public/favicon.svg", - "postinstall": "link-local || exit 0" + "link": "link-local || exit 0" }, "devDependencies": { "@happy-dom/global-registrator": "^15.7.4", From b67ee5b439419afd0b12df59624b9bba2ccf0ef1 Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 23:09:14 +0300 Subject: [PATCH 26/33] npm needs force --- .github/workflows/deploy.yml | 6 +++--- .github/workflows/preview.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6e7bb93..62af567 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,13 +32,13 @@ jobs: node-version: 22.x - name: Uninstall dev deps with scripts - run: npm remove @vite-pwa/assets-generator --ignore-scripts=true + run: npm remove @vite-pwa/assets-generator --force --ignore-scripts=true - name: Install dependencies - run: npm i --ignore-scripts=true + run: npm i --force --ignore-scripts=true - name: Install dev deps with scripts - run: npm i @vite-pwa/assets-generator -D + run: npm i @vite-pwa/assets-generator -D --force - name: Build run: bun run build diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 856c5d2..103a20a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -52,13 +52,13 @@ jobs: node-version: 22.x - name: Uninstall dev deps with scripts - run: npm remove @vite-pwa/assets-generator --ignore-scripts=true + run: npm remove @vite-pwa/assets-generator --force --ignore-scripts=true - name: Install dependencies - run: npm i --ignore-scripts=true + run: npm i --force --ignore-scripts=true - name: Install dev deps with scripts - run: npm i @vite-pwa/assets-generator -D + run: npm i @vite-pwa/assets-generator -D --force - name: Build run: bun run build From 82b8c5a7856ff13a8e5ad5f1c41ef1e275a1991f Mon Sep 17 00:00:00 2001 From: stagas Date: Sun, 20 Oct 2024 23:16:11 +0300 Subject: [PATCH 27/33] reenable asc pkg --- generated/typescript/dsp-gens.ts | 2 +- vite.config.ts | 40 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 9a2ae33..a42021a 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sun Oct 20 2024 22:30:57 GMT+0300 (Eastern European Summer Time) +// auto-generated Sun Oct 20 2024 23:15:37 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/vite.config.ts b/vite.config.ts index 39f0be5..d9b73af 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -121,26 +121,26 @@ export default ({ mode }) => { '--transform', './vendor/as-transform-unroll.js', ] }), - // ViteAssemblyScript({ - // configFile: 'asconfig-pkg.json', - // projectRoot: '.', - // srcMatch: 'as/assembly/pkg', - // srcEntryFile: 'as/assembly/pkg/index.ts', - // mapFile: './as/build/pkg.wasm.map', - // extra: [ - // '--transform', './vendor/as-transform-unroll.js', - // ] - // }), - // ViteAssemblyScript({ - // configFile: 'asconfig-pkg-nort.json', - // projectRoot: '.', - // srcMatch: 'as/assembly/pkg', - // srcEntryFile: 'as/assembly/pkg/index.ts', - // mapFile: './as/build/pkg-nort.wasm.map', - // extra: [ - // '--transform', './vendor/as-transform-unroll.js', - // ] - // }), + ViteAssemblyScript({ + configFile: 'asconfig-pkg.json', + projectRoot: '.', + srcMatch: 'as/assembly/pkg', + srcEntryFile: 'as/assembly/pkg/index.ts', + mapFile: './as/build/pkg.wasm.map', + extra: [ + '--transform', './vendor/as-transform-unroll.js', + ] + }), + ViteAssemblyScript({ + configFile: 'asconfig-pkg-nort.json', + projectRoot: '.', + srcMatch: 'as/assembly/pkg', + srcEntryFile: 'as/assembly/pkg/index.ts', + mapFile: './as/build/pkg-nort.wasm.map', + extra: [ + '--transform', './vendor/as-transform-unroll.js', + ] + }), ViteAssemblyScript({ configFile: 'asconfig-rms.json', projectRoot: '.', From 88e7e584acdd4145c8b66399a563ff26f730f8cf Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 22 Oct 2024 15:55:54 +0300 Subject: [PATCH 28/33] wip dsp unify --- api/oauth/routes/github.ts | 11 +- as/assembly/dsp/constants.ts | 2 + as/assembly/dsp/core/clock.ts | 2 +- as/assembly/dsp/index.ts | 4 + as/assembly/dsp/shared.ts | 8 ++ as/assembly/dsp/vm/dsp.ts | 8 +- as/assembly/dsp/vm/sound.ts | 33 +++--- generated/typescript/dsp-gens.ts | 2 +- src/as/dsp/dsp-node.ts | 23 +--- src/as/dsp/dsp-worklet.ts | 8 +- src/as/dsp/dsp.ts | 60 ++++------- src/as/dsp/pre-post.ts | 16 +++ src/as/dsp/preview-service.ts | 10 +- src/as/dsp/preview-worker.ts | 69 ++++++++---- src/as/dsp/shared.ts | 9 ++ src/as/dsp/value.ts | 8 ++ src/lang/interpreter.test.ts | 34 ++++++ src/lang/interpreter.ts | 12 ++- src/pages/DspNodeDemo.tsx | 150 ++++++++++++++++++++------ src/screen.ts | 8 +- src/ui/editor/buffer.test.ts | 14 ++- src/ui/editor/buffer.ts | 29 +++-- src/ui/editor/draw.ts | 27 +++-- src/ui/editor/input.ts | 9 +- src/ui/editor/widget.ts | 6 +- src/ui/editor/widgets/index.ts | 1 + src/ui/editor/widgets/list-mark.ts | 43 ++++++++ src/ui/editor/widgets/rms-deco.ts | 13 ++- src/ui/editor/widgets/wave-gl-deco.ts | 8 +- src/util/mod-wrap.ts | 3 + src/util/stabilizer.ts | 4 +- vite.config.ts | 2 +- 32 files changed, 444 insertions(+), 192 deletions(-) create mode 100644 src/as/dsp/pre-post.ts create mode 100644 src/lang/interpreter.test.ts create mode 100644 src/ui/editor/widgets/list-mark.ts create mode 100644 src/util/mod-wrap.ts diff --git a/api/oauth/routes/github.ts b/api/oauth/routes/github.ts index 9c6d39b..1a809e3 100644 --- a/api/oauth/routes/github.ts +++ b/api/oauth/routes/github.ts @@ -48,7 +48,7 @@ const OAuthUser = z.union([ const headers = { 'content-type': 'application/json', - 'user-agent': 'cfw-oauth-login', + 'user-agent': 'oauth-login', accept: 'application/json', } @@ -121,15 +121,6 @@ export function mount(app: Router) { url.searchParams.set('id', id) const res = ctx.redirect(302, url.href) - // res.headers.set('set-cookie', createCookie( - // 'oauth', - // id, - // expires, - // 'HttpOnly', - // 'Secure', - // 'SameSite=Strict' - // )) - return res }]) } diff --git a/as/assembly/dsp/constants.ts b/as/assembly/dsp/constants.ts index 8fbc192..a2a5584 100644 --- a/as/assembly/dsp/constants.ts +++ b/as/assembly/dsp/constants.ts @@ -1,4 +1,5 @@ export const BUFFER_SIZE = 2048 +export const MAX_AUDIOS = 1024 export const MAX_FLOATS = 4096 export const MAX_LISTS = 4096 export const MAX_LITERALS = 4096 @@ -7,3 +8,4 @@ export const MAX_RMSS = 1024 export const MAX_SCALARS = 4096 export const MAX_SOUNDS = 16 export const MAX_TRACKS = 16 +export const MAX_VALUES = 1024 diff --git a/as/assembly/dsp/core/clock.ts b/as/assembly/dsp/core/clock.ts index 06ed8af..9926b1c 100644 --- a/as/assembly/dsp/core/clock.ts +++ b/as/assembly/dsp/core/clock.ts @@ -4,7 +4,7 @@ export class Clock { timeStep: f64 = 0 prevTime: f64 = -1 startTime: f64 = 0 - endTime: f64 = 16 + endTime: f64 = Infinity bpm: f64 = 60 coeff: f64 = 0 barTime: f64 = 0 diff --git a/as/assembly/dsp/index.ts b/as/assembly/dsp/index.ts index 4c267b4..7ac3bdc 100644 --- a/as/assembly/dsp/index.ts +++ b/as/assembly/dsp/index.ts @@ -65,6 +65,10 @@ export function getSoundAudio(sound$: usize, index: i32): usize { return changetype(changetype(sound$).audios[index]) } +export function getSoundValue(sound$: usize, index: i32): usize { + return changetype(changetype(sound$).values[index]) +} + export function getSoundLiterals(sound$: usize): usize { return changetype(changetype(sound$).literals) } diff --git a/as/assembly/dsp/shared.ts b/as/assembly/dsp/shared.ts index d8736d6..65475aa 100644 --- a/as/assembly/dsp/shared.ts +++ b/as/assembly/dsp/shared.ts @@ -12,3 +12,11 @@ export class Out { L$: usize = 0 R$: usize = 0 } + +@unmanaged +export class SoundValue { + kind: i32 = 0 + ptr: i32 = 0 + scalar$: i32 = 0 + audio$: i32 = 0 +} diff --git a/as/assembly/dsp/vm/dsp.ts b/as/assembly/dsp/vm/dsp.ts index 14a5a81..6f90f8f 100644 --- a/as/assembly/dsp/vm/dsp.ts +++ b/as/assembly/dsp/vm/dsp.ts @@ -5,7 +5,7 @@ import { Gen } from '../gen/gen' import { fill } from '../graph/fill' import { BinOpAudioAudio, BinOpAudioScalar, BinOpScalarAudio, BinOpScalarScalar } from './bin-op' import { DspBinaryOp, SoundValueKind } from './dsp-shared' -import { Sound, SoundValue } from './sound' +import { Sound } from './sound' export { DspBinaryOp } @@ -36,9 +36,9 @@ export class Dsp { } @inline CreateValues(snd: Sound, count: i32): void { - for (let x = 0; x < count; x++) { - snd.values.push(new SoundValue(SoundValueKind.Null, 0)) - } + // for (let x = 0; x < count; x++) { + // snd.values.push(new SoundValue(SoundValueKind.Null, 0)) + // } } @inline AudioToScalar(snd: Sound, audio$: i32, scalar$: i32): void { diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index 4ac0d58..9b22800 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -1,9 +1,9 @@ import { run as dspRun } from '../../../../generated/assembly/dsp-runner' -import { BUFFER_SIZE, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_RMSS, MAX_SCALARS } from '../constants' +import { BUFFER_SIZE, MAX_AUDIOS, MAX_FLOATS, MAX_LISTS, MAX_LITERALS, MAX_SCALARS, MAX_VALUES } from '../constants' import { Clock } from '../core/clock' import { Engine } from '../core/engine' import { Gen } from '../gen/gen' -import { Out, Track } from '../shared' +import { Out, SoundValue, Track } from '../shared' import { SoundValueKind } from './dsp-shared' const enum Globals { @@ -17,14 +17,15 @@ export function ntof(n: f32): f32 { return 440 * 2 ** ((n - 69) / 12) } -export class SoundValue { - constructor( - public kind: SoundValueKind, - public ptr: i32, - ) { } - scalar$: i32 = 0 - audio$: i32 = 0 -} +// { n= 2 n 69 - 12 / ^ 440 * } ntof= +// export class SoundValue { +// constructor( +// public kind: SoundValueKind, +// public ptr: i32, +// ) { } +// scalar$: i32 = 0 +// audio$: i32 = 0 +// } export class Sound { constructor(public engine: Engine) { } @@ -37,14 +38,18 @@ export class Sound { prevGens: Gen[] = [] offsets: usize[][] = [] - audios: Array> = new Array>(MAX_RMSS).map(() => new StaticArray(BUFFER_SIZE)) + audios: StaticArray[] = new Array>(MAX_AUDIOS).map(() => new StaticArray(BUFFER_SIZE)) + values: SoundValue[] = new Array(MAX_VALUES).map((): SoundValue => { + const value: SoundValue = new SoundValue() + value.kind = SoundValueKind.Null + return value + }) + floats: StaticArray = new StaticArray(MAX_FLOATS) lists: StaticArray = new StaticArray(MAX_LISTS) literals: StaticArray = new StaticArray(MAX_LITERALS) scalars: StaticArray = new StaticArray(MAX_SCALARS) - values: SoundValue[] = [] - @inline reset(): void { this.gens.forEach(gen => { @@ -59,7 +64,7 @@ export class Sound { this.prevGens = this.gens this.gens = [] this.offsets = [] - this.values = [] + // this.values = [] // this.audios = [] } diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index a42021a..3f1aaca 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Sun Oct 20 2024 23:15:37 GMT+0300 (Eastern European Summer Time) +// auto-generated Tue Oct 22 2024 15:32:02 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/dsp-node.ts index 526fd5f..a3b1348 100644 --- a/src/as/dsp/dsp-node.ts +++ b/src/as/dsp/dsp-node.ts @@ -7,6 +7,7 @@ import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' import { DspWorklet, type DspProcessorOptions } from '~/src/as/dsp/dsp-worklet.ts' import dspWorkletUrl from '~/src/as/dsp/dsp-worklet.ts?url' +import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' import { Clock, DspWorkletMode, Track } from '~/src/as/dsp/shared.ts' import { AstNode, interpret } from '~/src/lang/interpreter.ts' import { Token, tokenize } from '~/src/lang/tokenize.ts' @@ -117,20 +118,6 @@ export function createDspNode(ctx: AudioContext) { createNode() } - const preTokens = Array.from(tokenize({ - // we implicit call [nrate 1] before our code - // so that the sample rate is reset. - code: `[nrate 1]` - // some builtin procedures - // + ` { .5* .5+ } norm= ` - // + ` { at= p= sp= 1 [inc sp co* at] clip - p^ } dec= ` - + ` [zero] ` - })) - - const postTokens = Array.from(tokenize({ - code: `@` - })) - function getTrackContext(track: Track) { const { view } = $.of(info) const literalsf = view.getF32(track.literals$, MAX_LITERALS) @@ -477,7 +464,7 @@ export function createDspNode(ctx: AudioContext) { const { tracks, currTrack, nextTrack } = $.of(info) let track = tracks[currTrack] - const tokens = Array.from(tokenize({ code })) + const tokens = Array.from(tokenize({ code: code.replaceAll('\n', '\r\n') })) // fix invisible tokens bounds to the // last visible token for errors @@ -489,14 +476,14 @@ export function createDspNode(ctx: AudioContext) { x.right = last.right x.bottom = last.bottom x.index = last.index - x.length = last.length + x.length = -1 return x } const tokensCopy = [ - ...preTokens.map(fixToken), + ...preTokens, //.map(fixToken), ...tokens, - ...postTokens.map(fixToken), + ...postTokens, //.map(fixToken), ].filter(t => t.type !== Token.Type.Comment).map(shallowCopy) // create hash id from tokens. We compare this afterwards to determine diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts index 698449f..ef320b2 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/dsp-worklet.ts @@ -1,5 +1,5 @@ import { getMemoryView, omit, rpc, toRing, wasmSourceMap } from 'utils' -import { BUFFER_SIZE, MAX_RMSS, MAX_TRACKS } from '~/as/assembly/dsp/constants.ts' +import { BUFFER_SIZE, MAX_AUDIOS, MAX_SCALARS, MAX_TRACKS, MAX_VALUES } from '~/as/assembly/dsp/constants.ts' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import hex from '~/as/build/dsp-nort.wasm?raw-hex' import dspConfig from '~/asconfig-dsp-nort.json' @@ -81,7 +81,9 @@ async function setup({ sourcemapUrl }: SetupOptions) { const player$ = wasm.createPlayer(sound$, out$) const player_track$ = player$ + wasm.getPlayerTrackOffset() - const player_audios$$ = Array.from({ length: MAX_RMSS }, (_, index) => wasm.getSoundAudio(sound$, index)) + const player_audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound$, index)) + const player_values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound$, index)) + const player_scalars = view.getF32(wasm.getSoundScalars(sound$), MAX_SCALARS) const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) const setup_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) @@ -98,6 +100,8 @@ async function setup({ sourcemapUrl }: SetupOptions) { player$, player_track$, player_audios$$, + player_values$$, + player_scalars, tracks$$, run_ops$$, setup_ops$$, diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index 0352c56..15db067 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -1,5 +1,5 @@ import { Sigui } from 'sigui' -import { fromEntries, getMemoryView, keys } from 'utils' +import { fromEntries, getMemoryView, keys, shallowCopy } from 'utils' import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { Op } from '~/generated/assembly/dsp-op.ts' @@ -13,6 +13,7 @@ import { getAllProps } from './util.ts' import { Value } from './value.ts' import { wasm } from './wasm.ts' import type { DspApi } from '~/src/as/dsp/dsp-node.ts' +import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' const DEBUG = false @@ -36,13 +37,6 @@ function getContext() { } } -function copy>(obj: T): T { - return { ...obj } -} - -let preTokens: Token[] -let postTokens: Token[] - export function Dsp({ sampleRate, core$ }: { sampleRate: number core$?: ReturnType @@ -57,22 +51,6 @@ export function Dsp({ sampleRate, core$ }: { const view = getMemoryView(wasm.memory) - preTokens ??= [...tokenize({ - code: - // we implicit call [nrate 1] before our code - // so that the sample rate is reset. - `[nrate 1]` - // some builtin procedures - + ` { .5* .5+ } norm= ` - + ` { at= p= sp= 1 [inc sp co* at] clip - p^ } dec= ` - // + `{ n= p= sp= 1 [inc sp co* t n*] clip - p^ } decay=` - // + ` { t= p= sp= 1 [inc sp co* t] clip - p^ } down= ` - })] - - postTokens ??= [...tokenize({ - code: `@` - })] - function Sound() { const sound$ = pin(wasm.createSound(engine$)) @@ -434,7 +412,7 @@ export function Dsp({ sampleRate, core$ }: { x.right = last.right x.bottom = last.bottom x.index = last.index - x.length = last.length + x.length = -1 return x } @@ -442,12 +420,12 @@ export function Dsp({ sampleRate, core$ }: { ...preTokens.map(fixToken), ...tokens, ...postTokens.map(fixToken), - ].filter(t => t.type !== Token.Type.Comment).map(copy) + ].filter(t => t.type !== Token.Type.Comment).map(shallowCopy) // create hash id from tokens. We compare this afterwards to determine // if we should make a new sound or update the old one. - const hashId = - [tokens.filter(t => t.type === Token.Type.Number).length].join('') + const hashId = '' + + [tokens.filter(t => t.type === Token.Type.Number).length].join('') + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') const isNew = hashId !== prevHashId @@ -484,19 +462,19 @@ export function Dsp({ sampleRate, core$ }: { scope.rt = new AstNode(AstNode.Type.Result, { value: rt }) scope.co = new AstNode(AstNode.Type.Result, { value: co }) - for (let i = 0; i < 6; i++) { - for (const p of 'nftv') { - const name = `n${i}${p}` - const value = (globals as any)[name] = sound.Value.Scalar.create() - scope[name] = new AstNode(AstNode.Type.Result, { value }) - } - } - - for (let i = 0; i < 6; i++) { - const name = `p${i}` - const value = (globals as any)[name] = sound.Value.Scalar.create() - scope[name] = new AstNode(AstNode.Type.Result, { value }) - } + // for (let i = 0; i < 6; i++) { + // for (const p of 'nftv') { + // const name = `n${i}${p}` + // const value = (globals as any)[name] = sound.Value.Scalar.create() + // scope[name] = new AstNode(AstNode.Type.Result, { value }) + // } + // } + + // for (let i = 0; i < 6; i++) { + // const name = `p${i}` + // const value = (globals as any)[name] = sound.Value.Scalar.create() + // scope[name] = new AstNode(AstNode.Type.Result, { value }) + // } const program = interpret(sound.api, scope, tokensCopy) diff --git a/src/as/dsp/pre-post.ts b/src/as/dsp/pre-post.ts new file mode 100644 index 0000000..aace3df --- /dev/null +++ b/src/as/dsp/pre-post.ts @@ -0,0 +1,16 @@ +import { tokenize } from '~/src/lang/tokenize.ts' + +export const preTokens = Array.from(tokenize({ + // we implicit call [nrate 1] before our code + // so that the sample rate is reset. + code: ` [nrate 1] ` + // some builtin procedures + // + ` { .5* .5+ } norm= ` + // + ` { at= p= sp= 1 [inc sp co* at] clip - p^ } dec= ` + + ` { x= 2 x 69 - 12 / ^ 440 * } ntof= ` + + ` [zero] ` +})) + +export const postTokens = Array.from(tokenize({ + code: `@` +})) diff --git a/src/as/dsp/preview-service.ts b/src/as/dsp/preview-service.ts index 5101ecb..2b681e0 100644 --- a/src/as/dsp/preview-service.ts +++ b/src/as/dsp/preview-service.ts @@ -1,5 +1,5 @@ import { Sigui } from 'sigui' -import { Deferred, rpc } from 'utils' +import { Deferred, getMemoryView, rpc, type MemoryView } from 'utils' import type { PreviewWorker } from './preview-worker.ts' import PreviewWorkerFactory from './preview-worker.ts?worker' @@ -20,6 +20,7 @@ export function PreviewService(ctx: AudioContext) { const info = $({ isReady: null as null | true, dsp: null as null | Awaited>, + view: null as null | MemoryView }) isReady.then(() => { @@ -29,7 +30,12 @@ export function PreviewService(ctx: AudioContext) { $.fx(() => { const { isReady } = $.of(info) $().then(async () => { - await service.createDsp(ctx.sampleRate) + const dsp = await service.createDsp(ctx.sampleRate) + const view = getMemoryView(dsp.memory) + $.batch(() => { + info.dsp = dsp + info.view = view + }) }) }) diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index c911887..4f362f5 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -4,10 +4,9 @@ self.document = { baseURI: location.origin } -import type { Value } from 'dsp' -import { Dsp, Sound, wasm } from 'dsp' -import { assign, Lru, rpc } from 'utils' -import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' +import { Dsp, Sound, Value, wasm } from 'dsp' +import { assign, getMemoryView, Lru, rpc } from 'utils' +import { BUFFER_SIZE, MAX_AUDIOS, MAX_SCALARS, MAX_VALUES } from '~/as/assembly/dsp/constants.ts' import { AstNode } from '~/src/lang/interpreter.ts' import { Token, tokenize } from '~/src/lang/tokenize.ts' @@ -19,22 +18,29 @@ const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Ar let epoch = 0 -interface WidgetInfoPartial { - index: number - floats: Float32Array | null +interface WidgetInfo { + value$: number bounds: Token.Bounds } -type WidgetInfo = (WidgetInfoPartial & { floats: WidgetInfoPartial['floats'] & {} }) +interface ListInfo { + list: Token.Bounds[] + value$: number + bounds: Token.Bounds +} -const waveWidgets: WidgetInfoPartial[] = [] -const rmsWidgets: WidgetInfoPartial[] = [] +const waveWidgets: WidgetInfo[] = [] +const rmsWidgets: WidgetInfo[] = [] +const listWidgets: ListInfo[] = [] const worker = { dsp: null as null | Dsp, error: null as null | Error, async createDsp(sampleRate: number) { this.dsp = Dsp({ sampleRate }) + return { + memory: wasm.memory, + } }, async createSound() { const dsp = this.dsp @@ -42,7 +48,12 @@ const worker = { const sound = dsp.Sound() sounds.set(sound.sound$, sound) - return sound.sound$ + + const audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound.sound$, index)) + const values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound.sound$, index)) + const scalars = getMemoryView(wasm.memory).getF32(wasm.getSoundScalars(sound.sound$), MAX_SCALARS) + + return { sound$: sound.sound$, audios$$, values$$, scalars } }, async build(sound$: number, code: string) { const dsp = this.dsp @@ -51,7 +62,8 @@ const worker = { const sound = sounds.get(sound$) if (!sound) throw new Error('Sound not found, id: ' + sound$) - const tokens = [...tokenize({ code })] + const tokens = Array.from(tokenize({ code: code.replaceAll('\n', '\r\n') })) + const { program, out } = sound.process(tokens) if (!out.LR) throw new Error('No audio in the stack.', { cause: { nodes: [] } }) @@ -62,36 +74,47 @@ const worker = { ) let last: AstNode | null = null - const nodes = new Map() + const nodes = new Map() let waveCount = 0 let rmsCount = 0 + let listCount = 0 for (const node of program.value.results) { - if ('genId' in node || 'op' in node) { + if (node.result.value instanceof Value && ('genId' in node || 'op' in node)) { const bounds = node.result.bounds + // pre-post tokens are set to length -1 to be ignored + if (bounds.length === -1) continue + if (last && last.bounds.line === bounds.line && last.bounds.right > bounds.col) { last.bounds.right = bounds.col - 1 nodes.get(last)!.bounds.right = bounds.col - 1 } - let info: WidgetInfoPartial + let info: WidgetInfo if ('genId' in node) { - info = (waveWidgets[waveCount] ??= { index: -1, floats: null, bounds }) + info = (waveWidgets[waveCount] ??= { value$: -1, bounds }) waveCount++ } else if ('op' in node) { - info = (rmsWidgets[rmsCount] ??= { index: -1, floats: null, bounds }) + if ('list' in node) { + const info = (listWidgets[listCount] ??= { list: [], value$: -1, bounds }) + info.list = node.list.scope.stack.map(n => n.bounds) + info.value$ = (node.index.value as Value.Dynamic).value$ + assign(info.bounds, bounds) + listCount++ + continue + } + info = (rmsWidgets[rmsCount] ??= { value$: -1, bounds }) rmsCount++ } else { throw new Error('unreachable') } - info.index = (node.result.value as Value.Audio).getAudio() - info.floats = sound.getAudio(info.index) + info.value$ = (node.result.value as Value.Dynamic).value$ assign(info.bounds, bounds) nodes.set(node.result, info) @@ -106,6 +129,9 @@ const worker = { delta = rmsWidgets.length - rmsCount while (delta-- > 0) rmsWidgets.pop() + delta = listWidgets.length - listCount + while (delta-- > 0) listWidgets.pop() + const LR = out.LR.getAudio() const length = BUFFER_SIZE @@ -118,6 +144,7 @@ const worker = { floats, waves: waveWidgets as WidgetInfo[], rmss: rmsWidgets as WidgetInfo[], + lists: listWidgets as ListInfo[], } }, async renderSource(sound$: number, code: string) { @@ -139,7 +166,7 @@ const worker = { wasm.clockUpdate(clock.ptr) - const { LR, floats, waves, rmss } = await this.build(sound$, code) + const { LR, floats, waves, rmss, lists } = await this.build(sound$, code) wasm.fillSound( sound.sound$, @@ -150,7 +177,7 @@ const worker = { floats.ptr, ) - return { LR, floats, waves, rmss } + return { LR, floats, waves, rmss, lists } } catch (e) { if (e instanceof Error) { diff --git a/src/as/dsp/shared.ts b/src/as/dsp/shared.ts index ba197e6..f2604a1 100644 --- a/src/as/dsp/shared.ts +++ b/src/as/dsp/shared.ts @@ -42,3 +42,12 @@ export const Out = Struct({ L$: 'usize', R$: 'usize', }) + +export type SoundValue = typeof SoundValue.type + +export const SoundValue = Struct({ + kind: 'i32', + ptr: 'i32', + scalar$: 'i32', + audio$: 'i32', +}) diff --git a/src/as/dsp/value.ts b/src/as/dsp/value.ts index 76aea66..bbadea3 100644 --- a/src/as/dsp/value.ts +++ b/src/as/dsp/value.ts @@ -57,6 +57,14 @@ export class Value { return this.ptr } } + getScalar() { + if (this.kind === Value.Kind.Dynamic) { + return this.scalar$ + } + else { + return this.ptr + } + } } export namespace Value { diff --git a/src/lang/interpreter.test.ts b/src/lang/interpreter.test.ts new file mode 100644 index 0000000..9e8a8d1 --- /dev/null +++ b/src/lang/interpreter.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, mock } from "bun:test" +import { interpret } from '~/src/lang/interpreter.ts' +import { tokenize } from '~/src/lang/tokenize.ts' + +describe('interpret', () => { + it('works', () => { + const tokens = Array.from(tokenize({ code: '[sin 42]' })) + const api = { + gen: { sin: mock() } + } as any + const result = interpret(api, {}, tokens) + expect(api.gen.sin).toHaveBeenCalledTimes(1) + expect(api.gen.sin).toHaveBeenCalledWith({ hz: { value: 42, format: 'f', digits: 0 } }) + }) + it('procedure', () => { + const tokens = Array.from(tokenize({ + code: ` + { x= x 2 * } double= + [sin 21 double] + ` })) + const api = { + math: { + '*': mock((a: any, b: any) => { + return { value: a.value * b.value } + }) + }, + gen: { sin: mock() } + + } as any + const result = interpret(api, {}, tokens) + expect(api.gen.sin).toHaveBeenCalledTimes(1) + expect(api.gen.sin).toBeCalledWith({ hz: { value: 42 } }) + }) +}) diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts index 804004d..0cfd0e9 100644 --- a/src/lang/interpreter.ts +++ b/src/lang/interpreter.ts @@ -1,6 +1,6 @@ -import { getAllPropsReverse } from 'dsp' import { dspGens } from '~/generated/typescript/dsp-gens.ts' import type { DspApi } from '~/src/as/dsp/dsp-node.ts' +import { getAllPropsReverse } from '~/src/as/dsp/util.ts' import type { Value } from '~/src/as/dsp/value.ts' import { Token } from '~/src/lang/tokenize.ts' import { parseNumber, type NumberFormat, type NumberInfo } from '~/src/lang/util.ts' @@ -172,13 +172,15 @@ export function interpret(g: DspApi, data: Record, tokens: Token[]) type Context = ReturnType + const uncaptured = new Set() + function createContext(tokens: Token[]) { let i = 0 let t: Token function next() { t = tokens[i++] - capturing.forEach(c => c.push(t)) + if (!uncaptured.has(t)) capturing.forEach(c => c.push(t)) return t } function peek() { @@ -398,7 +400,11 @@ export function interpret(g: DspApi, data: Record, tokens: Token[]) case '{': { const op = t.text const close = Token.Close[op] - const node = new AstNode(AstNode.Type.Procedure, { value: c.tokensUntil(close) }) + const value = c.tokensUntil(close) + // we prevent proc tokens from being captured + // when the procedure is invoked at a different location + value.tokens.forEach(t => uncaptured.add(t)) + const node = new AstNode(AstNode.Type.Procedure, { value }) node.kind = AstNode.ProcKind.User return node } diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index 9eedc41..e91c8ab 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -4,11 +4,14 @@ import { assign, Lru, throttle } from 'utils' import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import { createDspNode } from '~/src/as/dsp/dsp-node.ts' import { PreviewService } from '~/src/as/dsp/preview-service.ts' +import { SoundValue } from '~/src/as/dsp/shared.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' +import type { Token } from '~/src/lang/tokenize.ts' +import { screen } from '~/src/screen.ts' import { state } from '~/src/state.ts' import { Button } from '~/src/ui/Button.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlDecoWidget, RmsDecoWidget } from '~/src/ui/editor/widgets/index.ts' +import { WaveGlDecoWidget, RmsDecoWidget, ListMarkWidget } from '~/src/ui/editor/widgets/index.ts' import { copyRingInto } from '~/src/util/copy-ring-into.ts' /* @@ -23,6 +26,28 @@ t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig= [noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* [noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* +t 8* y= +[saw (35 38 42 35) t 12* ? ntof] [slp 227 .8] 2* [tanh] [delay 16 .9] [slp 289 [exp 1 y trig=] 1^ 2290*+ .9] [exp 6 y trig=] .8^ * a= a .6* [delay 892 [sin 0.820] 38 * + .69] +[shp 466] a [shp 473] @ [atan] .8* + +(1 2 3) t 8 * ? + +t 8* y= +[saw (35 38 42 35) t 8* ? ntof] [slp 227 .8] 2* [tanh] [delay 16 .9] [slp 289 [exp 1 y trig=] 1^ 2290*+ .9] [exp .05 y 8 / trig=] 15.8^ * a= a .6* [delay 892 [sin 0.820] 38 * + .69] +[shp 466] a [shp 473] @ [atan] [blp 5555 .8] .8* + +t 8* y= +[saw (35 38 42 35) t 1* ? ntof] [slp 227 .8] 2* [tanh] [delay 16 .9] [slp 289 [exp 1 y trig=] 1^ 2290*+ .9] [exp .05 y 8 / trig=] 15.8^ * a= a .6* [delay 892 [sin 0.820] 38 * + .69] +[shp 466] a [shp 473] @ [atan] [blp 5555 .8] .8* + +[saw (4 5 0) t 1 * ? 40 + ntof] +[saw (8 12 4) t 1 * ? 40 + ntof] @ [slp 277] [shp 251] .14* +t 4* x= [sin 100.00 409 [exp 1.00 x] 31.88^ * + x] [exp 1.00 x] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] 1* +[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x] * * [shp 7090 .7] .21* + +t 4* y= +[saw (35 38 42 40) 8 t* ? ntof] [exp 8 y] [lp 4.40] .5^ * .27 * [slp 265 5171 [exp 1 y] [lp 66.60] 1.35^ * + .9] + */ const getFloatsGfx = Lru(1024, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) @@ -30,17 +55,29 @@ export function DspNodeDemo() { using $ = Sigui() const info = $({ - get width() { return state.containerWidth / 2 }, - height: state.$.containerHeight, - code: `t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] -[saw (92 353 50 218) t 12* ? [sin 1 x trig=] 9^ 61* + x trig=] [clip .4] .7* [slp 156 22k [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [sno 516 2181 [sin .2 co * t .5 - trig=] * + ] [delay 15 .73] .59* -[noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* -[noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* + get width() { return screen.lg ? state.containerWidth / 2 : state.containerWidth }, + get height() { return screen.lg ? state.containerHeight : state.containerHeight / 2 }, + code: `t 4* y= +[saw (35 38 42 40) 8 t* ? ntof] [exp 8 y] [lp 4.40] .5^ * .27 * [slp 265 5171 [exp 1 y] [lp 66.60] 1.35^ * + .9] `, + //`(1 2 3) t 8 * ?`, + // `t 8* y= + // [saw (35 38 42 35) t 8* ? ntof] [slp 227 .8] 2* [tanh] [delay 16 .9] [slp 289 [exp 1 y trig=] 1^ 2290*+ .9] [exp .05 y 8 / trig=] 15.8^ * a= a .6* [delay 892 [sin 0.820] 38 * + .69] + // [shp 466] a [shp 473] @ [atan] [blp 5555 .8] .8* + // `, + // `t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] + // [saw (92 353 50 218) t 12* ? [sin 1 x trig=] 9^ 61* + x trig=] [clip .4] .7* [slp 156 22k [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [sno 516 2181 [sin .2 co * t .5 - trig=] * + ] [delay 15 .73] .59* + // [noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* + // [noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* + // `, codeWorking: null as null | string, audios: [] as Float32Array[], + values: [] as SoundValue[], floats: new Float32Array(), - sound$: null as null | number, + previewSound$: null as null | number, + previewAudios: [] as Float32Array[], + previewValues: [] as SoundValue[], + previewScalars: new Float32Array(), error: null as null | Error, }) @@ -63,20 +100,23 @@ export function DspNodeDemo() { const { dsp, view } = $.of(dspNode.info) $() info.audios = dsp?.player_audios$$.map(ptr => view.getF32(ptr, BUFFER_SIZE)) + info.values = dsp?.player_values$$.map(ptr => SoundValue(view.memory.buffer, ptr)) }) $.fx(() => { - const { audios } = info - const { isPlaying, clock } = $.of(dspNode.info) + const { audios, values } = $.of(info) + const { isPlaying, clock, dsp: { player_scalars } } = $.of(dspNode.info) $() if (isPlaying) { const { pane } = dspEditor.editor.info let animFrame: any const tick = () => { for (const wave of [...waveWidgets, plot]) { + // values[info] + // console.log('resultValue', values[(wave as any).info.resultValue?.value$]) copyRingInto( wave.info.stabilizerTemp, - audios[wave.info.index + 1], + audios[wave.info.index], clock.ringPos, wave.info.stabilizerTemp.length, 15 @@ -85,11 +125,16 @@ export function DspNodeDemo() { wave.info.floats.set(wave.info.stabilizerTemp.subarray(startIndex)) wave.info.floats.set(wave.info.stabilizerTemp.subarray(0, startIndex), startIndex) } + for (const rms of rmsWidgets) { - rms.update(audios[rms.info.index + 1]) + rms.update(audios, values, player_scalars) } + + for (const list of listWidgets) { + list.update(audios, values, player_scalars) + } + pane.view.anim.info.epoch++ - c.meshes.draw() animFrame = requestAnimationFrame(tick) } tick() @@ -111,36 +156,57 @@ export function DspNodeDemo() { const shapes = c.createShapes() c.sketch.scene.add(shapes) - const plot = WaveGlDecoWidget(shapes) - plot.widget.rect.w = view.width - plot.widget.rect.h = view.height + const widgetRect = Rect(0, 0, info.$.width, info.$.height) + const plot = WaveGlDecoWidget(shapes, widgetRect) plot.info.stabilizerTemp = getFloatsGfx('s:LR', BUFFER_SIZE) plot.info.previewFloats = getFloatsGfx('p:LR', BUFFER_SIZE) plot.info.floats = getFloatsGfx(`LR`, BUFFER_SIZE) const waveWidgets: WaveGlDecoWidget[] = [] const rmsWidgets: RmsDecoWidget[] = [] + const listWidgets: ListMarkWidget[] = [] $.fx(() => { - const { isReady } = $.of(preview.info) + const { isReady, view: previewView } = $.of(preview.info) $().then(async () => { - info.sound$ = await preview.service.createSound() + const result = await preview.service.createSound() + $.batch(() => { + info.previewSound$ = result.sound$ + info.previewAudios = result.audios$$.map(ptr => previewView.getF32(ptr, BUFFER_SIZE)) + info.previewValues = result.values$$.map(ptr => SoundValue(previewView.memory.buffer, ptr)) + info.previewScalars = result.scalars + }) }) }) async function build() { - const { sound$ } = info - if (!sound$) return + const { previewSound$, previewAudios, previewValues, previewScalars } = info + if (!previewSound$) return const { pane } = dspEditor.editor.info - const { codeVisual } = pane.buffer.info + const { code } = pane.buffer.info + + function fixBounds(bounds: Token.Bounds) { + let newBounds = { ...bounds } + { + const { x, y } = pane.buffer.logicalPointToVisualPoint({ x: bounds.col, y: bounds.line }) + newBounds.line = y + newBounds.col = x + } + { + const { x, y } = pane.buffer.logicalPointToVisualPoint({ x: bounds.right, y: bounds.line }) + newBounds.right = x + } + return newBounds + } let result: Awaited> let waveCount = 0 let rmsCount = 0 + let listCount = 0 try { - result = await preview.service.renderSource(sound$, codeVisual) + result = await preview.service.renderSource(previewSound$, code) const { isPlaying } = dspNode.info @@ -156,7 +222,7 @@ export function DspNodeDemo() { $.batch(() => { info.error = null - info.codeWorking = codeVisual + info.codeWorking = code }) const end = $.batch() @@ -180,11 +246,12 @@ export function DspNodeDemo() { ? wave.info.stabilizerTemp : getFloatsGfx(`s:${waveCount}`, BUFFER_SIZE) - wave.info.index = waveInfo.index - wave.info.previewFloats.set(waveInfo.floats) - if (!isPlaying) wave.info.floats.set(waveInfo.floats) + wave.info.index = previewValues[waveInfo.value$].ptr + const audio = previewAudios[wave.info.index] + wave.info.previewFloats.set(audio) + if (!isPlaying) wave.info.floats.set(audio) - assign(wave.widget.bounds, waveInfo.bounds) + assign(wave.widget.bounds, fixBounds(waveInfo.bounds)) pane.draw.widgets.deco.add(wave.widget) waveCount++ } @@ -192,15 +259,26 @@ export function DspNodeDemo() { for (const rmsInfo of result.rmss) { const rms = (rmsWidgets[rmsCount] ??= RmsDecoWidget(pane.draw.shapes)) - rms.info.index = rmsInfo.index - - if (!isPlaying) rms.update(rmsInfo.floats) + rms.info.index = previewValues[rmsInfo.value$].ptr + rms.info.value$ = rmsInfo.value$ + if (!isPlaying) rms.update(previewAudios, previewValues, previewScalars) - assign(rms.widget.bounds, rmsInfo.bounds) + assign(rms.widget.bounds, fixBounds(rmsInfo.bounds)) pane.draw.widgets.deco.add(rms.widget) rmsCount++ } + for (const listInfo of result.lists) { + const list = (listWidgets[listCount] ??= ListMarkWidget(pane)) + + list.info.list = listInfo.list.map(bounds => fixBounds(bounds)) + list.info.indexValue$ = listInfo.value$ + + assign(list.widget.bounds, list.info.list[0]) + pane.draw.widgets.mark.add(list.widget) + listCount++ + } + end() } catch (err) { @@ -218,9 +296,11 @@ export function DspNodeDemo() { delta = rmsWidgets.length - rmsCount while (delta-- > 0) rmsWidgets.pop()?.dispose() - pane.draw.info.triggerUpdateTokenDrawInfo++ + delta = listWidgets.length - listCount + while (delta-- > 0) listWidgets.pop()?.dispose() - c.meshes.draw() + pane.draw.info.triggerUpdateTokenDrawInfo++ + pane.view.anim.ticks.add(c.meshes.draw) pane.view.anim.info.epoch++ pane.draw.widgets.update() } @@ -229,7 +309,7 @@ export function DspNodeDemo() { queueMicrotask(() => { $.fx(() => { - const { sound$ } = $.of(info) + const { previewSound$ } = $.of(info) const { pane } = dspEditor.editor.info const { codeVisual } = pane.buffer.info const { isPlaying } = dspNode.info @@ -252,7 +332,7 @@ export function DspNodeDemo() { return () => dspEditor.info.error = null }) - return
+ return
diff --git a/src/screen.ts b/src/screen.ts index 4671272..9ebf13d 100644 --- a/src/screen.ts +++ b/src/screen.ts @@ -6,10 +6,10 @@ export const screen = $({ width: window.visualViewport!.width, height: window.visualViewport!.height, get sm() { - return screen.width < 640 + return screen.width < 680 }, get md() { - return screen.width >= 640 + return screen.width >= 680 }, get lg() { return screen.width >= 768 @@ -23,8 +23,4 @@ $.fx(() => [ screen.width = viewport.width screen.height = viewport.height }), { unsafeInitial: true }), - - dom.on(document, 'focus', () => { - console.log('trigger focus') - }) ]) diff --git a/src/ui/editor/buffer.test.ts b/src/ui/editor/buffer.test.ts index d77953b..c171a37 100644 --- a/src/ui/editor/buffer.test.ts +++ b/src/ui/editor/buffer.test.ts @@ -4,13 +4,14 @@ import { Buffer } from '~/src/ui/editor/buffer.ts' import { Dims } from '~/src/ui/editor/dims.ts' import { Rect } from '~/src/ui/editor/util/types.ts' -function B(code: string, maxWidth: number) { +function B(code: string, maxWidth: number, breakWords = true) { const info = $({ code, width: maxWidth, height: 300 }) const rect = $(Rect(), { w: 100 }) const dims = Dims({ rect }) dims.info.charWidth = 100 / maxWidth const b = Buffer({ dims, code: info.$.code, tokenize }) b.info.maxColumns = maxWidth + b.info.breakWords = breakWords return b } @@ -56,7 +57,7 @@ describe('TextBuffer', () => { ]) }) - it('splits words', () => { + it('breaks words', () => { const b = B('helloworld loremipsum', 8) expect(b.info.linesVisual).toEqual([ { text: 'hellowor' }, @@ -66,6 +67,15 @@ describe('TextBuffer', () => { ]) }) + it('does not break words when breakWords=false', () => { + const b = B('helloworld loremipsum', 8, false) + b.info.breakWords = false + expect(b.info.linesVisual).toEqual([ + { text: 'helloworld ' }, + { text: 'loremipsum' }, + ]) + }) + // it('splits words and lines', () => { // const lines = wordWrapText(l('helloworld loremipsum'), 5) // expect(lines).toEqual(['hello', 'world', 'lorem', 'ipsum']) diff --git a/src/ui/editor/buffer.ts b/src/ui/editor/buffer.ts index a9a0e84..f823a7f 100644 --- a/src/ui/editor/buffer.ts +++ b/src/ui/editor/buffer.ts @@ -16,21 +16,23 @@ export interface WordWrapProcessor { pre(s: string): string post(s: string): string } + export type Buffer = ReturnType function identity(x: any) { return x } -export function Buffer({ dims, code, tokenize, wordWrapProcessor = { pre: identity, post: identity } }: { +export function Buffer({ dims, code, tokenize, wordWrapProcessor: { pre = identity, post = identity } = {} }: { dims: Dims code: Signal tokenize: (source: Source) => Generator - wordWrapProcessor?: WordWrapProcessor + wordWrapProcessor?: Partial }) { using $ = Sigui() const info = $({ maxColumns: dims.info.$.pageWidth, wordWrapEnabled: true, + breakWords: true, code, lines: [] as string[], get length() { @@ -72,18 +74,18 @@ export function Buffer({ dims, code, tokenize, wordWrapProcessor = { pre: identi }) function wordWrap(): Line[] { - let { code, maxColumns } = info + let { code, maxColumns, breakWords } = info const wrapped: Line[] = [] let line = '' let word = '' let x = 0 - code = wordWrapProcessor.pre(code) + code = pre(code) function push() { const joined = line + word - if (joined.length > maxColumns) { + if (joined.length > maxColumns && breakWords) { wrapped.push({ text: line }) if (word.length) wrapped.push({ text: word }) } @@ -105,15 +107,21 @@ export function Buffer({ dims, code, tokenize, wordWrapProcessor = { pre: identi } else if (c === ' ') { if (x >= maxColumns) { - push() - word = c + if (breakWords) { + push() + word = c + } + else { + word += c + push() + } } else { line += word + c word = '' } } - else { + else if (breakWords) { if (word.length >= maxColumns) { wrapped.push({ text: word }) word = '' @@ -126,6 +134,9 @@ export function Buffer({ dims, code, tokenize, wordWrapProcessor = { pre: identi x = 0 } } + else { + word += c + } x++ } @@ -134,7 +145,7 @@ export function Buffer({ dims, code, tokenize, wordWrapProcessor = { pre: identi // } return wrapped.map(line => { - line.text = wordWrapProcessor.post(line.text) + line.text = post(line.text) return line }) } diff --git a/src/ui/editor/draw.ts b/src/ui/editor/draw.ts index dfad20c..95deea8 100644 --- a/src/ui/editor/draw.ts +++ b/src/ui/editor/draw.ts @@ -1,4 +1,4 @@ -import { Point, pointToLinecol, Widgets, type Buffer, type Caret, type Dims, type Linecol, type PaneInfo, type Selection, type View } from 'editor' +import { Point, pointToLinecol, Widgets, type Buffer, type Caret, type Dims, type Linecol, type PaneInfo, type Selection, type View, type Widget } from 'editor' import { Matrix, Rect } from 'gfx' import { Sigui } from 'sigui' import { assign, clamp, drawText, randomHex } from 'utils' @@ -170,6 +170,16 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize return { line, col, hoverLine } } + function updateMarkRect(w: Widget) { + const { charWidth, lineHeight } = dims.info + const b = w.bounds + const p = viewPointFromLinecol(b) + w.rect.x = p.x - 1 + w.rect.y = p.y - 1 + w.rect.w = (((b.right - b.col) * charWidth) | 0) + 2.75 + w.rect.h = (lineHeight) - .5 + } + // update token draw info $.fx(() => { const { triggerUpdateTokenDrawInfo } = info @@ -208,14 +218,7 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize w.rect.h = widgets.heights.subs }) - widgets.mark.forEach(w => { - const b = w.bounds - const p = viewPointFromLinecol(b) - w.rect.x = p.x - 1 - w.rect.y = p.y - 1 - w.rect.w = (((b.right - b.col) * charWidth) | 0) + 2.75 - w.rect.h = (lineHeight) - .5 - }) + widgets.mark.forEach(updateMarkRect) tokens.forEach(token => { const point = viewPointFromLinecol(token) @@ -227,6 +230,8 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize stroke, }) }) + + info.shouldRedraw = true }) // update caret view point @@ -442,6 +447,8 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize webgl, shapes: webglShapes, widgets, - linecolFromViewPoint + linecolFromViewPoint, + viewPointFromLinecol, + updateMarkRect, } } diff --git a/src/ui/editor/input.ts b/src/ui/editor/input.ts index c71907f..1181d08 100644 --- a/src/ui/editor/input.ts +++ b/src/ui/editor/input.ts @@ -1,9 +1,10 @@ -import { isPointInRect, type Pane, type Point, type View } from 'editor' +import { CLICK_TIMEOUT, isPointInRect, type Pane, type Point, type View } from 'editor' import { Sigui, type Signal } from 'sigui' import { assign, dom, isMobile, MouseButtons } from 'utils' export interface InputMouse extends Point { isDown: boolean + downTime: number buttons: number ctrl: boolean } @@ -93,6 +94,7 @@ export function Input({ view, pane, panes }: { // input mouse const inputMouse: InputMouse = $({ isDown: false, + downTime: 0, buttons: 0, ctrl: false, x: 0, @@ -218,6 +220,7 @@ export function Input({ view, pane, panes }: { if (!isMobile()) ev.preventDefault() updateInputMouseFromEvent(ev) inputMouse.isDown = true + inputMouse.downTime = performance.now() const { pane, hoveringPane } = info @@ -255,7 +258,9 @@ export function Input({ view, pane, panes }: { function preventAndFocus(ev: Event) { ev.preventDefault() - focus() + if (performance.now() - inputMouse.downTime < CLICK_TIMEOUT) { + focus() + } } function onFocus() { diff --git a/src/ui/editor/widget.ts b/src/ui/editor/widget.ts index 98b403c..b3c6022 100644 --- a/src/ui/editor/widget.ts +++ b/src/ui/editor/widget.ts @@ -62,9 +62,9 @@ export function Widgets({ c }: { export type Widget = ReturnType -export function Widget() { - const rect = Rect(0, 0, 1, 1) +export function Widget(rect = Rect(0, 0, 1, 1)) { + using $ = Sigui() const bounds = Bounds() function draw(c: CanvasRenderingContext2D) { } - return { rect, bounds, draw } + return $({ rect, bounds, draw }) } diff --git a/src/ui/editor/widgets/index.ts b/src/ui/editor/widgets/index.ts index 3631ed8..32d0a1d 100644 --- a/src/ui/editor/widgets/index.ts +++ b/src/ui/editor/widgets/index.ts @@ -1,5 +1,6 @@ export * from './error-sub.ts' export * from './hover-mark.ts' +export * from './list-mark.ts' export * from './rms-deco.ts' export * from './wave-canvas-deco.ts' export * from './wave-gl-deco.ts' diff --git a/src/ui/editor/widgets/list-mark.ts b/src/ui/editor/widgets/list-mark.ts new file mode 100644 index 0000000..589d22a --- /dev/null +++ b/src/ui/editor/widgets/list-mark.ts @@ -0,0 +1,43 @@ +import { hexToInt, Widget, type Pane } from 'editor' +import { Sigui } from 'sigui' +import { assign } from 'utils' +import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts' +import type { SoundValue } from '~/src/as/dsp/shared.ts' +import type { Token } from '~/src/lang/tokenize.ts' +import { modWrap } from '~/src/util/mod-wrap.ts' + +export type ListMarkWidget = ReturnType + +export function ListMarkWidget(pane: Pane) { + using $ = Sigui() + + const info = $({ + color: '#fff', + list: [] as Token.Bounds[], + indexValue$: -1, + value: -1, + }) + + const widget = Widget() + const box = pane.draw.shapes.Box(widget.rect) + box.view.color = hexToInt(info.color) + box.view.alpha = .2 + + function update(audios: Float32Array[], values: SoundValue[], scalars: Float32Array) { + const value = values[info.indexValue$] + if (value.kind === SoundValueKind.Scalar) { + info.value = scalars[value.ptr] + } + else if (value.kind === SoundValueKind.Audio) { + info.value = audios[value.ptr][0] + } + assign(widget.bounds, info.list[modWrap(info.value, info.list.length) >> 0]) + pane.draw.updateMarkRect(widget) + } + + function dispose() { + box.remove() + } + + return { info, widget, box, update, dispose } +} diff --git a/src/ui/editor/widgets/rms-deco.ts b/src/ui/editor/widgets/rms-deco.ts index d468d7d..cc926cc 100644 --- a/src/ui/editor/widgets/rms-deco.ts +++ b/src/ui/editor/widgets/rms-deco.ts @@ -4,7 +4,9 @@ import { wasm } from 'rms' import { Sigui } from 'sigui' import { getMemoryView } from 'utils' import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' +import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { ShapeOpts } from '~/as/assembly/gfx/sketch-shared.ts' +import type { SoundValue } from '~/src/as/dsp/shared.ts' export type RmsDecoWidget = ReturnType @@ -14,6 +16,7 @@ export function RmsDecoWidget(shapes: Shapes) { const info = $({ rect: Rect(), index: -1, + value$: -1, color: '#fff', peak: 0, value: 0, @@ -43,8 +46,14 @@ export function RmsDecoWidget(shapes: Shapes) { rect.y = y + h - rect.h }) - function update(floats: Float32Array) { - rmsFloats.set(floats) + function update(audios: Float32Array[], values: SoundValue[], scalars: Float32Array) { + const value = values[info.value$] + if (value.kind === SoundValueKind.Scalar) { + rmsFloats.fill(scalars[value.ptr]) + } + else if (value.kind === SoundValueKind.Audio) { + rmsFloats.set(audios[value.ptr]) + } info.value = wasm.run() } diff --git a/src/ui/editor/widgets/wave-gl-deco.ts b/src/ui/editor/widgets/wave-gl-deco.ts index dd9773c..18c89e6 100644 --- a/src/ui/editor/widgets/wave-gl-deco.ts +++ b/src/ui/editor/widgets/wave-gl-deco.ts @@ -1,16 +1,18 @@ import { Widget } from 'editor' -import { wasm, type Shapes } from 'gfx' +import { wasm, type Rect, type Shapes } from 'gfx' import { Sigui } from 'sigui' +import type { SoundValue } from '~/src/as/dsp/shared.ts' import { hexToInt } from '~/src/ui/editor/util/rgb.ts' import { Stabilizer } from '~/src/util/stabilizer.ts' export type WaveGlDecoWidget = ReturnType -export function WaveGlDecoWidget(shapes: Shapes) { +export function WaveGlDecoWidget(shapes: Shapes, rect?: Rect) { using $ = Sigui() const info = $({ index: -1, + resultValue: null as null | SoundValue, previewFloats: wasm.alloc(Float32Array, 0), floats: wasm.alloc(Float32Array, 0), stabilizer: new Stabilizer(), @@ -18,7 +20,7 @@ export function WaveGlDecoWidget(shapes: Shapes) { color: '#fff', }) - const widget = Widget() + const widget = Widget(rect) const wave = shapes.Wave(widget.rect) $.fx(() => { diff --git a/src/util/mod-wrap.ts b/src/util/mod-wrap.ts new file mode 100644 index 0000000..fc7ebe8 --- /dev/null +++ b/src/util/mod-wrap.ts @@ -0,0 +1,3 @@ +export function modWrap(x: number, N: number): number { + return (x % N + N) % N +} diff --git a/src/util/stabilizer.ts b/src/util/stabilizer.ts index 39052e4..fafec24 100644 --- a/src/util/stabilizer.ts +++ b/src/util/stabilizer.ts @@ -3,8 +3,8 @@ import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' // based on https://github.com/fenomas/webaudio-viz/blob/master/src/index.js export class Stabilizer { // overly magical ad-hoc routine to choose where to start drawing data - numOffsets = 40 - offsetsSpan = 200 + numOffsets = 32 + offsetsSpan = 128 offsets: Uint32Array = Uint32Array.from({ length: this.numOffsets }).map((_, i) => ((this.offsetsSpan / this.numOffsets) * ((i / this.numOffsets) ** 9) * this.numOffsets) | 0 ) diff --git a/vite.config.ts b/vite.config.ts index d9b73af..4189c98 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,7 @@ import path from 'node:path' import { defineConfig, loadEnv, type Plugin } from 'vite' import externalize from "vite-plugin-externalize-dependencies" import { VitePWA } from 'vite-plugin-pwa' +import { watchAndRun } from 'vite-plugin-watch-and-run' import TsConfigPaths from 'vite-tsconfig-paths' import { ViteAssemblyScript } from './vendor/vite-plugin-assemblyscript.ts' import { ViteBundleUrl } from './vendor/vite-plugin-bundle-url.ts' @@ -12,7 +13,6 @@ import { ViteHexLoader } from './vendor/vite-plugin-hex-loader.ts' import { ViteOpenInEditor } from './vendor/vite-plugin-open-in-editor.ts' import { VitePrintAddress } from './vendor/vite-plugin-print-address.ts' import { ViteUsing } from './vendor/vite-plugin-using.ts' -import { watchAndRun } from 'vite-plugin-watch-and-run' const root = process.cwd() const homedir = os.homedir() From 773ac77fa62726aea74e9529d5566a77e0d33d75 Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 22 Oct 2024 17:49:11 +0300 Subject: [PATCH 29/33] wip refactor dsp --- as/assembly/dsp/index.ts | 16 ++ generated/typescript/dsp-gens.ts | 2 +- src/as/dsp/dsp-build.ts | 465 +++++++++++++++++++++++++++++++ src/as/dsp/dsp-node.ts | 455 +----------------------------- src/as/dsp/dsp-wasm.ts | 52 ++++ src/as/dsp/dsp-worklet.ts | 51 +--- src/as/dsp/dsp.ts | 1 - src/as/dsp/preview-worker.ts | 110 +++++--- src/as/dsp/shared.ts | 4 +- src/pages/DspNodeDemo.tsx | 34 +-- 10 files changed, 634 insertions(+), 556 deletions(-) create mode 100644 src/as/dsp/dsp-build.ts create mode 100644 src/as/dsp/dsp-wasm.ts diff --git a/as/assembly/dsp/index.ts b/as/assembly/dsp/index.ts index 7ac3bdc..5c792e1 100644 --- a/as/assembly/dsp/index.ts +++ b/as/assembly/dsp/index.ts @@ -61,6 +61,22 @@ export function fillSound( ) } +export function fillTrack( + sound$: usize, + track$: usize, + begin: u32, + end: u32, + out$: usize +): void { + const sound = changetype(sound$) + sound.fillTrack( + track$, + begin, + end, + out$ + ) +} + export function getSoundAudio(sound$: usize, index: i32): usize { return changetype(changetype(sound$).audios[index]) } diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 3f1aaca..2382e60 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Tue Oct 22 2024 15:32:02 GMT+0300 (Eastern European Summer Time) +// auto-generated Tue Oct 22 2024 17:00:50 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/src/as/dsp/dsp-build.ts b/src/as/dsp/dsp-build.ts new file mode 100644 index 0000000..0665028 --- /dev/null +++ b/src/as/dsp/dsp-build.ts @@ -0,0 +1,465 @@ +import { DEBUG, getAllProps, Value as ValueBase, type Value } from 'dsp' +import { Sigui } from 'sigui' +import { fromEntries, keys, shallowCopy, type MemoryView } from 'utils' +import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' +import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' +import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' +import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' +import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' +import { Track, type Clock } from '~/src/as/dsp/shared.ts' +import { AstNode, interpret } from '~/src/lang/interpreter.ts' +import { Token, tokenize } from '~/src/lang/tokenize.ts' +import { parseNumber } from '~/src/lang/util.ts' + +const dspGensKeys = keys(dspGens) + +type GenApi = { + (opt: Record): Value | [Value, Value] | void +} + +type BinaryOp = { + (lhs: number, rhs: number): number + (lhs: number, rhs: Value): Value + (lhs: Value, rhs: number): Value + (lhs: Value, rhs: Value): Value +} + +export interface DspApi { + globals: Record + math: Record + gen: Record + gen_st: Record + pan(value: Value | number): void + pick(values: T[], index: Value | number): Value +} + +const commutative = new Set([DspBinaryOp.Add, DspBinaryOp.Mul]) +const audioProps = new Set(['in', 'sidechain']) +const textProps = new Set(['text', 'id']) + +export function getTrackContext(view: MemoryView, track: Track) { + // const { view } = $.of(info) + const literalsf = view.getF32(track.literals$, MAX_LITERALS) + const listsi = view.getI32(track.lists$, MAX_LISTS) + return { + gens: 0, + values: 0, + floats: 0, + scalars: 0, + audios: 0, + literals: 0, + literalsf, + lists: 0, + listsi, + } +} + +const vms: Map = new Map() +const contexts: Map> = new Map() +export const builds: Map = new Map() + +export type TrackBuild = ReturnType + +export function TrackBuild(track: Track) { + const { runVm, setupVm } = vms.get(track)! + const context = contexts.get(track)! + const Value = ValueBase.Factory({ context, vm: runVm }) + + function binaryOp( + BinOp: DspBinaryOp, + LiteralLiteral: (a: number, b: number) => number + ): BinaryOp { + return function binaryOp(lhs: Value | number, rhs: Value | number) { + if (typeof lhs === 'number' && typeof rhs === 'number') { + return LiteralLiteral(lhs, rhs) + } + + let out = Value.Dynamic.create() + + let l: Value + let r: Value + + if (commutative.has(BinOp)) { + if (typeof lhs === 'number') { + if (typeof rhs === 'number') throw 'unreachable' + + l = rhs + r = Value.Literal.create(lhs) + } + else if (typeof rhs === 'number') { + l = lhs + r = Value.Literal.create(rhs) + } + else { + l = lhs + r = rhs + } + } + else { + if (typeof lhs === 'number') { + if (typeof rhs === 'number') throw 'unreachable' + + l = Value.Literal.create(lhs) + r = rhs + } + else if (typeof rhs === 'number') { + l = lhs + r = Value.Literal.create(rhs) + } + else { + l = lhs + r = rhs + } + } + + if (!l) { + throw new Error('Missing left operand.') + } + + if (!r) { + throw new Error('Missing right operand.') + } + + runVm.BinaryOp( + BinOp, + l.value$, + r.value$, + out.value$ + ) + + return out + } as BinaryOp + } + + function defineGen(name: keyof Gen, stereo: boolean) { + const props = getAllProps(name) + const Gen = dspGens[name] + const kind_index = dspGensKeys.indexOf(name) + + function handle(opt: Record) { + const gen$ = context.gens++ + setupVm.CreateGen(kind_index) + DEBUG && console.log('CreateGen', name, gen$) + + for (let p in opt) { + const prop = `${name}.${p}` + const prop$ = props.indexOf(p) + DEBUG && console.log('Property', prop, opt[p]) + + if (prop$ >= 0) { + let value: number | undefined + outer: { + if (opt[p] instanceof ValueBase) { + const v: Value = opt[p] + + // Audio + if (v.kind === ValueBase.Kind.Audio) { + if (audioProps.has(p)) { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, v.value$) + } + else { + const scalar = Value.Scalar.create() + runVm.AudioToScalar(v.ptr, scalar.ptr) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, scalar.value$) + } + break outer + } + else { + if (audioProps.has(p)) { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, v.value$) + } + else { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, v.value$) + } + break outer + } + } + // Text + else if (typeof opt[p] === 'string') { + if (name === 'say' && p === 'text') { + const floats = Value.Floats.create() + const text = opt[p] + // loadSayText(floats, text) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Floats, floats.value$) + break outer + } + if (name === 'freesound' && p === 'id') { + const floats = Value.Floats.create() + const text = opt[p] + // loadFreesound(floats, text) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Floats, floats.value$) + break outer + } + value = 0 + } + // Literal + else if (typeof opt[p] === 'number') { + value = opt[p] + } + else { + throw new TypeError( + `Invalid type for property "${prop}": ${typeof opt[p]}`) + } + + if (typeof value !== 'number') { + throw new TypeError( + `Invalid value for property "${prop}": ${value}`) + } + + const literal = Value.Literal.create(value) + if (audioProps.has(p)) { + const audio = Value.Audio.create() + runVm.LiteralToAudio(literal.ptr, audio.ptr) + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, audio.value$) + } + else { + runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, literal.value$) + } + } + } + } + + return gen$ + } + + function processMono(opt: Record): Value | void { + const gen$ = handle(opt) + + if ('hasAudioOut' in Gen && Gen.hasAudioOut) { + const audio = Value.Audio.create() + runVm.ProcessAudio(gen$, audio.ptr) + return audio + } + else { + runVm.UpdateGen(gen$) + } + } + + function processStereo(opt: Record): [Value, Value] | void { + const gen$ = handle(opt) + + if ('hasStereoOut' in Gen && Gen.hasStereoOut) { + const out_0 = Value.Audio.create() + const out_1 = Value.Audio.create() + runVm.ProcessAudioStereo(gen$, out_0.ptr, out_1.ptr) + return [out_0, out_1] + } + else { + runVm.UpdateGen(gen$) + } + } + + return stereo + ? processStereo + : processMono + } + + const globals = {} as { + sr: Value.Scalar + t: Value.Scalar + rt: Value.Scalar + co: Value.Scalar + } + + const math = { + add: binaryOp(DspBinaryOp.Add, (a, b) => a + b), + mul: binaryOp(DspBinaryOp.Mul, (a, b) => a * b), + sub: binaryOp(DspBinaryOp.Sub, (a, b) => a - b), + div: binaryOp(DspBinaryOp.Div, (a, b) => a / b), + pow: binaryOp(DspBinaryOp.Pow, (a, b) => a ** b), + } + + const api = { + globals, + math: { + ...math, + '+': math.add, + '*': math.mul, + '-': math.sub, + '/': math.div, + '^': math.pow, + }, + pan(value: Value | number): void { + if (typeof value === 'number') { + value = Value.Literal.create(value) + } + runVm.Pan(value.value$) + }, + pick(values: T[], index: Value | number): Value { + const list$ = context.lists + + const length = values.length + context.lists += length + + let i = list$ + for (let v of values) { + if (typeof v === 'number') { + const literal = Value.Literal.create(v) + context.listsi[i++] = literal.value$ + } + else { + context.listsi[i++] = v.value$ + } + } + + if (typeof index === 'number') { + index = Value.Literal.create(index) + } + + const out = Value.Dynamic.create() + runVm.Pick(list$, length, index.value$, out.value$) + return out + }, + gen: fromEntries( + dspGensKeys.map(name => + [name, defineGen(name, false)] + ) + ), + gen_st: fromEntries( + dspGensKeys.map(name => + [name, defineGen(name, true)] + ) + ) + } + + function begin(scope: Record) { + runVm.Begin() + setupVm.Begin() + + context.gens = + context.audios = + context.literals = + context.floats = + context.scalars = + context.lists = + context.values = + 0 + + globals.sr = Value.Scalar.create() + globals.t = Value.Scalar.create() + globals.rt = Value.Scalar.create() + globals.co = Value.Scalar.create() + + const { sr, t, rt, co } = globals + scope.sr = new AstNode(AstNode.Type.Result, { value: sr }) + scope.t = new AstNode(AstNode.Type.Result, { value: t }) + scope.rt = new AstNode(AstNode.Type.Result, { value: rt }) + scope.co = new AstNode(AstNode.Type.Result, { value: co }) + } + + function buildTrack(tokensCopy: Token[]) { + const scope: Record = {} + const literals: AstNode[] = [] + + // replace number tokens with literal references ids + // and populate initial scope for those ids. + for (const t of tokensCopy) { + if (t.type === Token.Type.Number) { + const id = `lit_${literals.length}` + const literal = new AstNode(AstNode.Type.Literal, parseNumber(t.text), [t]) + literals.push(literal) + scope[id] = literal + t.type = Token.Type.Id + t.text = id + } + } + + begin(scope) + + const program = interpret(api, scope, tokensCopy) + + runVm.End() + + setupVm.CreateAudios(context.audios) + setupVm.CreateValues(context.values) + setupVm.End() + + let L = program.scope.vars['L'] + let R = program.scope.vars['R'] + let LR = program.scope.vars['LR'] + + const slice = program.scope.stack.slice(-2) + if (slice.length === 2) { + R ??= slice.pop()! + L ??= slice.pop()! + } + else if (slice.length === 1) { + LR ??= slice.pop()! + } + + const out = { + L: L?.value as Value.Audio | undefined, + R: R?.value as Value.Audio | undefined, + LR: LR?.value as Value.Audio | undefined, + } + + return { program, out } + } + + return buildTrack +} + +export function setupTracks( + view: MemoryView, + tracks$$: number[], + run_ops$$: number[], + setup_ops$$: number[], + literals$$: number[], + lists$$: number[], +) { + const tracks = tracks$$.map(ptr => + Track(view.memory.buffer, ptr) + ) + + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i] + track.run_ops$ = run_ops$$[i] + track.setup_ops$ = setup_ops$$[i] + track.literals$ = literals$$[i] + track.lists$ = lists$$[i] + + const run_ops = view.getI32(track.run_ops$, MAX_OPS) + const setup_ops = view.getI32(track.setup_ops$, MAX_OPS) + vms.set(track, { + runVm: createVm(run_ops), + setupVm: createVm(setup_ops), + }) + + contexts.set(track, getTrackContext(view, track)) + builds.set(track, TrackBuild(track)) + } + + return tracks +} + +export function getTokens(code: string) { + const tokens = Array.from(tokenize({ code: code.replaceAll('\n', '\r\n') })) + + // fix invisible tokens bounds to the + // last visible token for errors + const last = tokens.at(-1) + function fixToken(x: Token) { + if (!last) return x + x.line = last.line + x.col = last.col + x.right = last.right + x.bottom = last.bottom + x.index = last.index + x.length = -1 + return x + } + + const tokensCopy = [ + ...preTokens.map(fixToken), + ...tokens, + ...postTokens.map(fixToken), + ].filter(t => t.type !== Token.Type.Comment).map(shallowCopy) + + // create hash id from tokens. We compare this afterwards to determine + // if we should make a new sound or update the old one. + const hashId = '' + + [tokens.filter(t => t.type === Token.Type.Number).length].join('') + + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') + + return { tokens: tokensCopy, hashId } +} diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/dsp-node.ts index a3b1348..2daf237 100644 --- a/src/as/dsp/dsp-node.ts +++ b/src/as/dsp/dsp-node.ts @@ -1,19 +1,9 @@ -import { DEBUG, getAllProps, Value as ValueBase, type Value } from 'dsp' import { Sigui } from 'sigui' -import { fromEntries, getMemoryView, keys, rpc, shallowCopy, type MemoryView } from 'utils' -import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' -import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' -import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' -import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' +import { getMemoryView, rpc, type MemoryView } from 'utils' +import { builds, getTokens, setupTracks } from '~/src/as/dsp/dsp-build.ts' import { DspWorklet, type DspProcessorOptions } from '~/src/as/dsp/dsp-worklet.ts' import dspWorkletUrl from '~/src/as/dsp/dsp-worklet.ts?url' -import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' import { Clock, DspWorkletMode, Track } from '~/src/as/dsp/shared.ts' -import { AstNode, interpret } from '~/src/lang/interpreter.ts' -import { Token, tokenize } from '~/src/lang/tokenize.ts' -import { parseNumber } from '~/src/lang/util.ts' - -const dspGensKeys = keys(dspGens) export class DspNode extends AudioWorkletNode { constructor( @@ -55,40 +45,17 @@ export class DspNode extends AudioWorkletNode { const registeredContexts = new Set() -type GenApi = { - (opt: Record): Value | [Value, Value] | void -} - -type BinaryOp = { - (lhs: number, rhs: number): number - (lhs: number, rhs: Value): Value - (lhs: Value, rhs: number): Value - (lhs: Value, rhs: Value): Value -} - -export interface DspApi { - globals: Record - math: Record - gen: Record - gen_st: Record - pan(value: Value | number): void - pick(values: T[], index: Value | number): Value -} - -const commutative = new Set([DspBinaryOp.Add, DspBinaryOp.Mul]) -const audioProps = new Set(['in', 'sidechain']) -const textProps = new Set(['text', 'id']) export function createDspNode(ctx: AudioContext) { using $ = Sigui() const info = $({ + code: null as null | string, node: null as null | DspNode, dsp: null as null | Awaited>, view: null as null | MemoryView, clock: null as null | Clock, tracks: null as null | Track[], - code: null as null | string, currTrack: 0, nextTrack: 1, isPlaying: false, @@ -118,379 +85,13 @@ export function createDspNode(ctx: AudioContext) { createNode() } - function getTrackContext(track: Track) { - const { view } = $.of(info) - const literalsf = view.getF32(track.literals$, MAX_LITERALS) - const listsi = view.getI32(track.lists$, MAX_LISTS) - return { - gens: 0, - values: 0, - floats: 0, - scalars: 0, - audios: 0, - literals: 0, - literalsf, - lists: 0, - listsi, - } - } - - const vms: Map = new Map() - const contexts: Map> = new Map() - const builds: Map> = new Map() const hashes: Map = new Map() - function TrackBuild(track: Track) { - const { runVm, setupVm } = vms.get(track)! - const context = contexts.get(track)! - const Value = ValueBase.Factory({ context, vm: runVm }) - - function binaryOp( - BinOp: DspBinaryOp, - LiteralLiteral: (a: number, b: number) => number - ): BinaryOp { - return function binaryOp(lhs: Value | number, rhs: Value | number) { - if (typeof lhs === 'number' && typeof rhs === 'number') { - return LiteralLiteral(lhs, rhs) - } - - let out = Value.Dynamic.create() - - let l: Value - let r: Value - - if (commutative.has(BinOp)) { - if (typeof lhs === 'number') { - if (typeof rhs === 'number') throw 'unreachable' - - l = rhs - r = Value.Literal.create(lhs) - } - else if (typeof rhs === 'number') { - l = lhs - r = Value.Literal.create(rhs) - } - else { - l = lhs - r = rhs - } - } - else { - if (typeof lhs === 'number') { - if (typeof rhs === 'number') throw 'unreachable' - - l = Value.Literal.create(lhs) - r = rhs - } - else if (typeof rhs === 'number') { - l = lhs - r = Value.Literal.create(rhs) - } - else { - l = lhs - r = rhs - } - } - - if (!l) { - throw new Error('Missing left operand.') - } - - if (!r) { - throw new Error('Missing right operand.') - } - - runVm.BinaryOp( - BinOp, - l.value$, - r.value$, - out.value$ - ) - - return out - } as BinaryOp - } - - function defineGen(name: keyof Gen, stereo: boolean) { - const props = getAllProps(name) - const Gen = dspGens[name] - const kind_index = dspGensKeys.indexOf(name) - - function handle(opt: Record) { - const gen$ = context.gens++ - setupVm.CreateGen(kind_index) - DEBUG && console.log('CreateGen', name, gen$) - - for (let p in opt) { - const prop = `${name}.${p}` - const prop$ = props.indexOf(p) - DEBUG && console.log('Property', prop, opt[p]) - - if (prop$ >= 0) { - let value: number | undefined - outer: { - if (opt[p] instanceof ValueBase) { - const v: Value = opt[p] - - // Audio - if (v.kind === ValueBase.Kind.Audio) { - if (audioProps.has(p)) { - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, v.value$) - } - else { - const scalar = Value.Scalar.create() - runVm.AudioToScalar(v.ptr, scalar.ptr) - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, scalar.value$) - } - break outer - } - else { - if (audioProps.has(p)) { - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, v.value$) - } - else { - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, v.value$) - } - break outer - } - } - // Text - else if (typeof opt[p] === 'string') { - if (name === 'say' && p === 'text') { - const floats = Value.Floats.create() - const text = opt[p] - // loadSayText(floats, text) - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Floats, floats.value$) - break outer - } - if (name === 'freesound' && p === 'id') { - const floats = Value.Floats.create() - const text = opt[p] - // loadFreesound(floats, text) - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Floats, floats.value$) - break outer - } - value = 0 - } - // Literal - else if (typeof opt[p] === 'number') { - value = opt[p] - } - else { - throw new TypeError( - `Invalid type for property "${prop}": ${typeof opt[p]}`) - } - - if (typeof value !== 'number') { - throw new TypeError( - `Invalid value for property "${prop}": ${value}`) - } - - const literal = Value.Literal.create(value) - if (audioProps.has(p)) { - const audio = Value.Audio.create() - runVm.LiteralToAudio(literal.ptr, audio.ptr) - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Audio, audio.value$) - } - else { - runVm.SetProperty(gen$, prop$, ValueBase.Kind.Scalar, literal.value$) - } - } - } - } - - return gen$ - } - - function processMono(opt: Record): Value | void { - const gen$ = handle(opt) - - if ('hasAudioOut' in Gen && Gen.hasAudioOut) { - const audio = Value.Audio.create() - runVm.ProcessAudio(gen$, audio.ptr) - return audio - } - else { - runVm.UpdateGen(gen$) - } - } - - function processStereo(opt: Record): [Value, Value] | void { - const gen$ = handle(opt) - - if ('hasStereoOut' in Gen && Gen.hasStereoOut) { - const out_0 = Value.Audio.create() - const out_1 = Value.Audio.create() - runVm.ProcessAudioStereo(gen$, out_0.ptr, out_1.ptr) - return [out_0, out_1] - } - else { - runVm.UpdateGen(gen$) - } - } - - return stereo - ? processStereo - : processMono - } - - const globals = {} as { - sr: Value.Scalar - t: Value.Scalar - rt: Value.Scalar - co: Value.Scalar - } - - const math = { - add: binaryOp(DspBinaryOp.Add, (a, b) => a + b), - mul: binaryOp(DspBinaryOp.Mul, (a, b) => a * b), - sub: binaryOp(DspBinaryOp.Sub, (a, b) => a - b), - div: binaryOp(DspBinaryOp.Div, (a, b) => a / b), - pow: binaryOp(DspBinaryOp.Pow, (a, b) => a ** b), - } - - const api = { - globals, - math: { - ...math, - '+': math.add, - '*': math.mul, - '-': math.sub, - '/': math.div, - '^': math.pow, - }, - pan(value: Value | number): void { - if (typeof value === 'number') { - value = Value.Literal.create(value) - } - runVm.Pan(value.value$) - }, - pick(values: T[], index: Value | number): Value { - const list$ = context.lists - - const length = values.length - context.lists += length - - let i = list$ - for (let v of values) { - if (typeof v === 'number') { - const literal = Value.Literal.create(v) - context.listsi[i++] = literal.value$ - } - else { - context.listsi[i++] = v.value$ - } - } - - if (typeof index === 'number') { - index = Value.Literal.create(index) - } - - const out = Value.Dynamic.create() - runVm.Pick(list$, length, index.value$, out.value$) - return out - }, - gen: fromEntries( - dspGensKeys.map(name => - [name, defineGen(name, false)] - ) - ), - gen_st: fromEntries( - dspGensKeys.map(name => - [name, defineGen(name, true)] - ) - ) - } - - function begin(scope: Record) { - runVm.Begin() - setupVm.Begin() - - context.gens = - context.audios = - context.literals = - context.floats = - context.scalars = - context.lists = - context.values = - 0 - - globals.sr = Value.Scalar.create() - globals.t = Value.Scalar.create() - globals.rt = Value.Scalar.create() - globals.co = Value.Scalar.create() - - const { sr, t, rt, co } = globals - scope.sr = new AstNode(AstNode.Type.Result, { value: sr }) - scope.t = new AstNode(AstNode.Type.Result, { value: t }) - scope.rt = new AstNode(AstNode.Type.Result, { value: rt }) - scope.co = new AstNode(AstNode.Type.Result, { value: co }) - } - - function buildTrack(tokensCopy: Token[]) { - const scope: Record = {} - const literals: AstNode[] = [] - - // replace number tokens with literal references ids - // and populate initial scope for those ids. - for (const t of tokensCopy) { - if (t.type === Token.Type.Number) { - const id = `lit_${literals.length}` - const literal = new AstNode(AstNode.Type.Literal, parseNumber(t.text), [t]) - literals.push(literal) - scope[id] = literal - t.type = Token.Type.Id - t.text = id - } - } - - begin(scope) - - const program = interpret(api, scope, tokensCopy) - - runVm.End() - - setupVm.CreateAudios(context.audios) - setupVm.CreateValues(context.values) - setupVm.End() - - return program - } - - return buildTrack - } - function build(code: string) { const { tracks, currTrack, nextTrack } = $.of(info) let track = tracks[currTrack] - const tokens = Array.from(tokenize({ code: code.replaceAll('\n', '\r\n') })) - - // fix invisible tokens bounds to the - // last visible token for errors - const last = tokens.at(-1) - function fixToken(x: Token) { - if (!last) return x - x.line = last.line - x.col = last.col - x.right = last.right - x.bottom = last.bottom - x.index = last.index - x.length = -1 - return x - } - - const tokensCopy = [ - ...preTokens, //.map(fixToken), - ...tokens, - ...postTokens, //.map(fixToken), - ].filter(t => t.type !== Token.Type.Comment).map(shallowCopy) - - // create hash id from tokens. We compare this afterwards to determine - // if we should make a new sound or update the old one. - const hashId = '' - + [tokens.filter(t => t.type === Token.Type.Number).length].join('') - + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') + const { tokens, hashId } = getTokens(code.replaceAll('\n', '\r\n')) const prevHashId = hashes.get(track) const isNew = hashId !== prevHashId @@ -503,26 +104,7 @@ export function createDspNode(ctx: AudioContext) { const buildTrack = builds.get(track)! - const program = buildTrack(tokensCopy) - - let L = program.scope.vars['L'] - let R = program.scope.vars['R'] - let LR = program.scope.vars['LR'] - - const slice = program.scope.stack.slice(-2) - if (slice.length === 2) { - R ??= slice.pop()! - L ??= slice.pop()! - } - else if (slice.length === 1) { - LR ??= slice.pop()! - } - - const out = { - L: L?.value as Value.Audio | undefined, - R: R?.value as Value.Audio | undefined, - LR: LR?.value as Value.Audio | undefined, - } + const { out } = buildTrack(tokens) track.audio_LR$ = out.LR?.audio$ || out.LR?.ptr || 0 @@ -535,27 +117,14 @@ export function createDspNode(ctx: AudioContext) { $() const view = info.view = getMemoryView(dsp.memory) - const tracks = info.tracks = dsp.tracks$$.map(ptr => - Track(dsp.memory.buffer, ptr) + info.tracks = setupTracks( + view, + dsp.tracks$$, + dsp.run_ops$$, + dsp.setup_ops$$, + dsp.literals$$, + dsp.lists$$, ) - - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i] - track.run_ops$ = dsp.run_ops$$[i] - track.setup_ops$ = dsp.setup_ops$$[i] - track.literals$ = dsp.literals$$[i] - track.lists$ = dsp.lists$$[i] - - const run_ops = view.getI32(track.run_ops$, MAX_OPS) - const setup_ops = view.getI32(track.setup_ops$, MAX_OPS) - vms.set(track, { - runVm: createVm(run_ops), - setupVm: createVm(setup_ops), - }) - - contexts.set(track, getTrackContext(track)) - builds.set(track, TrackBuild(track)) - } }) // update player track diff --git a/src/as/dsp/dsp-wasm.ts b/src/as/dsp/dsp-wasm.ts new file mode 100644 index 0000000..1dc5022 --- /dev/null +++ b/src/as/dsp/dsp-wasm.ts @@ -0,0 +1,52 @@ +import { getMemoryView } from 'utils' +import { BUFFER_SIZE, MAX_AUDIOS, MAX_SCALARS, MAX_TRACKS, MAX_VALUES } from '~/as/assembly/dsp/constants.ts' +import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' +import { Out } from '~/src/as/dsp/shared.ts' + +export function createDspWasm(sampleRate: number, wasm: typeof WasmExports, memory: WebAssembly.Memory) { + const view = getMemoryView(memory) + + const core$ = wasm.createCore(sampleRate) + const engine$ = wasm.createEngine(sampleRate, core$) + const clock$ = wasm.getEngineClock(engine$) + + const sound$ = wasm.createSound(engine$) + + const out$ = wasm.createOut() + const out = Out(memory.buffer, out$) + const L$ = wasm.allocF32(BUFFER_SIZE) + const R$ = wasm.allocF32(BUFFER_SIZE) + out.L$ = L$ + out.R$ = R$ + const L = view.getF32(out.L$, BUFFER_SIZE) + const R = view.getF32(out.R$, BUFFER_SIZE) + + const player$ = wasm.createPlayer(sound$, out$) + const player_track$ = player$ + wasm.getPlayerTrackOffset() + const player_audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound$, index)) + const player_values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound$, index)) + const player_scalars = view.getF32(wasm.getSoundScalars(sound$), MAX_SCALARS) + const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) + const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) + const setup_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) + const literals$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLiterals()) + const lists$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLists()) + + return { + clock$, + sound$, + out$, + L, + R, + player$, + player_track$, + player_audios$$, + player_values$$, + player_scalars, + tracks$$, + run_ops$$, + setup_ops$$, + literals$$, + lists$$, + } +} diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts index ef320b2..ce4eef7 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/dsp-worklet.ts @@ -1,9 +1,9 @@ -import { getMemoryView, omit, rpc, toRing, wasmSourceMap } from 'utils' -import { BUFFER_SIZE, MAX_AUDIOS, MAX_SCALARS, MAX_TRACKS, MAX_VALUES } from '~/as/assembly/dsp/constants.ts' +import { omit, rpc, toRing, wasmSourceMap } from 'utils' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import hex from '~/as/build/dsp-nort.wasm?raw-hex' import dspConfig from '~/asconfig-dsp-nort.json' -import { Clock, DspWorkletMode, Out } from '~/src/as/dsp/shared.ts' +import { createDspWasm } from '~/src/as/dsp/dsp-wasm.ts' +import { Clock, DspWorkletMode } from '~/src/as/dsp/shared.ts' type AudioProcess = (inputs: Float32Array[], outputs: Float32Array[]) => void @@ -62,51 +62,12 @@ async function setup({ sourcemapUrl }: SetupOptions) { const wasm: typeof WasmExports = instance.exports as any - const view = getMemoryView(memory) - - const core$ = wasm.createCore(sampleRate) - const engine$ = wasm.createEngine(sampleRate, core$) - const clock$ = wasm.getEngineClock(engine$) - - const sound$ = wasm.createSound(engine$) - - const out$ = wasm.createOut() - const out = Out(memory.buffer, out$) - const L$ = wasm.allocF32(BUFFER_SIZE) - const R$ = wasm.allocF32(BUFFER_SIZE) - out.L$ = L$ - out.R$ = R$ - const L = view.getF32(out.L$, BUFFER_SIZE) - const R = view.getF32(out.R$, BUFFER_SIZE) - - const player$ = wasm.createPlayer(sound$, out$) - const player_track$ = player$ + wasm.getPlayerTrackOffset() - const player_audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound$, index)) - const player_values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound$, index)) - const player_scalars = view.getF32(wasm.getSoundScalars(sound$), MAX_SCALARS) - const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) - const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) - const setup_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) - const literals$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLiterals()) - const lists$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createLists()) - - // TODO: preallocate audios and return here their pointers + const dsp = createDspWasm(sampleRate, wasm, memory) + return { wasm, memory, - clock$, - L, - R, - player$, - player_track$, - player_audios$$, - player_values$$, - player_scalars, - tracks$$, - run_ops$$, - setup_ops$$, - literals$$, - lists$$, + ...dsp, } } diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts index 15db067..96e4219 100644 --- a/src/as/dsp/dsp.ts +++ b/src/as/dsp/dsp.ts @@ -12,7 +12,6 @@ import { Clock } from './shared.ts' import { getAllProps } from './util.ts' import { Value } from './value.ts' import { wasm } from './wasm.ts' -import type { DspApi } from '~/src/as/dsp/dsp-node.ts' import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' const DEBUG = false diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index 4f362f5..98dabb3 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -4,16 +4,18 @@ self.document = { baseURI: location.origin } -import { Dsp, Sound, Value, wasm } from 'dsp' -import { assign, getMemoryView, Lru, rpc } from 'utils' -import { BUFFER_SIZE, MAX_AUDIOS, MAX_SCALARS, MAX_VALUES } from '~/as/assembly/dsp/constants.ts' +import { Value, wasm } from 'dsp' +import { assign, getMemoryView, Lru, rpc, type MemoryView } from 'utils' +import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' +import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' +import { builds, getTokens, setupTracks } from '~/src/as/dsp/dsp-build.ts' +import { createDspWasm } from '~/src/as/dsp/dsp-wasm.ts' +import { Clock, Out, type Track } from '~/src/as/dsp/shared.ts' import { AstNode } from '~/src/lang/interpreter.ts' -import { Token, tokenize } from '~/src/lang/tokenize.ts' +import { Token } from '~/src/lang/tokenize.ts' export type PreviewWorker = typeof worker -const sounds = new Map() - const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) let epoch = 0 @@ -34,37 +36,59 @@ const rmsWidgets: WidgetInfo[] = [] const listWidgets: ListInfo[] = [] const worker = { - dsp: null as null | Dsp, + dsp: null as null | ReturnType, + view: null as null | MemoryView, + out$: null as null | number, + clock: null as null | Clock, + tracks: null as null | Track[], + track: null as null | Track, error: null as null | Error, async createDsp(sampleRate: number) { - this.dsp = Dsp({ sampleRate }) + const dsp = this.dsp = createDspWasm(sampleRate, wasm as unknown as typeof WasmExports, wasm.memory) + const view = this.view = getMemoryView(wasm.memory) + this.clock = Clock(wasm.memory.buffer, dsp.clock$) + this.tracks = setupTracks( + view, + dsp.tracks$$, + dsp.run_ops$$, + dsp.setup_ops$$, + dsp.literals$$, + dsp.lists$$, + ) + this.track = this.tracks[0] return { memory: wasm.memory, + L: dsp.L, + R: dsp.R, + sound$: dsp.sound$, + audios$$: dsp.player_audios$$, + values$$: dsp.player_values$$, + scalars: dsp.player_scalars, } }, - async createSound() { - const dsp = this.dsp - if (!dsp) throw new Error('Dsp not ready.') + // async createSound() { + // const dsp = this.dsp + // if (!dsp) throw new Error('Dsp not ready.') - const sound = dsp.Sound() - sounds.set(sound.sound$, sound) + // const sound = dsp.Sound() + // sounds.set(sound.sound$, sound) - const audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound.sound$, index)) - const values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound.sound$, index)) - const scalars = getMemoryView(wasm.memory).getF32(wasm.getSoundScalars(sound.sound$), MAX_SCALARS) + // const audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound.sound$, index)) + // const values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound.sound$, index)) + // const scalars = getMemoryView(wasm.memory).getF32(wasm.getSoundScalars(sound.sound$), MAX_SCALARS) - return { sound$: sound.sound$, audios$$, values$$, scalars } - }, - async build(sound$: number, code: string) { - const dsp = this.dsp - if (!dsp) throw new Error('Dsp not ready.') + // return { sound$: sound.sound$, audios$$, values$$, scalars } + // }, + async build(code: string) { + const { dsp, track } = this + if (!dsp || !track) throw new Error('Dsp not ready.') - const sound = sounds.get(sound$) - if (!sound) throw new Error('Sound not found, id: ' + sound$) + const { tokens } = getTokens(code.replaceAll('\n', '\r\n')) - const tokens = Array.from(tokenize({ code: code.replaceAll('\n', '\r\n') })) + const buildTrack = builds.get(track)! + + const { program, out } = buildTrack(tokens) - const { program, out } = sound.process(tokens) if (!out.LR) throw new Error('No audio in the stack.', { cause: { nodes: [] } }) program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => @@ -134,31 +158,25 @@ const worker = { const LR = out.LR.getAudio() - const length = BUFFER_SIZE - const key = `${sound$}:${length}:${epoch++}` - const floats = getFloats(key, length) - return { - ops$: sound.ops.ptr, LR, - floats, waves: waveWidgets as WidgetInfo[], rmss: rmsWidgets as WidgetInfo[], lists: listWidgets as ListInfo[], } }, - async renderSource(sound$: number, code: string) { - const dsp = this.dsp - if (!dsp) throw new Error('Dsp not ready.') + async renderSource(code: string) { + const { dsp, view, clock, track } = this + if (!dsp || !view || !clock || !track) throw new Error('Dsp not ready.') - const sound = sounds.get(sound$) - if (!sound) throw new Error('Sound not found, id: ' + sound$) + // const sound = sounds.get(sound$) + // if (!sound) throw new Error('Sound not found, id: ' + sound$) - const { clock } = dsp const info = this try { - sound.reset() + wasm.resetSound(dsp.sound$) + wasm.clearSound(dsp.sound$) clock.time = 0 clock.barTime = 0 @@ -166,17 +184,17 @@ const worker = { wasm.clockUpdate(clock.ptr) - const { LR, floats, waves, rmss, lists } = await this.build(sound$, code) - - wasm.fillSound( - sound.sound$, - sound.ops.ptr, - LR, + const { LR, waves, rmss, lists } = await this.build(code) + console.log(dsp.sound$, track.ptr, dsp.out$) + wasm.fillTrack( + dsp.sound$, + track.ptr, 0, - floats.length, - floats.ptr, + BUFFER_SIZE, + dsp.out$, ) + const floats = dsp.L return { LR, floats, waves, rmss, lists } } catch (e) { diff --git a/src/as/dsp/shared.ts b/src/as/dsp/shared.ts index f2604a1..8a813e8 100644 --- a/src/as/dsp/shared.ts +++ b/src/as/dsp/shared.ts @@ -9,7 +9,6 @@ export const enum DspWorkletMode { } export type Clock = typeof Clock.type - export const Clock = Struct({ time: 'f64', timeStep: 'f64', @@ -29,7 +28,6 @@ export const Clock = Struct({ }) export type Track = typeof Track.type - export const Track = Struct({ run_ops$: 'usize', setup_ops$: 'usize', @@ -38,13 +36,13 @@ export const Track = Struct({ audio_LR$: 'i32', }) +export type Out = typeof Out.type export const Out = Struct({ L$: 'usize', R$: 'usize', }) export type SoundValue = typeof SoundValue.type - export const SoundValue = Struct({ kind: 'i32', ptr: 'i32', diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index e91c8ab..c80a3e4 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -11,7 +11,7 @@ import { screen } from '~/src/screen.ts' import { state } from '~/src/state.ts' import { Button } from '~/src/ui/Button.tsx' import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlDecoWidget, RmsDecoWidget, ListMarkWidget } from '~/src/ui/editor/widgets/index.ts' +import { ListMarkWidget, RmsDecoWidget, WaveGlDecoWidget } from '~/src/ui/editor/widgets/index.ts' import { copyRingInto } from '~/src/util/copy-ring-into.ts' /* @@ -99,8 +99,8 @@ export function DspNodeDemo() { $.fx(() => { const { dsp, view } = $.of(dspNode.info) $() - info.audios = dsp?.player_audios$$.map(ptr => view.getF32(ptr, BUFFER_SIZE)) - info.values = dsp?.player_values$$.map(ptr => SoundValue(view.memory.buffer, ptr)) + info.audios = dsp.player_audios$$.map(ptr => view.getF32(ptr, BUFFER_SIZE)) + info.values = dsp.player_values$$.map(ptr => SoundValue(view.memory.buffer, ptr)) }) $.fx(() => { @@ -167,16 +167,16 @@ export function DspNodeDemo() { const listWidgets: ListMarkWidget[] = [] $.fx(() => { - const { isReady, view: previewView } = $.of(preview.info) - $().then(async () => { - const result = await preview.service.createSound() - $.batch(() => { - info.previewSound$ = result.sound$ - info.previewAudios = result.audios$$.map(ptr => previewView.getF32(ptr, BUFFER_SIZE)) - info.previewValues = result.values$$.map(ptr => SoundValue(previewView.memory.buffer, ptr)) - info.previewScalars = result.scalars - }) - }) + const { isReady, dsp, view: previewView } = $.of(preview.info) + $() //.then(async () => { + // const result = await preview.service.createSound() + // $.batch(() => { + info.previewSound$ = dsp.sound$ + info.previewAudios = dsp.audios$$.map(ptr => previewView.getF32(ptr, BUFFER_SIZE)) + info.previewValues = dsp.values$$.map(ptr => SoundValue(previewView.memory.buffer, ptr)) + info.previewScalars = dsp.scalars + // }) + // }) }) async function build() { @@ -206,7 +206,7 @@ export function DspNodeDemo() { let listCount = 0 try { - result = await preview.service.renderSource(previewSound$, code) + result = await preview.service.renderSource(code) const { isPlaying } = dspNode.info @@ -216,9 +216,9 @@ export function DspNodeDemo() { if (result.error) { throw new Error(result.error.message, { cause: result.error.cause }) } - if (!result?.floats) { - throw new Error('Could not render.') - } + // if (!result?.floats) { + // throw new Error('Could not render.') + // } $.batch(() => { info.error = null From f2317703079f6fa3f3daa093f7757f69cf1d08fe Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 22 Oct 2024 22:39:51 +0300 Subject: [PATCH 30/33] refactor dsp --- as/assembly/dsp/index.ts | 4 +++ as/assembly/dsp/vm/player.ts | 6 +--- as/assembly/dsp/vm/sound.ts | 8 +++++ generated/typescript/dsp-gens.ts | 2 +- src/as/dsp/dsp-build.ts | 6 ++-- src/as/dsp/dsp-node.ts | 1 - src/as/dsp/dsp-wasm.ts | 4 ++- src/as/dsp/dsp-worklet.ts | 4 +-- src/as/dsp/{dsp.ts => dsp.ts_depr} | 0 src/as/dsp/index.ts | 1 - src/as/dsp/preview-worker.ts | 14 ++++----- src/as/dsp/value.ts | 6 ++-- src/lang/interpreter.ts | 2 +- src/pages/App.tsx | 6 ---- ...{DspAsyncDemo.tsx => DspAsyncDemo.tsx-old} | 0 src/pages/DspNodeDemo.tsx | 31 +++++-------------- .../{DspSyncDemo.tsx => DspSyncDemo.tsx-old} | 0 src/pages/Home.tsx | 3 -- 18 files changed, 40 insertions(+), 58 deletions(-) rename src/as/dsp/{dsp.ts => dsp.ts_depr} (100%) rename src/pages/{DspAsyncDemo.tsx => DspAsyncDemo.tsx-old} (100%) rename src/pages/{DspSyncDemo.tsx => DspSyncDemo.tsx-old} (100%) diff --git a/as/assembly/dsp/index.ts b/as/assembly/dsp/index.ts index 5c792e1..d94f5f7 100644 --- a/as/assembly/dsp/index.ts +++ b/as/assembly/dsp/index.ts @@ -43,6 +43,10 @@ export function clearSound(sound$: usize): void { changetype(sound$).clear() } +export function soundSetupTrack(sound$: usize, track$: usize): void { + changetype(sound$).setupTrack(track$) +} + export function fillSound( sound$: usize, ops$: usize, diff --git a/as/assembly/dsp/vm/player.ts b/as/assembly/dsp/vm/player.ts index 0d87178..06ca32d 100644 --- a/as/assembly/dsp/vm/player.ts +++ b/as/assembly/dsp/vm/player.ts @@ -22,11 +22,7 @@ export class Player { // ideally we should compare gens and move // the reused gens to the new context sound.clear() - - const track = changetype(track$) - sound.literals = changetype>(track.literals$) - sound.lists = changetype>(track.lists$) - dspRun(this.sound$, track.setup_ops$) + sound.setupTrack(track$) } // console.log(`${track$}`) sound.fillTrack(track$, begin, end, this.out$) diff --git a/as/assembly/dsp/vm/sound.ts b/as/assembly/dsp/vm/sound.ts index 9b22800..da526df 100644 --- a/as/assembly/dsp/vm/sound.ts +++ b/as/assembly/dsp/vm/sound.ts @@ -68,6 +68,14 @@ export class Sound { // this.audios = [] } + @inline + setupTrack(track$: usize): void { + const track = changetype(track$) + this.literals = changetype>(track.literals$) + this.lists = changetype>(track.lists$) + dspRun(changetype(this), track.setup_ops$) + } + @inline updateScalars(c: Clock): void { this.scalars[Globals.t] = f32(c.barTime) diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 2382e60..93ef410 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Tue Oct 22 2024 17:00:50 GMT+0300 (Eastern European Summer Time) +// auto-generated Tue Oct 22 2024 22:11:36 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/src/as/dsp/dsp-build.ts b/src/as/dsp/dsp-build.ts index 0665028..ddbee24 100644 --- a/src/as/dsp/dsp-build.ts +++ b/src/as/dsp/dsp-build.ts @@ -1,12 +1,11 @@ import { DEBUG, getAllProps, Value as ValueBase, type Value } from 'dsp' -import { Sigui } from 'sigui' import { fromEntries, keys, shallowCopy, type MemoryView } from 'utils' import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' -import { Track, type Clock } from '~/src/as/dsp/shared.ts' +import { Track } from '~/src/as/dsp/shared.ts' import { AstNode, interpret } from '~/src/lang/interpreter.ts' import { Token, tokenize } from '~/src/lang/tokenize.ts' import { parseNumber } from '~/src/lang/util.ts' @@ -37,8 +36,9 @@ const commutative = new Set([DspBinaryOp.Add, DspBinaryOp.Mul]) const audioProps = new Set(['in', 'sidechain']) const textProps = new Set(['text', 'id']) +export type TrackContext = ReturnType + export function getTrackContext(view: MemoryView, track: Track) { - // const { view } = $.of(info) const literalsf = view.getF32(track.literals$, MAX_LITERALS) const listsi = view.getI32(track.lists$, MAX_LISTS) return { diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/dsp-node.ts index 2daf237..3e0b4c6 100644 --- a/src/as/dsp/dsp-node.ts +++ b/src/as/dsp/dsp-node.ts @@ -45,7 +45,6 @@ export class DspNode extends AudioWorkletNode { const registeredContexts = new Set() - export function createDspNode(ctx: AudioContext) { using $ = Sigui() diff --git a/src/as/dsp/dsp-wasm.ts b/src/as/dsp/dsp-wasm.ts index 1dc5022..396dbd2 100644 --- a/src/as/dsp/dsp-wasm.ts +++ b/src/as/dsp/dsp-wasm.ts @@ -3,7 +3,7 @@ import { BUFFER_SIZE, MAX_AUDIOS, MAX_SCALARS, MAX_TRACKS, MAX_VALUES } from '~/ import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import { Out } from '~/src/as/dsp/shared.ts' -export function createDspWasm(sampleRate: number, wasm: typeof WasmExports, memory: WebAssembly.Memory) { +export function createDsp(sampleRate: number, wasm: typeof WasmExports, memory: WebAssembly.Memory) { const view = getMemoryView(memory) const core$ = wasm.createCore(sampleRate) @@ -23,9 +23,11 @@ export function createDspWasm(sampleRate: number, wasm: typeof WasmExports, memo const player$ = wasm.createPlayer(sound$, out$) const player_track$ = player$ + wasm.getPlayerTrackOffset() + const player_audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound$, index)) const player_values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound$, index)) const player_scalars = view.getF32(wasm.getSoundScalars(sound$), MAX_SCALARS) + const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) const setup_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/dsp-worklet.ts index ce4eef7..64062ae 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/dsp-worklet.ts @@ -2,7 +2,7 @@ import { omit, rpc, toRing, wasmSourceMap } from 'utils' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import hex from '~/as/build/dsp-nort.wasm?raw-hex' import dspConfig from '~/asconfig-dsp-nort.json' -import { createDspWasm } from '~/src/as/dsp/dsp-wasm.ts' +import { createDsp } from '~/src/as/dsp/dsp-wasm.ts' import { Clock, DspWorkletMode } from '~/src/as/dsp/shared.ts' type AudioProcess = (inputs: Float32Array[], outputs: Float32Array[]) => void @@ -62,7 +62,7 @@ async function setup({ sourcemapUrl }: SetupOptions) { const wasm: typeof WasmExports = instance.exports as any - const dsp = createDspWasm(sampleRate, wasm, memory) + const dsp = createDsp(sampleRate, wasm, memory) return { wasm, diff --git a/src/as/dsp/dsp.ts b/src/as/dsp/dsp.ts_depr similarity index 100% rename from src/as/dsp/dsp.ts rename to src/as/dsp/dsp.ts_depr diff --git a/src/as/dsp/index.ts b/src/as/dsp/index.ts index f4648b3..f67e96f 100644 --- a/src/as/dsp/index.ts +++ b/src/as/dsp/index.ts @@ -1,5 +1,4 @@ export * from './constants.ts' -export * from './dsp.ts' export * from './util.ts' export * from './value.ts' export * from './wasm.ts' diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index 98dabb3..6123ddb 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -9,7 +9,7 @@ import { assign, getMemoryView, Lru, rpc, type MemoryView } from 'utils' import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import { builds, getTokens, setupTracks } from '~/src/as/dsp/dsp-build.ts' -import { createDspWasm } from '~/src/as/dsp/dsp-wasm.ts' +import { createDsp } from '~/src/as/dsp/dsp-wasm.ts' import { Clock, Out, type Track } from '~/src/as/dsp/shared.ts' import { AstNode } from '~/src/lang/interpreter.ts' import { Token } from '~/src/lang/tokenize.ts' @@ -36,7 +36,7 @@ const rmsWidgets: WidgetInfo[] = [] const listWidgets: ListInfo[] = [] const worker = { - dsp: null as null | ReturnType, + dsp: null as null | ReturnType, view: null as null | MemoryView, out$: null as null | number, clock: null as null | Clock, @@ -44,7 +44,7 @@ const worker = { track: null as null | Track, error: null as null | Error, async createDsp(sampleRate: number) { - const dsp = this.dsp = createDspWasm(sampleRate, wasm as unknown as typeof WasmExports, wasm.memory) + const dsp = this.dsp = createDsp(sampleRate, wasm as unknown as typeof WasmExports, wasm.memory) const view = this.view = getMemoryView(wasm.memory) this.clock = Clock(wasm.memory.buffer, dsp.clock$) this.tracks = setupTracks( @@ -88,7 +88,6 @@ const worker = { const buildTrack = builds.get(track)! const { program, out } = buildTrack(tokens) - if (!out.LR) throw new Error('No audio in the stack.', { cause: { nodes: [] } }) program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => @@ -169,9 +168,6 @@ const worker = { const { dsp, view, clock, track } = this if (!dsp || !view || !clock || !track) throw new Error('Dsp not ready.') - // const sound = sounds.get(sound$) - // if (!sound) throw new Error('Sound not found, id: ' + sound$) - const info = this try { @@ -185,7 +181,9 @@ const worker = { wasm.clockUpdate(clock.ptr) const { LR, waves, rmss, lists } = await this.build(code) - console.log(dsp.sound$, track.ptr, dsp.out$) + + wasm.soundSetupTrack(dsp.sound$, track.ptr) + wasm.fillTrack( dsp.sound$, track.ptr, diff --git a/src/as/dsp/value.ts b/src/as/dsp/value.ts index bbadea3..7bfa744 100644 --- a/src/as/dsp/value.ts +++ b/src/as/dsp/value.ts @@ -1,15 +1,15 @@ -import { Sound, SoundContext } from 'dsp' import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { DspVm } from '~/generated/typescript/dsp-vm.ts' +import type { TrackContext } from '~/src/as/dsp/dsp-build.ts' -type SoundPartial = { context: SoundContext, vm: DspVm } +type SoundPartial = { context: TrackContext, vm: DspVm } export class Value { value$: number ptr: number = 0 scalar$: number = 0 audio$: number = 0 - context: Sound['context'] + context: TrackContext constructor(sound: SoundPartial, kind: T extends Value.Kind.I32 | Value.Kind.Literal ? T : never, value: number) constructor(sound: SoundPartial, kind: T extends Value.Kind.Null | Value.Kind.Floats | Value.Kind.Scalar | Value.Kind.Audio | Value.Kind.Dynamic ? T : never) constructor(public sound: SoundPartial, public kind: Value.Kind, value?: number) { diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts index 0cfd0e9..f3a4216 100644 --- a/src/lang/interpreter.ts +++ b/src/lang/interpreter.ts @@ -1,5 +1,5 @@ import { dspGens } from '~/generated/typescript/dsp-gens.ts' -import type { DspApi } from '~/src/as/dsp/dsp-node.ts' +import type { DspApi } from '~/src/as/dsp/dsp-build.ts' import { getAllPropsReverse } from '~/src/as/dsp/util.ts' import type { Value } from '~/src/as/dsp/value.ts' import { Token } from '~/src/lang/tokenize.ts' diff --git a/src/pages/App.tsx b/src/pages/App.tsx index bfc335b..5ff5900 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -10,10 +10,7 @@ import { About } from '~/src/pages/About.tsx' import { AssemblyScript } from '~/src/pages/AssemblyScript.tsx' import { CanvasDemo } from '~/src/pages/CanvasDemo' import { Chat } from '~/src/pages/Chat/Chat.tsx' -import { DspAsyncDemo } from '~/src/pages/DspAsyncDemo.tsx' import { DspNodeDemo } from '~/src/pages/DspNodeDemo.tsx' -import { DspSyncDemo } from '~/src/pages/DspSyncDemo' -import { DspWorkerDemo } from '~/src/pages/DspWorkerDemo/DspWorkerDemo' import { EditorDemo } from '~/src/pages/EditorDemo.tsx' import { Home } from '~/src/pages/Home.tsx' import { OAuthRegister } from '~/src/pages/OAuthRegister.tsx' @@ -45,9 +42,6 @@ export function App() { '!/canvas': () => , '/webgl': () => , '/editor': () => , - '/dsp-sync': () => , - '/dsp-async': () => , - '/dsp-worker': () => , '/dsp-node': () => , '/worker-worklet': () => , '/asc': () => , diff --git a/src/pages/DspAsyncDemo.tsx b/src/pages/DspAsyncDemo.tsx-old similarity index 100% rename from src/pages/DspAsyncDemo.tsx rename to src/pages/DspAsyncDemo.tsx-old diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index c80a3e4..3b58c25 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -48,6 +48,9 @@ t 4* x= [sin 100.00 409 [exp 1.00 x] 31.88^ * + x] [exp 1.00 x] 6.26^ * [sno 83 t 4* y= [saw (35 38 42 40) 8 t* ? ntof] [exp 8 y] [lp 4.40] .5^ * .27 * [slp 265 5171 [exp 1 y] [lp 66.60] 1.35^ * + .9] +t 4* y= +[saw (35 38 42 40) 4 [sin 1 co* t 4/]* ? ntof] [exp .25 y 8 /] [lp 9.15] .5^ * .27 * [slp 616 9453 [exp .4 y 4/ ] [lp 88.91] 1.35^ * + .9] + */ const getFloatsGfx = Lru(1024, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) @@ -58,18 +61,8 @@ export function DspNodeDemo() { get width() { return screen.lg ? state.containerWidth / 2 : state.containerWidth }, get height() { return screen.lg ? state.containerHeight : state.containerHeight / 2 }, code: `t 4* y= -[saw (35 38 42 40) 8 t* ? ntof] [exp 8 y] [lp 4.40] .5^ * .27 * [slp 265 5171 [exp 1 y] [lp 66.60] 1.35^ * + .9] +[saw (35 38 42 40) 4 [sin 1 co* t 4/]* ? ntof] [exp .25 y 8 /] [lp 9.15] .5^ * .27 * [slp 616 9453 [exp .4 y 4/ ] [lp 88.91] 1.35^ * + .9] `, - //`(1 2 3) t 8 * ?`, - // `t 8* y= - // [saw (35 38 42 35) t 8* ? ntof] [slp 227 .8] 2* [tanh] [delay 16 .9] [slp 289 [exp 1 y trig=] 1^ 2290*+ .9] [exp .05 y 8 / trig=] 15.8^ * a= a .6* [delay 892 [sin 0.820] 38 * + .69] - // [shp 466] a [shp 473] @ [atan] [blp 5555 .8] .8* - // `, - // `t 4* x= [sin 100.00 409 [exp 1.00 x trig=] 31.88^ * + x trig=] [exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.088] [clip .40] - // [saw (92 353 50 218) t 12* ? [sin 1 x trig=] 9^ 61* + x trig=] [clip .4] .7* [slp 156 22k [exp 8 x [sin .24 x trig] .15* + trig=] 4.7^ * + .86] [exp 8 x trig=] .5^ * [sno 516 2181 [sin .2 co * t .5 - trig=] * + ] [delay 15 .73] .59* - // [noi 4.23] [adsr .03 100 .3 48 x 3* on= x 3* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 2 x trig=] * * [shp 7090 .7] .21* - // [noi 14.23] [adsr .03 10 .3 248 x 4* on= x 4* .012 - off=] 2 [sin .3] 1.0 + .9^ * ^ [sin 8 x trig=] * * [sbp 3790 .17 .60 [sin .5 co* t 2 / trig=]*+ ] .16* - // `, codeWorking: null as null | string, audios: [] as Float32Array[], values: [] as SoundValue[], @@ -112,8 +105,6 @@ export function DspNodeDemo() { let animFrame: any const tick = () => { for (const wave of [...waveWidgets, plot]) { - // values[info] - // console.log('resultValue', values[(wave as any).info.resultValue?.value$]) copyRingInto( wave.info.stabilizerTemp, audios[wave.info.index], @@ -168,15 +159,11 @@ export function DspNodeDemo() { $.fx(() => { const { isReady, dsp, view: previewView } = $.of(preview.info) - $() //.then(async () => { - // const result = await preview.service.createSound() - // $.batch(() => { + $() info.previewSound$ = dsp.sound$ info.previewAudios = dsp.audios$$.map(ptr => previewView.getF32(ptr, BUFFER_SIZE)) info.previewValues = dsp.values$$.map(ptr => SoundValue(previewView.memory.buffer, ptr)) info.previewScalars = dsp.scalars - // }) - // }) }) async function build() { @@ -216,9 +203,6 @@ export function DspNodeDemo() { if (result.error) { throw new Error(result.error.message, { cause: result.error.cause }) } - // if (!result?.floats) { - // throw new Error('Could not render.') - // } $.batch(() => { info.error = null @@ -228,8 +212,9 @@ export function DspNodeDemo() { const end = $.batch() plot.info.index = result.LR - plot.info.previewFloats.set(result.floats) - if (!isPlaying) plot.info.floats.set(result.floats) + const floats = previewAudios[plot.info.index] + plot.info.previewFloats.set(floats) + if (!isPlaying) plot.info.floats.set(floats) for (const waveInfo of result.waves) { const wave = (waveWidgets[waveCount] ??= WaveGlDecoWidget(pane.draw.shapes)) diff --git a/src/pages/DspSyncDemo.tsx b/src/pages/DspSyncDemo.tsx-old similarity index 100% rename from src/pages/DspSyncDemo.tsx rename to src/pages/DspSyncDemo.tsx-old diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7fb09a2..7a0c25d 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -28,9 +28,6 @@ export function Home() { Canvas WebGL Editor - Dsp Sync - Dsp Async - Dsp Worker Dsp Node Worker-Worklet AssemblyScript From 6efef92100218ba6a3dd092117bec966b9162ed9 Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 22 Oct 2024 23:11:32 +0300 Subject: [PATCH 31/33] cleanup --- generated/typescript/dsp-gens.ts | 2 +- src/as/dsp/{dsp-build.ts => build.ts} | 4 +- src/as/dsp/{dsp-wasm.ts => dsp.ts} | 12 +- src/as/dsp/dsp.ts_depr | 546 --------------------- src/as/dsp/index.ts | 5 + src/as/dsp/{dsp-node.ts => node.ts} | 6 +- src/as/dsp/preview-worker.ts | 38 +- src/as/dsp/value.ts | 2 +- src/as/dsp/{dsp-worklet.ts => worklet.ts} | 2 +- src/lang/index.ts | 4 + src/lang/interpreter.ts | 2 +- src/pages/App.tsx | 2 +- src/pages/DspAsyncDemo.tsx-old | 133 ----- src/pages/DspNodeDemo.tsx | 22 +- src/pages/DspSyncDemo.tsx-old | 144 ------ src/pages/DspWorkerDemo/DspWorkerDemo.tsx | 196 -------- src/pages/DspWorkerDemo/basic-processor.ts | 56 --- src/pages/DspWorkerDemo/constants.ts | 4 - src/pages/DspWorkerDemo/dsp-worker.ts | 90 ---- src/pages/DspWorkerDemo/free-queue.ts | 258 ---------- src/pages/Home.tsx | 2 +- src/ui/editor/widgets/list-mark.ts | 2 +- src/ui/editor/widgets/rms-deco.ts | 2 +- tsconfig.json | 6 + 24 files changed, 55 insertions(+), 1485 deletions(-) rename src/as/dsp/{dsp-build.ts => build.ts} (98%) rename src/as/dsp/{dsp-wasm.ts => dsp.ts} (81%) delete mode 100644 src/as/dsp/dsp.ts_depr rename src/as/dsp/{dsp-node.ts => node.ts} (96%) rename src/as/dsp/{dsp-worklet.ts => worklet.ts} (98%) create mode 100644 src/lang/index.ts delete mode 100644 src/pages/DspAsyncDemo.tsx-old delete mode 100644 src/pages/DspSyncDemo.tsx-old delete mode 100644 src/pages/DspWorkerDemo/DspWorkerDemo.tsx delete mode 100644 src/pages/DspWorkerDemo/basic-processor.ts delete mode 100644 src/pages/DspWorkerDemo/constants.ts delete mode 100644 src/pages/DspWorkerDemo/dsp-worker.ts delete mode 100644 src/pages/DspWorkerDemo/free-queue.ts diff --git a/generated/typescript/dsp-gens.ts b/generated/typescript/dsp-gens.ts index 93ef410..9145254 100644 --- a/generated/typescript/dsp-gens.ts +++ b/generated/typescript/dsp-gens.ts @@ -1,5 +1,5 @@ // -// auto-generated Tue Oct 22 2024 22:11:36 GMT+0300 (Eastern European Summer Time) +// auto-generated Tue Oct 22 2024 23:08:20 GMT+0300 (Eastern European Summer Time) import { Value } from '../../src/as/dsp/value.ts' diff --git a/src/as/dsp/dsp-build.ts b/src/as/dsp/build.ts similarity index 98% rename from src/as/dsp/dsp-build.ts rename to src/as/dsp/build.ts index ddbee24..a9f4cf1 100644 --- a/src/as/dsp/dsp-build.ts +++ b/src/as/dsp/build.ts @@ -1,11 +1,13 @@ -import { DEBUG, getAllProps, Value as ValueBase, type Value } from 'dsp' import { fromEntries, keys, shallowCopy, type MemoryView } from 'utils' import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { dspGens, type Gen } from '~/generated/typescript/dsp-gens.ts' import { createVm, type DspVm } from '~/generated/typescript/dsp-vm.ts' +import { DEBUG } from '~/src/as/dsp/constants.ts' import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' import { Track } from '~/src/as/dsp/shared.ts' +import { getAllProps } from '~/src/as/dsp/util.ts' +import { Value as ValueBase, type Value } from '~/src/as/dsp/value.ts' import { AstNode, interpret } from '~/src/lang/interpreter.ts' import { Token, tokenize } from '~/src/lang/tokenize.ts' import { parseNumber } from '~/src/lang/util.ts' diff --git a/src/as/dsp/dsp-wasm.ts b/src/as/dsp/dsp.ts similarity index 81% rename from src/as/dsp/dsp-wasm.ts rename to src/as/dsp/dsp.ts index 396dbd2..64d9638 100644 --- a/src/as/dsp/dsp-wasm.ts +++ b/src/as/dsp/dsp.ts @@ -24,9 +24,9 @@ export function createDsp(sampleRate: number, wasm: typeof WasmExports, memory: const player$ = wasm.createPlayer(sound$, out$) const player_track$ = player$ + wasm.getPlayerTrackOffset() - const player_audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound$, index)) - const player_values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound$, index)) - const player_scalars = view.getF32(wasm.getSoundScalars(sound$), MAX_SCALARS) + const audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound$, index)) + const values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound$, index)) + const scalars = view.getF32(wasm.getSoundScalars(sound$), MAX_SCALARS) const tracks$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createTrack()) const run_ops$$ = Array.from({ length: MAX_TRACKS }, () => wasm.createOps()) @@ -42,9 +42,9 @@ export function createDsp(sampleRate: number, wasm: typeof WasmExports, memory: R, player$, player_track$, - player_audios$$, - player_values$$, - player_scalars, + audios$$, + values$$, + scalars, tracks$$, run_ops$$, setup_ops$$, diff --git a/src/as/dsp/dsp.ts_depr b/src/as/dsp/dsp.ts_depr deleted file mode 100644 index 96e4219..0000000 --- a/src/as/dsp/dsp.ts_depr +++ /dev/null @@ -1,546 +0,0 @@ -import { Sigui } from 'sigui' -import { fromEntries, getMemoryView, keys, shallowCopy } from 'utils' -import { BUFFER_SIZE, MAX_LISTS, MAX_LITERALS, MAX_OPS } from '~/as/assembly/dsp/constants.ts' -import { DspBinaryOp } from '~/as/assembly/dsp/vm/dsp-shared.ts' -import { Op } from '~/generated/assembly/dsp-op.ts' -import { Gen, dspGens } from '~/generated/typescript/dsp-gens.ts' -import { createVm } from '~/generated/typescript/dsp-vm.ts' -import { AstNode, interpret } from '~/src/lang/interpreter.ts' -import { Token, tokenize } from '~/src/lang/tokenize.ts' -import { parseNumber } from '~/src/lang/util.ts' -import { Clock } from './shared.ts' -import { getAllProps } from './util.ts' -import { Value } from './value.ts' -import { wasm } from './wasm.ts' -import { postTokens, preTokens } from '~/src/as/dsp/pre-post.ts' - -const DEBUG = false - -const dspGensKeys = keys(dspGens) - -export type Dsp = ReturnType -export type Sound = ReturnType -export type SoundContext = ReturnType - -function getContext() { - return { - gens: 0, - values: 0, - floats: 0, - scalars: 0, - audios: 0, - literals: 0, - literalsf: null as unknown as Float32Array, - lists: 0, - listsi: null as unknown as Int32Array, - } -} - -export function Dsp({ sampleRate, core$ }: { - sampleRate: number - core$?: ReturnType -}) { - using $ = Sigui() - - core$ ??= wasm.createCore(sampleRate) - const pin = (x: T): T => { wasm.__pin(+x); return x } - const engine$ = wasm.createEngine(sampleRate, core$) - const clock$ = wasm.getEngineClock(engine$) - const clock = Clock(wasm.memory.buffer, clock$) - - const view = getMemoryView(wasm.memory) - - function Sound() { - const sound$ = pin(wasm.createSound(engine$)) - - const context = getContext() - - // TODO: ops/setup_ops need to be created new - // on every new code - // all of the context is prepared - // and then sent in one byte to the worklet - const ops = wasm.alloc(Int32Array, MAX_OPS) - const vm = createVm(ops) - const setup_ops = wasm.alloc(Int32Array, MAX_OPS) - const setup_vm = createVm(setup_ops) - - const lists$ = wasm.getSoundLists(sound$) - const lists = view.getI32(lists$, MAX_LISTS) - context.listsi = lists - - const literals$ = wasm.getSoundLiterals(sound$) - const literals = view.getF32(literals$, MAX_LITERALS) - const preset = createLiteralsPreset(literals) - preset.set() - - function reset() { - wasm.resetSound(sound$) - } - - function prepare() { - context.gens = - context.audios = - context.literals = - context.floats = - context.scalars = - context.lists = - context.values = - 0 - } - - function run() { - wasm.dspRun(sound$, ops.ptr) - } - - function commit() { - vm.End() - - if (DEBUG) { - let op$: Op - let i = 0 - while (op$ = ops[i++]) { - const op = Op[op$] - const len = (vm as any)[op].length - const slice = ops.subarray(i, i + len) - console.log(op, ...slice) - i += len - } - } - } - - function setup() { - setup_vm.CreateAudios(context.audios) - setup_vm.CreateValues(context.values) - setup_vm.End() - wasm.dspRun(sound$, setup_ops.ptr) - } - - function getAudio(index: number) { - const ptr = wasm.getSoundAudio(sound$, index) - const audio = view.getF32(ptr, BUFFER_SIZE) - return audio - } - - function createLiteralsPreset( - literals: Float32Array = wasm.alloc(Float32Array, MAX_LITERALS) - ) { - function set() { - context.literalsf = literals - wasm.setSoundLiterals(sound$, literals.byteOffset) - } - return { literals, set } - } - - function binaryOp( - BinOp: DspBinaryOp, - LiteralLiteral: (a: number, b: number) => number - ): BinaryOp { - return function binaryOp(lhs: Value | number, rhs: Value | number) { - if (typeof lhs === 'number' && typeof rhs === 'number') { - return LiteralLiteral(lhs, rhs) - } - - const { Value } = sound - - let out = Value.Dynamic.create() - - let l: Value - let r: Value - - if (commutative.has(BinOp)) { - if (typeof lhs === 'number') { - if (typeof rhs === 'number') throw 'unreachable' - - l = rhs - r = Value.Literal.create(lhs) - } - else if (typeof rhs === 'number') { - l = lhs - r = Value.Literal.create(rhs) - } - else { - l = lhs - r = rhs - } - } - else { - if (typeof lhs === 'number') { - if (typeof rhs === 'number') throw 'unreachable' - - l = Value.Literal.create(lhs) - r = rhs - } - else if (typeof rhs === 'number') { - l = lhs - r = Value.Literal.create(rhs) - } - else { - l = lhs - r = rhs - } - } - - if (!l) { - throw new Error('Missing left operand.') - } - - if (!r) { - throw new Error('Missing right operand.') - } - - sound.vm.BinaryOp( - BinOp, - l.value$, - r.value$, - out.value$ - ) - - return out - } as BinaryOp - } - - const soundPartial = { - context, - vm, - } - - const math = { - add: binaryOp(DspBinaryOp.Add, (a, b) => a + b), - mul: binaryOp(DspBinaryOp.Mul, (a, b) => a * b), - sub: binaryOp(DspBinaryOp.Sub, (a, b) => a - b), - div: binaryOp(DspBinaryOp.Div, (a, b) => a / b), - pow: binaryOp(DspBinaryOp.Pow, (a, b) => a ** b), - } - - function defineGen(name: keyof Gen, stereo: boolean) { - const props = getAllProps(name) - const Gen = dspGens[name] - const kind_index = dspGensKeys.indexOf(name) - - function handle(opt: Record) { - const gen$ = context.gens++ - setup_vm.CreateGen(kind_index) - DEBUG && console.log('CreateGen', name, gen$) - - for (let p in opt) { - const prop = `${name}.${p}` - const prop$ = props.indexOf(p) - DEBUG && console.log('Property', prop, opt[p]) - - if (prop$ >= 0) { - let value: number | undefined - outer: { - if (opt[p] instanceof Value) { - const v: Value = opt[p] - - // Audio - if (v.kind === Value.Kind.Audio) { - if (audioProps.has(p)) { - vm.SetProperty(gen$, prop$, Value.Kind.Audio, v.value$) - } - else { - const scalar = sound.Value.Scalar.create() - vm.AudioToScalar(v.ptr, scalar.ptr) - vm.SetProperty(gen$, prop$, Value.Kind.Scalar, scalar.value$) - } - break outer - } - else { - if (audioProps.has(p)) { - vm.SetProperty(gen$, prop$, Value.Kind.Audio, v.value$) - } - else { - vm.SetProperty(gen$, prop$, Value.Kind.Scalar, v.value$) - } - break outer - } - } - // Text - else if (typeof opt[p] === 'string') { - if (name === 'say' && p === 'text') { - const floats = sound.Value.Floats.create() - const text = opt[p] - // loadSayText(floats, text) - vm.SetProperty(gen$, prop$, Value.Kind.Floats, floats.value$) - break outer - } - if (name === 'freesound' && p === 'id') { - const floats = sound.Value.Floats.create() - const text = opt[p] - // loadFreesound(floats, text) - vm.SetProperty(gen$, prop$, Value.Kind.Floats, floats.value$) - break outer - } - value = 0 - } - // Literal - else if (typeof opt[p] === 'number') { - value = opt[p] - } - else { - throw new TypeError( - `Invalid type for property "${prop}": ${typeof opt[p]}`) - } - - if (typeof value !== 'number') { - throw new TypeError( - `Invalid value for property "${prop}": ${value}`) - } - - const literal = sound.Value.Literal.create(value) - if (audioProps.has(p)) { - const audio = sound.Value.Audio.create() - vm.LiteralToAudio(literal.ptr, audio.ptr) - vm.SetProperty(gen$, prop$, Value.Kind.Audio, audio.value$) - } - else { - vm.SetProperty(gen$, prop$, Value.Kind.Scalar, literal.value$) - } - } - } - } - - return gen$ - } - - function processMono(opt: Record): Value | void { - const gen$ = handle(opt) - - if ('hasAudioOut' in Gen && Gen.hasAudioOut) { - const audio = sound.Value.Audio.create() - vm.ProcessAudio(gen$, audio.ptr) - return audio - } - else { - vm.UpdateGen(gen$) - } - } - - function processStereo(opt: Record): [Value, Value] | void { - const gen$ = handle(opt) - - if ('hasStereoOut' in Gen && Gen.hasStereoOut) { - const out_0 = sound.Value.Audio.create() - const out_1 = sound.Value.Audio.create() - vm.ProcessAudioStereo(gen$, out_0.ptr, out_1.ptr) - return [out_0, out_1] - } - else { - vm.UpdateGen(gen$) - } - } - - return stereo - ? processStereo - : processMono - } - - const globals = {} as { - sr: Value.Scalar - t: Value.Scalar - rt: Value.Scalar - co: Value.Scalar - } - - const api = { - globals, - math: { - ...math, - '+': math.add, - '*': math.mul, - '-': math.sub, - '/': math.div, - '^': math.pow, - }, - pan(value: Value | number): void { - if (typeof value === 'number') { - value = sound.Value.Literal.create(value) - } - vm.Pan(value.value$) - }, - pick(values: T[], index: Value | number): Value { - const list$ = context.lists - - const length = values.length - context.lists += length - - let i = list$ - for (let v of values) { - if (typeof v === 'number') { - const literal = sound.Value.Literal.create(v) - context.listsi[i++] = literal.value$ - } - else { - context.listsi[i++] = v.value$ - } - } - - if (typeof index === 'number') { - index = sound.Value.Literal.create(index) - } - - const out = sound.Value.Dynamic.create() - vm.Pick(list$, length, index.value$, out.value$) - return out - }, - gen: fromEntries( - dspGensKeys.map(name => - [name, defineGen(name, false)] - ) - ), - gen_st: fromEntries( - dspGensKeys.map(name => - [name, defineGen(name, true)] - ) - ) - } as const - - let prevHashId: string - - function process(tokens: Token[]) { - const scope = {} as any - const literals: AstNode[] = [] - - // fix invisible tokens bounds to the - // last visible token for errors - const last = tokens.at(-1) - function fixToken(x: Token) { - if (!last) return x - x.line = last.line - x.col = last.col - x.right = last.right - x.bottom = last.bottom - x.index = last.index - x.length = -1 - return x - } - - let tokensCopy = [ - ...preTokens.map(fixToken), - ...tokens, - ...postTokens.map(fixToken), - ].filter(t => t.type !== Token.Type.Comment).map(shallowCopy) - - // create hash id from tokens. We compare this afterwards to determine - // if we should make a new sound or update the old one. - const hashId = '' - + [tokens.filter(t => t.type === Token.Type.Number).length].join('') - + tokens.filter(t => [Token.Type.Id, Token.Type.Op].includes(t.type)).map(t => t.text).join('') - - const isNew = hashId !== prevHashId - prevHashId = hashId - - // replace number tokens with literal references ids - // and populate initial scope for those ids. - for (const t of tokensCopy) { - if (t.type === Token.Type.Number) { - const id = `lit_${literals.length}` - const literal = new AstNode(AstNode.Type.Literal, parseNumber(t.text), [t]) - literals.push(literal) - scope[id] = literal - t.type = Token.Type.Id - t.text = id - } - } - - vm.Begin() - setup_vm.Begin() - prepare() - if (isNew) { - wasm.clearSound(sound$) - } - - globals.sr = sound.Value.Scalar.create() - globals.t = sound.Value.Scalar.create() - globals.rt = sound.Value.Scalar.create() - globals.co = sound.Value.Scalar.create() - - const { sr, t, rt, co } = globals - scope.sr = new AstNode(AstNode.Type.Result, { value: sr }) - scope.t = new AstNode(AstNode.Type.Result, { value: t }) - scope.rt = new AstNode(AstNode.Type.Result, { value: rt }) - scope.co = new AstNode(AstNode.Type.Result, { value: co }) - - // for (let i = 0; i < 6; i++) { - // for (const p of 'nftv') { - // const name = `n${i}${p}` - // const value = (globals as any)[name] = sound.Value.Scalar.create() - // scope[name] = new AstNode(AstNode.Type.Result, { value }) - // } - // } - - // for (let i = 0; i < 6; i++) { - // const name = `p${i}` - // const value = (globals as any)[name] = sound.Value.Scalar.create() - // scope[name] = new AstNode(AstNode.Type.Result, { value }) - // } - - const program = interpret(sound.api, scope, tokensCopy) - - let L = program.scope.vars['L'] - let R = program.scope.vars['R'] - let LR = program.scope.vars['LR'] - - const slice = program.scope.stack.slice(-2) - if (slice.length === 2) { - R ??= slice.pop()! - L ??= slice.pop()! - } - else if (slice.length === 1) { - LR ??= slice.pop()! - } - - const out = { - L: L?.value as Value.Audio | undefined, - R: R?.value as Value.Audio | undefined, - LR: LR?.value as Value.Audio | undefined, - } - - commit() - - if (isNew) { - setup() - } - - return { - isNew, - out, - program, - } - } - - const sound = { - api, - commit, - context, - createLiteralsPreset, - getAudio, - ops, - preset, - process, - reset, - run, - setup_ops, - setup_vm, - sound$, - Value: Value.Factory(soundPartial), - values: [] as Value[], - vm, - } - - return sound - } - - return { engine$, core$, clock, Sound } -} - -const commutative = new Set([DspBinaryOp.Add, DspBinaryOp.Mul]) -const audioProps = new Set(['in', 'sidechain']) -const textProps = new Set(['text', 'id']) - -export type BinaryOp = { - (lhs: number, rhs: number): number - (lhs: number, rhs: Value): Value - (lhs: Value, rhs: number): Value - (lhs: Value, rhs: Value): Value -} diff --git a/src/as/dsp/index.ts b/src/as/dsp/index.ts index f67e96f..647c676 100644 --- a/src/as/dsp/index.ts +++ b/src/as/dsp/index.ts @@ -1,4 +1,9 @@ +export * from '~/as/assembly/dsp/constants.ts' +export * from './build.ts' export * from './constants.ts' +export * from './node.ts' +export * from './preview-service.ts' +export * from './shared.ts' export * from './util.ts' export * from './value.ts' export * from './wasm.ts' diff --git a/src/as/dsp/dsp-node.ts b/src/as/dsp/node.ts similarity index 96% rename from src/as/dsp/dsp-node.ts rename to src/as/dsp/node.ts index 3e0b4c6..1c01d6d 100644 --- a/src/as/dsp/dsp-node.ts +++ b/src/as/dsp/node.ts @@ -1,9 +1,9 @@ import { Sigui } from 'sigui' import { getMemoryView, rpc, type MemoryView } from 'utils' -import { builds, getTokens, setupTracks } from '~/src/as/dsp/dsp-build.ts' -import { DspWorklet, type DspProcessorOptions } from '~/src/as/dsp/dsp-worklet.ts' -import dspWorkletUrl from '~/src/as/dsp/dsp-worklet.ts?url' +import { builds, getTokens, setupTracks } from '~/src/as/dsp/build.ts' import { Clock, DspWorkletMode, Track } from '~/src/as/dsp/shared.ts' +import { DspWorklet, type DspProcessorOptions } from '~/src/as/dsp/worklet.ts' +import dspWorkletUrl from '~/src/as/dsp/worklet.ts?url' export class DspNode extends AudioWorkletNode { constructor( diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index 6123ddb..7ab09e4 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -4,22 +4,19 @@ self.document = { baseURI: location.origin } -import { Value, wasm } from 'dsp' -import { assign, getMemoryView, Lru, rpc, type MemoryView } from 'utils' +import { assign, getMemoryView, rpc, type MemoryView } from 'utils' import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' -import { builds, getTokens, setupTracks } from '~/src/as/dsp/dsp-build.ts' -import { createDsp } from '~/src/as/dsp/dsp-wasm.ts' -import { Clock, Out, type Track } from '~/src/as/dsp/shared.ts' +import { builds, getTokens, setupTracks } from '~/src/as/dsp/build.ts' +import { createDsp } from '~/src/as/dsp/dsp.ts' +import { Clock, type Track } from '~/src/as/dsp/shared.ts' +import { Value } from '~/src/as/dsp/value.ts' +import { wasm } from '~/src/as/dsp/wasm.ts' import { AstNode } from '~/src/lang/interpreter.ts' import { Token } from '~/src/lang/tokenize.ts' export type PreviewWorker = typeof worker -const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) - -let epoch = 0 - interface WidgetInfo { value$: number bounds: Token.Bounds @@ -61,25 +58,12 @@ const worker = { L: dsp.L, R: dsp.R, sound$: dsp.sound$, - audios$$: dsp.player_audios$$, - values$$: dsp.player_values$$, - scalars: dsp.player_scalars, + audios$$: dsp.audios$$, + values$$: dsp.values$$, + scalars: dsp.scalars, } }, - // async createSound() { - // const dsp = this.dsp - // if (!dsp) throw new Error('Dsp not ready.') - - // const sound = dsp.Sound() - // sounds.set(sound.sound$, sound) - - // const audios$$ = Array.from({ length: MAX_AUDIOS }, (_, index) => wasm.getSoundAudio(sound.sound$, index)) - // const values$$ = Array.from({ length: MAX_VALUES }, (_, index) => wasm.getSoundValue(sound.sound$, index)) - // const scalars = getMemoryView(wasm.memory).getF32(wasm.getSoundScalars(sound.sound$), MAX_SCALARS) - - // return { sound$: sound.sound$, audios$$, values$$, scalars } - // }, - async build(code: string) { + build(code: string) { const { dsp, track } = this if (!dsp || !track) throw new Error('Dsp not ready.') @@ -180,7 +164,7 @@ const worker = { wasm.clockUpdate(clock.ptr) - const { LR, waves, rmss, lists } = await this.build(code) + const { LR, waves, rmss, lists } = this.build(code) wasm.soundSetupTrack(dsp.sound$, track.ptr) diff --git a/src/as/dsp/value.ts b/src/as/dsp/value.ts index 7bfa744..8601076 100644 --- a/src/as/dsp/value.ts +++ b/src/as/dsp/value.ts @@ -1,6 +1,6 @@ import { SoundValueKind } from '~/as/assembly/dsp/vm/dsp-shared.ts' import { DspVm } from '~/generated/typescript/dsp-vm.ts' -import type { TrackContext } from '~/src/as/dsp/dsp-build.ts' +import type { TrackContext } from '~/src/as/dsp/build.ts' type SoundPartial = { context: TrackContext, vm: DspVm } diff --git a/src/as/dsp/dsp-worklet.ts b/src/as/dsp/worklet.ts similarity index 98% rename from src/as/dsp/dsp-worklet.ts rename to src/as/dsp/worklet.ts index 64062ae..8451931 100644 --- a/src/as/dsp/dsp-worklet.ts +++ b/src/as/dsp/worklet.ts @@ -2,7 +2,7 @@ import { omit, rpc, toRing, wasmSourceMap } from 'utils' import type { __AdaptedExports as WasmExports } from '~/as/build/dsp-nort.d.ts' import hex from '~/as/build/dsp-nort.wasm?raw-hex' import dspConfig from '~/asconfig-dsp-nort.json' -import { createDsp } from '~/src/as/dsp/dsp-wasm.ts' +import { createDsp } from '~/src/as/dsp/dsp.ts' import { Clock, DspWorkletMode } from '~/src/as/dsp/shared.ts' type AudioProcess = (inputs: Float32Array[], outputs: Float32Array[]) => void diff --git a/src/lang/index.ts b/src/lang/index.ts new file mode 100644 index 0000000..cad4ffe --- /dev/null +++ b/src/lang/index.ts @@ -0,0 +1,4 @@ +export * from './interpreter.ts' +export * from './tokenize.ts' +export * from './util.ts' + diff --git a/src/lang/interpreter.ts b/src/lang/interpreter.ts index f3a4216..d7d8674 100644 --- a/src/lang/interpreter.ts +++ b/src/lang/interpreter.ts @@ -1,5 +1,5 @@ import { dspGens } from '~/generated/typescript/dsp-gens.ts' -import type { DspApi } from '~/src/as/dsp/dsp-build.ts' +import type { DspApi } from '~/src/as/dsp/build' import { getAllPropsReverse } from '~/src/as/dsp/util.ts' import type { Value } from '~/src/as/dsp/value.ts' import { Token } from '~/src/lang/tokenize.ts' diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 5ff5900..7de746b 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -42,7 +42,7 @@ export function App() { '!/canvas': () => , '/webgl': () => , '/editor': () => , - '/dsp-node': () => , + '/dsp': () => , '/worker-worklet': () => , '/asc': () => , '/qrcode': () => , diff --git a/src/pages/DspAsyncDemo.tsx-old b/src/pages/DspAsyncDemo.tsx-old deleted file mode 100644 index 9695f31..0000000 --- a/src/pages/DspAsyncDemo.tsx-old +++ /dev/null @@ -1,133 +0,0 @@ -import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' -import { Sigui } from 'sigui' -import { assign, Lru } from 'utils' -import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' -import { PreviewService } from '~/src/as/dsp/preview-service' -import { DspEditor } from '~/src/comp/DspEditor.tsx' -import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' -import { H2 } from '~/src/ui/Heading.tsx' - -const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) - -export function DspAsyncDemo() { - using $ = Sigui() - - const info = $({ - width: 400, - height: 300, - code: `[sin 42.11 303 -[exp 2.00] 6.66^ * +] -[exp 1.00] 9.99^ * -`, - floats: new Float32Array(), - sound$: null as null | number, - error: null as null | Error, - }) - - const ctx = new AudioContext({ sampleRate: 48000 }) - - const preview = PreviewService(ctx) - $.fx(() => preview.dispose) - - const length = BUFFER_SIZE - - const canvas = as HTMLCanvasElement - const gfx = Gfx({ canvas }) - const view = Rect(0, 0, 500, 500) - const matrix = Matrix() - const c = gfx.createContext(view, matrix) - const shapes = c.createShapes() - c.sketch.scene.add(shapes) - - const plot = WaveGlDecoWidget(shapes) - plot.widget.rect.w = 400 - plot.widget.rect.h = 300 - plot.info.floats = wasmGfx.alloc(Float32Array, length) - - const waveWidgets: WaveGlDecoWidget[] = [] - - $.fx(() => { - const { isReady } = $.of(preview.info) - $().then(async () => { - info.sound$ = await preview.service.createSound() - }) - }) - - queueMicrotask(() => { - $.fx(() => { - const { sound$ } = $.of(info) - const { pane } = dspEditor.editor.info - const { codeVisual } = pane.buffer.info - $().then(async () => { - let result: Awaited> - let nodeCount = 0 - - try { - result = await preview.service.renderSource(sound$, codeVisual) - - pane.draw.widgets.deco.clear() - plot.info.floats.fill(0) - - if (result.error) { - throw new Error(result.error.message, { cause: result.error.cause }) - } - if (!result?.floats) { - throw new Error('Could not render.') - } - - info.error = null - plot.info.floats.set(result.floats) - - for (const waveData of result.waves) { - const wave = (waveWidgets[nodeCount] ??= WaveGlDecoWidget(pane.draw.shapes)) - wave.info.floats = wave.info.floats.length - ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) - wave.info.floats.set(waveData.floats) - assign(wave.widget.bounds, waveData.bounds) - pane.draw.widgets.deco.add(wave.widget) - nodeCount++ - } - } - catch (err) { - if (err instanceof Error) { - info.error = err - } - else { - throw err - } - } - - let delta = waveWidgets.length - nodeCount - while (delta-- > 0) waveWidgets.pop()?.dispose() - - pane.draw.info.triggerUpdateTokenDrawInfo++ - - c.meshes.draw() - pane.view.anim.info.epoch++ - pane.draw.widgets.update() - }) - }) - }) - - const dspEditor = DspEditor({ - width: info.$.width, - height: info.$.height, - code: info.$.code, - }) - - $.fx(() => { - const { error } = $.of(info) - if (!error) return - console.error(error) - dspEditor.info.error = error - return () => dspEditor.info.error = null - }) - - return
-

Dsp Async demo

- {dspEditor} - {canvas} -
-} diff --git a/src/pages/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx index 3b58c25..6742f33 100644 --- a/src/pages/DspNodeDemo.tsx +++ b/src/pages/DspNodeDemo.tsx @@ -1,16 +1,12 @@ +import { BUFFER_SIZE, createDspNode, PreviewService, SoundValue } from 'dsp' import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' +import type { Token } from 'lang' import { Sigui } from 'sigui' +import { Button, Canvas } from 'ui' import { assign, Lru, throttle } from 'utils' -import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' -import { createDspNode } from '~/src/as/dsp/dsp-node.ts' -import { PreviewService } from '~/src/as/dsp/preview-service.ts' -import { SoundValue } from '~/src/as/dsp/shared.ts' import { DspEditor } from '~/src/comp/DspEditor.tsx' -import type { Token } from '~/src/lang/tokenize.ts' import { screen } from '~/src/screen.ts' import { state } from '~/src/state.ts' -import { Button } from '~/src/ui/Button.tsx' -import { Canvas } from '~/src/ui/Canvas.tsx' import { ListMarkWidget, RmsDecoWidget, WaveGlDecoWidget } from '~/src/ui/editor/widgets/index.ts' import { copyRingInto } from '~/src/util/copy-ring-into.ts' @@ -92,13 +88,13 @@ export function DspNodeDemo() { $.fx(() => { const { dsp, view } = $.of(dspNode.info) $() - info.audios = dsp.player_audios$$.map(ptr => view.getF32(ptr, BUFFER_SIZE)) - info.values = dsp.player_values$$.map(ptr => SoundValue(view.memory.buffer, ptr)) + info.audios = dsp.audios$$.map(ptr => view.getF32(ptr, BUFFER_SIZE)) + info.values = dsp.values$$.map(ptr => SoundValue(view.memory.buffer, ptr)) }) $.fx(() => { const { audios, values } = $.of(info) - const { isPlaying, clock, dsp: { player_scalars } } = $.of(dspNode.info) + const { isPlaying, clock, dsp: { scalars } } = $.of(dspNode.info) $() if (isPlaying) { const { pane } = dspEditor.editor.info @@ -118,11 +114,11 @@ export function DspNodeDemo() { } for (const rms of rmsWidgets) { - rms.update(audios, values, player_scalars) + rms.update(values, audios, scalars) } for (const list of listWidgets) { - list.update(audios, values, player_scalars) + list.update(values, audios, scalars) } pane.view.anim.info.epoch++ @@ -246,7 +242,7 @@ export function DspNodeDemo() { rms.info.index = previewValues[rmsInfo.value$].ptr rms.info.value$ = rmsInfo.value$ - if (!isPlaying) rms.update(previewAudios, previewValues, previewScalars) + if (!isPlaying) rms.update(previewValues, previewAudios, previewScalars) assign(rms.widget.bounds, fixBounds(rmsInfo.bounds)) pane.draw.widgets.deco.add(rms.widget) diff --git a/src/pages/DspSyncDemo.tsx-old b/src/pages/DspSyncDemo.tsx-old deleted file mode 100644 index 02288b8..0000000 --- a/src/pages/DspSyncDemo.tsx-old +++ /dev/null @@ -1,144 +0,0 @@ -import { Dsp, wasm as wasmDsp } from 'dsp' -import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' -import { Sigui } from 'sigui' -import { assign, Lru } from 'utils' -import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' -import type { Value } from '~/src/as/dsp/value.ts' -import { DspEditor } from '~/src/comp/DspEditor.tsx' -import type { AstNode } from '~/src/lang/interpreter.ts' -import { tokenize } from '~/src/lang/tokenize.ts' -import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' -import { H2 } from '~/src/ui/Heading.tsx' - -const getFloats = Lru(20, (key: string, length: number) => wasmDsp.alloc(Float32Array, length), item => item.fill(0), item => item.free()) -const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) -const getBuffer = Lru(20, (length: number) => wasmDsp.alloc(Float32Array, length), item => item.fill(0), item => item.free()) -const getPointers = Lru(20, (length: number) => wasmDsp.alloc(Uint32Array, length), item => item.fill(0), item => item.free()) - -export function DspSyncDemo() { - using $ = Sigui() - - const info = $({ - width: 400, - height: 300, - code: `[sin 42.11 303 -[exp 2.01] 6.66^ * +] -[exp 1.00] 9.99^ * -`, - floats: new Float32Array(), - error: null as Error | null, - }) - - const ctx = new AudioContext({ sampleRate: 48000 }) - const dsp = Dsp({ sampleRate: ctx.sampleRate }) - const sound = dsp.Sound() - - const length = BUFFER_SIZE - - const canvas = as HTMLCanvasElement - const gfx = Gfx({ canvas }) - const view = Rect(0, 0, 500, 500) - const matrix = Matrix() - const c = gfx.createContext(view, matrix) - const shapes = c.createShapes() - c.sketch.scene.add(shapes) - - const plot = WaveGlDecoWidget(shapes) - plot.widget.rect.w = 400 - plot.widget.rect.h = 300 - plot.info.floats = wasmGfx.alloc(Float32Array, length) - - const waveWidgets: WaveGlDecoWidget[] = [] - - queueMicrotask(() => { - $.fx(() => { - const { pane } = dspEditor.editor.info - const { codeVisual } = pane.buffer.info - $() - pane.draw.widgets.deco.clear() - plot.info.floats.fill(0) - let nodeCount = 0 - - try { - const tokens = Array.from(tokenize({ code: codeVisual })) - - sound.reset() - const { program, out } = sound.process(tokens) - if (!out.LR) throw new Error('No audio in the stack!') - - const floats = getFloats('floats', length) - info.floats = floats - - wasmDsp.fillSound( - sound.sound$, - sound.ops.ptr, - out.LR.getAudio(), - 0, - floats.length, - floats.ptr, - ) - - plot.info.floats.set(floats) - - program.value.results.sort(({ result: { bounds: a } }, { result: { bounds: b } }) => - a.line === b.line - ? a.col - b.col - : a.line - b.line - ) - - let last: AstNode | null = null - const waves = new Map() - - for (const node of program.value.results) { - if ('genId' in node) { - const bounds = node.result.bounds - if (last && last.bounds.line === bounds.line && last.bounds.right > bounds.col) { - last.bounds.right = bounds.col - 1 - waves.get(last)!.widget.bounds.right = bounds.col - 1 - } - const wave = (waveWidgets[nodeCount] ??= WaveGlDecoWidget(pane.draw.shapes)) - wave.info.floats = wave.info.floats.length - ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) - wave.info.floats.set(sound.getAudio((node.result.value as Value.Audio).ptr)) - assign(wave.widget.bounds, bounds) - pane.draw.widgets.deco.add(wave.widget) - waves.set(node.result, wave) - last = node.result - nodeCount++ - } - } - } - catch (err) { - if (err instanceof Error) { - info.error = err - } - else { - throw err - } - } - - let delta = waveWidgets.length - nodeCount - while (delta-- > 0) waveWidgets.pop()?.dispose() - - pane.draw.info.triggerUpdateTokenDrawInfo++ - - c.meshes.draw() - pane.view.anim.info.epoch++ - pane.draw.widgets.update() - }) - }) - - const dspEditor = DspEditor({ - width: info.$.width, - height: info.$.height, - code: info.$.code, - }) - - return
-

Dsp Sync demo

- {dspEditor} - {canvas} -
-} diff --git a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx b/src/pages/DspWorkerDemo/DspWorkerDemo.tsx deleted file mode 100644 index 9e339c1..0000000 --- a/src/pages/DspWorkerDemo/DspWorkerDemo.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { Gfx, Matrix, Rect, wasm as wasmGfx } from 'gfx' -import { Sigui } from 'sigui' -import { assign, Lru, rpc } from 'utils' -import { BUFFER_SIZE } from '~/as/assembly/dsp/constants.ts' -import { PreviewService } from '~/src/as/dsp/preview-service.ts' -import { DspEditor } from '~/src/comp/DspEditor.tsx' -import basicProcessorUrl from '~/src/pages/DspWorkerDemo/basic-processor.ts?url' -import { QUEUE_SIZE } from '~/src/pages/DspWorkerDemo/constants' -import { DspWorker } from '~/src/pages/DspWorkerDemo/dsp-worker' -import DspWorkerFactory from '~/src/pages/DspWorkerDemo/dsp-worker.ts?worker' -import { FreeQueue } from '~/src/pages/DspWorkerDemo/free-queue' -import { Canvas } from '~/src/ui/Canvas.tsx' -import { WaveGlDecoWidget } from '~/src/ui/editor/widgets/wave-gl-deco' -import { H3 } from '~/src/ui/Heading.tsx' - -const getFloatsGfx = Lru(20, (key: string, length: number) => wasmGfx.alloc(Float32Array, length), item => item.fill(0), item => item.free()) - -export function DspWorkerDemo() { - using $ = Sigui() - - const info = $({ - width: 400, - height: 300, - code: `t 8* x= -[sin 67.51 346 -[exp 1.00 x trig=] 18.95^ * + x trig=] -[exp 1.00 x trig=] 6.26^ * [sno 83 .9] [dclipexp 1.535] [clip .44] - -[saw 42 x trig=] [clip .4] .52* [slp 100 2336 [exp 8 x trig=] .2^ * + .92] [exp 8 x trig=] .2^ * [lp 800] -`, - floats: new Float32Array(), - sound$: null as null | number, - error: null as null | Error, - codeWorking: '', - }) - const inputQueue = new FreeQueue(QUEUE_SIZE, 1) - const outputQueue = new FreeQueue(QUEUE_SIZE, 1) - // Create an atomic state for synchronization between worker and AudioWorklet. - const atomicState = new Int32Array(new SharedArrayBuffer(1 * Int32Array.BYTES_PER_ELEMENT)) - const cmd = new Uint8Array(new SharedArrayBuffer(1 * Uint8Array.BYTES_PER_ELEMENT)) - const state = new Uint8Array(new SharedArrayBuffer(16384 * Uint8Array.BYTES_PER_ELEMENT)) - - const dspWorker = new DspWorkerFactory() - $.fx(() => () => dspWorker.terminate()) - - const dspService = rpc(dspWorker, { - isReady() { - dspService.setup({ - sampleRate: audioContext.sampleRate, - inputQueue, - outputQueue, - atomicState, - cmd, - state, - }) - } - }) - - - const audioContext = new AudioContext({ latencyHint: 0.000001 }) - $.fx(() => () => audioContext.close()) - - const encoder = new TextEncoder() - - $.fx(() => { - const { codeWorking } = info - $() - const encoded = encoder.encode(codeWorking) - state.set(encoded) - cmd[0] = encoded.length - }) - - $.fx(() => { - $().then(async () => { - await audioContext.audioWorklet.addModule(basicProcessorUrl) - - const processorNode = new AudioWorkletNode(audioContext, 'basic-processor', { - processorOptions: { - inputQueue, - outputQueue, - atomicState, - } - }) - - const osc = new OscillatorNode(audioContext) - osc.connect(processorNode).connect(audioContext.destination) - }) - }) - - const preview = PreviewService(audioContext) - $.fx(() => preview.dispose) - - const length = BUFFER_SIZE - - const canvas = as HTMLCanvasElement - const gfx = Gfx({ canvas }) - const view = Rect(0, 0, 500, 500) - const matrix = Matrix() - const c = gfx.createContext(view, matrix) - const shapes = c.createShapes() - c.sketch.scene.add(shapes) - - const plot = WaveGlDecoWidget(shapes) - plot.widget.rect.w = 400 - plot.widget.rect.h = 300 - plot.info.floats = wasmGfx.alloc(Float32Array, length) - - const waveWidgets: WaveGlDecoWidget[] = [] - - $.fx(() => { - const { isReady } = $.of(preview.info) - $().then(async () => { - info.sound$ = await preview.service.createSound() - }) - }) - - queueMicrotask(() => { - $.fx(() => { - const { sound$ } = $.of(info) - const { pane } = dspEditor.editor.info - const { codeVisual } = pane.buffer.info - queueMicrotask(async () => { - const { codeVisual } = pane.buffer.info - - let result: Awaited> - let nodeCount = 0 - - try { - result = await preview.service.renderSource(sound$, codeVisual) - - pane.draw.widgets.deco.clear() - plot.info.floats.fill(0) - - if (result.error) { - throw new Error(result.error.message, { cause: result.error.cause }) - } - if (!result?.floats) { - throw new Error('Could not render.') - } - - info.error = null - info.codeWorking = codeVisual - plot.info.floats.set(result.floats) - - for (const waveData of result.waves) { - const wave = (waveWidgets[nodeCount] ??= WaveGlDecoWidget(pane.draw.shapes)) - wave.info.floats = wave.info.floats.length - ? wave.info.floats - : getFloatsGfx(`${nodeCount}`, BUFFER_SIZE) - wave.info.floats.set(waveData.floats) - assign(wave.widget.bounds, waveData.bounds) - pane.draw.widgets.deco.add(wave.widget) - nodeCount++ - } - } - catch (err) { - if (err instanceof Error) { - info.error = err - } - else { - throw err - } - } - - let delta = waveWidgets.length - nodeCount - while (delta-- > 0) waveWidgets.pop()?.dispose() - - pane.draw.info.triggerUpdateTokenDrawInfo++ - - c.meshes.draw() - pane.view.anim.info.epoch++ - pane.draw.widgets.update() - }) - }) - }) - - const dspEditor = DspEditor({ - width: info.$.width, - height: info.$.height, - code: info.$.code, - }) - - $.fx(() => { - const { error } = $.of(info) - if (!error) return - console.warn(error) - dspEditor.info.error = error - return () => dspEditor.info.error = null - }) - - return
-

Dsp Node demo

- {dspEditor} - {canvas} -
-} diff --git a/src/pages/DspWorkerDemo/basic-processor.ts b/src/pages/DspWorkerDemo/basic-processor.ts deleted file mode 100644 index 3b9e5a4..0000000 --- a/src/pages/DspWorkerDemo/basic-processor.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { FRAME_SIZE, RENDER_QUANTUM } from '~/src/pages/WorkerWorklet/constants.ts' -import { FreeQueue } from '~/src/pages/WorkerWorklet/free-queue.ts' - -/** - * A simple AudioWorkletProcessor node. - * - * @class BasicProcessor - * @extends AudioWorkletProcessor - */ -class BasicProcessor extends AudioWorkletProcessor { - inputQueue: FreeQueue - outputQueue: FreeQueue - atomicState: Int32Array - - /** - * Constructor to initialize, input and output FreeQueue instances - * and atomicState to synchronise Worker with AudioWorklet - * @param {Object} options AudioWorkletProcessor options - * to initialize inputQueue, outputQueue and atomicState - */ - constructor(options: AudioWorkletNodeOptions) { - super() - - this.inputQueue = options.processorOptions.inputQueue - this.outputQueue = options.processorOptions.outputQueue - this.atomicState = options.processorOptions.atomicState - Object.setPrototypeOf(this.inputQueue, FreeQueue.prototype) - Object.setPrototypeOf(this.outputQueue, FreeQueue.prototype) - } - - process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean { - // const input = inputs[0] - const output = outputs[0] - - // Push data from input into inputQueue. - // this.inputQueue.push(input, RENDER_QUANTUM) - - // Try to pull data out of outputQueue and store it in output. - // const didPull = - this.outputQueue.pull(output, RENDER_QUANTUM) - // if (!didPull) { - // // console.log("failed to pull.") - // } - - // Wake up worker to process a frame of data. - // if (this.inputQueue.isFrameAvailable(FRAME_SIZE)) { - if (this.outputQueue.getAvailableSamples() < FRAME_SIZE) { - Atomics.notify(this.atomicState, 0, 1) - } - // } - - return true - } -} - -registerProcessor('basic-processor', BasicProcessor) diff --git a/src/pages/DspWorkerDemo/constants.ts b/src/pages/DspWorkerDemo/constants.ts deleted file mode 100644 index c7b4e1c..0000000 --- a/src/pages/DspWorkerDemo/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const KERNEL_LENGTH = 40 -export const RENDER_QUANTUM = 128 -export const FRAME_SIZE = KERNEL_LENGTH * RENDER_QUANTUM -export const QUEUE_SIZE = 16384 diff --git a/src/pages/DspWorkerDemo/dsp-worker.ts b/src/pages/DspWorkerDemo/dsp-worker.ts deleted file mode 100644 index a42b703..0000000 --- a/src/pages/DspWorkerDemo/dsp-worker.ts +++ /dev/null @@ -1,90 +0,0 @@ -// @ts-ignore -self.document = { - querySelectorAll() { return [] as any }, - baseURI: location.origin -} - -import { Dsp, wasm } from 'dsp' -import { Lru, rpc } from 'utils' -import { tokenize } from '~/src/lang/tokenize.ts' -import { FRAME_SIZE } from '~/src/pages/DspWorkerDemo/constants' -import { FreeQueue } from '~/src/pages/DspWorkerDemo/free-queue' - -export type DspWorker = typeof worker - -const getFloats = Lru(10, (_key: string, length: number) => wasm.alloc(Float32Array, length), item => item.fill(0), item => item.free()) - -let epoch = 0 - -const worker = { - async setup({ sampleRate, inputQueue, outputQueue, atomicState, cmd, state }: { - sampleRate: number - inputQueue: FreeQueue - outputQueue: FreeQueue - atomicState: Int32Array - cmd: Uint8Array - state: Uint8Array - }) { - console.log('[dsp-worker] setup') - Object.setPrototypeOf(inputQueue, FreeQueue.prototype) - Object.setPrototypeOf(outputQueue, FreeQueue.prototype) - - const dsp = Dsp({ sampleRate }) - const sounds = Array.from({ length: 4 }, () => dsp.Sound()) - - let current = 0 - let sound = sounds[current++] - - // buffer for storing data pulled out from queue. - const input = getFloats(`${FRAME_SIZE}:${epoch++}`, FRAME_SIZE) - const output = getFloats(`${FRAME_SIZE}:${epoch++}`, FRAME_SIZE) - const decoder = new TextDecoder() - - let LR: number = -1 - - // loop for processing data. - while (Atomics.wait(atomicState, 0, 0) === 'ok') { - - // pull data out from inputQueue. - // const didPull = inputQueue.pull([input], FRAME_SIZE) - - // If pulling data out was successfull, process it and push it to - // outputQueue - if (outputQueue.getAvailableSamples() < FRAME_SIZE) { - if (LR >= 0) { - wasm.fillSound( - sound.sound$, - sound.ops.ptr, - LR, - 0, - output.length, - output.ptr, - ) - } - - outputQueue.push([output], FRAME_SIZE) - } - - if (cmd[0]) { - const code = decoder.decode(state.slice(0, cmd[0])) - const tokens = Array.from(tokenize({ code })) - // sound = sounds[current++ % sounds.length] - const { out } = sound.process(tokens) - if (out.LR) { - LR = out.LR.getAudio() - } - cmd[0] = 0 - } - - if (outputQueue.getAvailableSamples() >= FRAME_SIZE) { - Atomics.store(atomicState, 0, 0) - } - - // outputQueue.printAvailableReadAndWrite() - } - }, -} - -const host = rpc<{ isReady(): void }>(self as any, worker) -host.isReady() -console.debug('[dsp-worker] started') diff --git a/src/pages/DspWorkerDemo/free-queue.ts b/src/pages/DspWorkerDemo/free-queue.ts deleted file mode 100644 index 006c5b0..0000000 --- a/src/pages/DspWorkerDemo/free-queue.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * A shared storage for FreeQueue operation backed by SharedArrayBuffer. - * - * @typedef SharedRingBuffer - * @property {Uint32Array} states Backed by SharedArrayBuffer. - * @property {number} bufferLength The frame buffer length. Should be identical - * throughout channels. - * @property {Array} channelData The length must be > 0. - * @property {number} channelCount same with channelData.length - */ - -interface SharedRingBuffer { - states: Uint32Array - bufferLength: number - channelData: Float32Array[] - channelCount: number -} - -/** - * A single-producer/single-consumer lock-free FIFO backed by SharedArrayBuffer. - * In a typical pattern is that a worklet pulls the data from the queue and a - * worker renders audio data to fill in the queue. - */ - -export class FreeQueue { - states: Uint32Array - bufferLength: number - channelCount: number - channelData: Float32Array[] - - /** - * An index set for shared state fields. Requires atomic access. - * @enum {number} - */ - States = { - /** @type {number} A shared index for reading from the queue. (consumer) */ - READ: 0, - /** @type {number} A shared index for writing into the queue. (producer) */ - WRITE: 1, - }; - - /** - * FreeQueue constructor. A shared buffer created by this constuctor - * will be shared between two threads. - * - * @param {number} size Frame buffer length. - * @param {number} channelCount Total channel count. - */ - constructor(size: number, channelCount: number = 1) { - this.states = new Uint32Array( - new SharedArrayBuffer( - Object.keys(this.States).length * Uint32Array.BYTES_PER_ELEMENT - ) - ) - /** - * Use one extra bin to distinguish between the read and write indices - * when full. See Tim Blechmann's |boost::lockfree::spsc_queue| - * implementation. - */ - this.bufferLength = size + 1 - this.channelCount = channelCount - this.channelData = [] - for (let i = 0; i < channelCount; i++) { - this.channelData.push( - new Float32Array( - new SharedArrayBuffer( - this.bufferLength * Float32Array.BYTES_PER_ELEMENT - ) - ) - ) - } - } - - /** - * Helper function for creating FreeQueue from pointers. - * @param {FreeQueuePointers} queuePointers - * An object containing various pointers required to create FreeQueue - * - * interface FreeQueuePointers { - * memory: WebAssembly.Memory; // Reference to WebAssembly Memory - * bufferLengthPointer: number; - * channelCountPointer: number; - * statePointer: number; - * channelDataPointer: number; - * } - * @returns FreeQueue - */ - static fromPointers(queuePointers: { - memory: WebAssembly.Memory - bufferLengthPointer: number - channelCountPointer: number - statePointer: number - channelDataPointer: number - }): FreeQueue { - const queue = new FreeQueue(0, 0) - const HEAPU32 = new Uint32Array(queuePointers.memory.buffer) - const HEAPF32 = new Float32Array(queuePointers.memory.buffer) - const bufferLength = HEAPU32[queuePointers.bufferLengthPointer / 4] - const channelCount = HEAPU32[queuePointers.channelCountPointer / 4] - const states = HEAPU32.subarray( - HEAPU32[queuePointers.statePointer / 4] / 4, - HEAPU32[queuePointers.statePointer / 4] / 4 + 2 - ) - const channelData: Float32Array[] = [] - for (let i = 0; i < channelCount; i++) { - channelData.push( - HEAPF32.subarray( - HEAPU32[HEAPU32[queuePointers.channelDataPointer / 4] / 4 + i] / 4, - HEAPU32[HEAPU32[queuePointers.channelDataPointer / 4] / 4 + i] / 4 + - bufferLength - ) - ) - } - queue.bufferLength = bufferLength - queue.channelCount = channelCount - queue.states = states - queue.channelData = channelData - return queue - } - - /** - * Pushes the data into queue. Used by producer. - * - * @param {Float32Array[]} input Its length must match with the channel - * count of this queue. - * @param {number} blockLength Input block frame length. It must be identical - * throughout channels. - * @return {boolean} False if the operation fails. - */ - push(input: Float32Array[], blockLength: number): boolean { - const currentRead = Atomics.load(this.states, this.States.READ) - const currentWrite = Atomics.load(this.states, this.States.WRITE) - if (this._getAvailableWrite(currentRead, currentWrite) < blockLength) { - return false - } - let nextWrite = currentWrite + blockLength - if (this.bufferLength < nextWrite) { - nextWrite -= this.bufferLength - for (let channel = 0; channel < this.channelCount; channel++) { - if (!input[channel]) continue - const blockA = this.channelData[channel].subarray(currentWrite) - const blockB = this.channelData[channel].subarray(0, nextWrite) - blockA.set(input[channel].subarray(0, blockA.length)) - blockB.set(input[channel].subarray(blockA.length)) - } - } else { - for (let channel = 0; channel < this.channelCount; channel++) { - if (!input[channel]) continue - this.channelData[channel] - .subarray(currentWrite, nextWrite) - .set(input[channel].subarray(0, blockLength)) - } - if (nextWrite === this.bufferLength) nextWrite = 0 - } - Atomics.store(this.states, this.States.WRITE, nextWrite) - return true - } - - /** - * Pulls data out of the queue. Used by consumer. - * - * @param {Float32Array[]} output Its length must match with the channel - * count of this queue. - * @param {number} blockLength output block length. It must be identical - * throughout channels. - * @return {boolean} False if the operation fails. - */ - pull(output: Float32Array[], blockLength: number): boolean { - const currentRead = Atomics.load(this.states, this.States.READ) - const currentWrite = Atomics.load(this.states, this.States.WRITE) - if (this._getAvailableRead(currentRead, currentWrite) < blockLength) { - return false - } - let nextRead = currentRead + blockLength - if (this.bufferLength < nextRead) { - nextRead -= this.bufferLength - for (let channel = 0; channel < this.channelCount; channel++) { - const blockA = this.channelData[channel].subarray(currentRead) - const blockB = this.channelData[channel].subarray(0, nextRead) - output[channel].set(blockA) - output[channel].set(blockB, blockA.length) - } - } else { - for (let channel = 0; channel < this.channelCount; ++channel) { - output[channel].set( - this.channelData[channel].subarray(currentRead, nextRead) - ) - } - if (nextRead === this.bufferLength) { - nextRead = 0 - } - } - Atomics.store(this.states, this.States.READ, nextRead) - return true - } - - /** - * Helper function for debugging. - * Prints currently available read and write. - */ - printAvailableReadAndWrite() { - const currentRead = Atomics.load(this.states, this.States.READ) - const currentWrite = Atomics.load(this.states, this.States.WRITE) - console.log( - this._getAvailableRead(currentRead, currentWrite), - this._getAvailableWrite(currentRead, currentWrite) - ) - // console.log(this, { - // availableRead: this._getAvailableRead(currentRead, currentWrite), - // availableWrite: this._getAvailableWrite(currentRead, currentWrite), - // }) - } - - /** - * - * @returns {number} number of samples available for read - */ - getAvailableSamples(): number { - const currentRead = Atomics.load(this.states, this.States.READ) - const currentWrite = Atomics.load(this.states, this.States.WRITE) - return this._getAvailableRead(currentRead, currentWrite) - } - - /** - * - * @param {number} size - * @returns boolean. if frame of given size is available or not. - */ - isFrameAvailable(size: number): boolean { - return this.getAvailableSamples() >= size - } - - /** - * @return {number} - */ - getBufferLength(): number { - return this.bufferLength - 1 - } - - private _getAvailableWrite(readIndex: number, writeIndex: number): number { - if (writeIndex >= readIndex) - return this.bufferLength - writeIndex + readIndex - 1 - return readIndex - writeIndex - 1 - } - - private _getAvailableRead(readIndex: number, writeIndex: number): number { - if (writeIndex >= readIndex) return writeIndex - readIndex - return writeIndex + this.bufferLength - readIndex - } - - private _reset() { - for (let channel = 0; channel < this.channelCount; channel++) { - this.channelData[channel].fill(0) - } - Atomics.store(this.states, this.States.READ, 0) - Atomics.store(this.states, this.States.WRITE, 0) - } -} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7a0c25d..254abad 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -28,7 +28,7 @@ export function Home() { Canvas WebGL Editor - Dsp Node + Dsp Worker-Worklet AssemblyScript QrCode diff --git a/src/ui/editor/widgets/list-mark.ts b/src/ui/editor/widgets/list-mark.ts index 589d22a..dccbdae 100644 --- a/src/ui/editor/widgets/list-mark.ts +++ b/src/ui/editor/widgets/list-mark.ts @@ -23,7 +23,7 @@ export function ListMarkWidget(pane: Pane) { box.view.color = hexToInt(info.color) box.view.alpha = .2 - function update(audios: Float32Array[], values: SoundValue[], scalars: Float32Array) { + function update(values: SoundValue[], audios: Float32Array[], scalars: Float32Array) { const value = values[info.indexValue$] if (value.kind === SoundValueKind.Scalar) { info.value = scalars[value.ptr] diff --git a/src/ui/editor/widgets/rms-deco.ts b/src/ui/editor/widgets/rms-deco.ts index cc926cc..af8dafd 100644 --- a/src/ui/editor/widgets/rms-deco.ts +++ b/src/ui/editor/widgets/rms-deco.ts @@ -46,7 +46,7 @@ export function RmsDecoWidget(shapes: Shapes) { rect.y = y + h - rect.h }) - function update(audios: Float32Array[], values: SoundValue[], scalars: Float32Array) { + function update(values: SoundValue[], audios: Float32Array[], scalars: Float32Array) { const value = values[info.value$] if (value.kind === SoundValueKind.Scalar) { rmsFloats.fill(scalars[value.ptr]) diff --git a/tsconfig.json b/tsconfig.json index a338ada..9a75991 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -43,6 +43,12 @@ "allowJs": true, "strict": true, "paths": { + "ui": [ + "./src/ui/index.ts" + ], + "lang": [ + "./src/lang/index.ts" + ], "editor": [ "./src/ui/editor/index.ts" ], From 4ca4d309a06f401b4f4e9f7babba292eafa001ae Mon Sep 17 00:00:00 2001 From: stagas Date: Tue, 22 Oct 2024 23:24:30 +0300 Subject: [PATCH 32/33] bring back error cause --- src/as/dsp/preview-worker.ts | 2 +- src/comp/DspEditor.tsx | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/as/dsp/preview-worker.ts b/src/as/dsp/preview-worker.ts index 7ab09e4..14334f0 100644 --- a/src/as/dsp/preview-worker.ts +++ b/src/as/dsp/preview-worker.ts @@ -187,7 +187,7 @@ const worker = { return { error: { message: e.message, - cause: /* (e as any).cause ?? */{ nodes: [] } + cause: (e as any).cause ?? { nodes: [] } } } } diff --git a/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx index 0e1d894..1ee97d2 100644 --- a/src/comp/DspEditor.tsx +++ b/src/comp/DspEditor.tsx @@ -283,6 +283,16 @@ export function DspEditor({ code, width, height }: { pane.draw.widgets.subs.add(errorSub.widget) errorSub.info.error = error errorSub.widget.bounds = Token.bounds((error as any).cause?.nodes ?? [] as Token[]) + { + const { x, y } = pane.buffer.logicalPointToVisualPoint({ x: errorSub.widget.bounds.col, y: errorSub.widget.bounds.line }) + errorSub.widget.bounds.line = y + errorSub.widget.bounds.col = x + } + { + const { x, y } = pane.buffer.logicalPointToVisualPoint({ x: errorSub.widget.bounds.right, y: errorSub.widget.bounds.bottom }) + errorSub.widget.bounds.bottom = y + errorSub.widget.bounds.right = x + } pane.draw.info.triggerUpdateTokenDrawInfo++ pane.view.anim.info.epoch++ return () => { From 58a7cd222dc5d7c20d019d46f0c3a5fa8026cf48 Mon Sep 17 00:00:00 2001 From: stagas Date: Wed, 23 Oct 2024 08:43:06 +0300 Subject: [PATCH 33/33] feat: separate deco/mark and subs draws --- src/ui/editor/draw.ts | 5 +++-- src/ui/editor/widget.ts | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ui/editor/draw.ts b/src/ui/editor/draw.ts index 95deea8..3f3ad80 100644 --- a/src/ui/editor/draw.ts +++ b/src/ui/editor/draw.ts @@ -429,15 +429,16 @@ export function Draw({ paneInfo, view, selection, caret, dims, buffer, colorize drawClear() drawActiveLine() drawSelection() - widgets.draw() + widgets.drawDecoMark() drawCode() + widgets.drawSubs() drawCaret() c.restore() drawScrollbars() info.shouldRedraw = false } else { - widgets.draw() + widgets.drawDecoMark() } } diff --git a/src/ui/editor/widget.ts b/src/ui/editor/widget.ts index b3c6022..92ac0ec 100644 --- a/src/ui/editor/widget.ts +++ b/src/ui/editor/widget.ts @@ -48,14 +48,18 @@ export function Widgets({ c }: { }) } - function draw() { + function drawDecoMark() { update() deco.forEach(widgetDraw) - subs.forEach(widgetDraw) mark.forEach(widgetDraw) } - const widgets = { update, draw, deco, subs, mark, heights, lines } + function drawSubs() { + update() + subs.forEach(widgetDraw) + } + + const widgets = { update, drawDecoMark, drawSubs, deco, subs, mark, heights, lines } return widgets }