diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 393829d..62af567 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,8 +31,14 @@ jobs: with: node-version: 22.x + - name: Uninstall dev deps with scripts + run: npm remove @vite-pwa/assets-generator --force --ignore-scripts=true + - name: Install dependencies - run: npm install --ignore-scripts --force + run: npm i --force --ignore-scripts=true + + - name: Install dev deps with scripts + 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 9913e26..103a20a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -51,8 +51,14 @@ jobs: with: node-version: 22.x + - name: Uninstall dev deps with scripts + run: npm remove @vite-pwa/assets-generator --force --ignore-scripts=true + - name: Install dependencies - run: npm install --ignore-scripts --force + run: npm i --force --ignore-scripts=true + + - name: Install dev deps with scripts + run: npm i @vite-pwa/assets-generator -D --force - name: Build run: bun run build 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 -
+ - 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..1a809e3 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' @@ -49,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', } @@ -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) @@ -122,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/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/constants.ts b/as/assembly/dsp/constants.ts new file mode 100644 index 0000000..a2a5584 --- /dev/null +++ b/as/assembly/dsp/constants.ts @@ -0,0 +1,11 @@ +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 +export const MAX_OPS = 4096 +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/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..9926b1c --- /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 = Infinity + 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..b2512b0 --- /dev/null +++ b/as/assembly/dsp/gen/adsr.ts @@ -0,0 +1,91 @@ +import { clamp } from '../../util' +import { Gen } from './gen' + +enum AdsrState { + Release, + Attack, + Decay, +} + +export class Adsr extends Gen { + _name: string = 'Adsr' + 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..86f4cdd --- /dev/null +++ b/as/assembly/dsp/gen/atan.ts @@ -0,0 +1,35 @@ +import { Gen } from './gen' + +export class Atan extends Gen { + _name: string = 'Atan'; + + 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..4dac212 --- /dev/null +++ b/as/assembly/dsp/gen/bap.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Bap extends Biquad { + _name: string = 'Bap' + 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..58a7a08 --- /dev/null +++ b/as/assembly/dsp/gen/bbp.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Bbp extends Biquad { + _name: string = 'Bbp' + 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..4a68707 --- /dev/null +++ b/as/assembly/dsp/gen/bhp.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Bhp extends Biquad { + _name: string = 'Bhp' + 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..583f0ff --- /dev/null +++ b/as/assembly/dsp/gen/bhs.ts @@ -0,0 +1,12 @@ +import { Biquad } from './biquad' + +export class Bhs extends Biquad { + _name: string = 'Bhs' + 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..e5a90f1 --- /dev/null +++ b/as/assembly/dsp/gen/blp.ts @@ -0,0 +1,11 @@ +import { Biquad } from './biquad' + +export class Blp extends Biquad { + _name: string = 'Blp' + 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..f42370d --- /dev/null +++ b/as/assembly/dsp/gen/bls.ts @@ -0,0 +1,12 @@ +import { Biquad } from './biquad' + +export class Bls extends Biquad { + _name: string = 'Bls' + 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..47a077d --- /dev/null +++ b/as/assembly/dsp/gen/clip.ts @@ -0,0 +1,39 @@ +import { Gen } from './gen' + +export class Clip extends Gen { + _name: string = 'Clip' + 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..83ac756 --- /dev/null +++ b/as/assembly/dsp/gen/dclipexp.ts @@ -0,0 +1,34 @@ +import { Gen } from './gen' + +export class Dclipexp extends Gen { + _name: string = 'Dclipexp' + 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..416a311 --- /dev/null +++ b/as/assembly/dsp/gen/exp.ts @@ -0,0 +1,8 @@ +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/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..e51d9d2 --- /dev/null +++ b/as/assembly/dsp/gen/gen.ts @@ -0,0 +1,11 @@ +import { Engine } from '../core/engine' + +export abstract class Gen { + _name: string = '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..3aa5825 --- /dev/null +++ b/as/assembly/dsp/gen/lp.ts @@ -0,0 +1,47 @@ +import { Gen } from './gen' + +export class Lp extends Gen { + _name: string = 'Lp' + 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..4991520 --- /dev/null +++ b/as/assembly/dsp/gen/noi.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..8f0d305 --- /dev/null +++ b/as/assembly/dsp/gen/nrate.ts @@ -0,0 +1,20 @@ +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 + 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..57492c1 --- /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 = 0 + /** 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..103d3b6 --- /dev/null +++ b/as/assembly/dsp/gen/saw.ts @@ -0,0 +1,8 @@ +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/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..b540a27 --- /dev/null +++ b/as/assembly/dsp/gen/sbp.ts @@ -0,0 +1,36 @@ +import { Svf } from './svf' + +export class Sbp extends Svf { + _name: string = 'Sbp' + 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..c568394 --- /dev/null +++ b/as/assembly/dsp/gen/shp.ts @@ -0,0 +1,36 @@ +import { Svf } from './svf' + +export class Shp extends Svf { + _name: string = 'Shp' + 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..2ef9d4e --- /dev/null +++ b/as/assembly/dsp/gen/sin.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..e571c2a --- /dev/null +++ b/as/assembly/dsp/gen/slp.ts @@ -0,0 +1,36 @@ +import { Svf } from './svf' + +export class Slp extends Svf { + _name: string = 'Slp' + 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..8790387 --- /dev/null +++ b/as/assembly/dsp/gen/sno.ts @@ -0,0 +1,36 @@ +import { Svf } from './svf' + +export class Sno extends Svf { + _name: string = 'Sno' + 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..af85d34 --- /dev/null +++ b/as/assembly/dsp/gen/tanh.ts @@ -0,0 +1,31 @@ +import { Gen } from './gen' + +export class Tanh 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 + + 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..e368f04 --- /dev/null +++ b/as/assembly/dsp/gen/tanha.ts @@ -0,0 +1,62 @@ +import { Gen } from './gen' + +export class Tanha extends Gen { + in: u32 = 0 + + _gainv: v128 = f32x4.splat(1.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..d94f5f7 --- /dev/null +++ b/as/assembly/dsp/index.ts @@ -0,0 +1,143 @@ +import { MAX_LISTS, MAX_LITERALS, MAX_OPS } from './constants' +import { Clock } from './core/clock' +import { Core, Engine } from './core/engine' +import { Out, Track } from './shared' +import { Player } from './vm/player' +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): usize { + return changetype(new Engine(sampleRate, core)) +} + +export function getEngineClock(engine$: usize): usize { + return changetype(changetype(engine$).clock) +} + +export function clockReset(clock$: usize): void { + const clock = changetype(clock$) + clock.reset() +} + +export function clockUpdate(clock$: usize): void { + const clock = changetype(clock$) + clock.update() +} + +export function createSound(engine$: usize): usize { + return changetype(new Sound(changetype(engine$))) +} + +export function resetSound(sound$: usize): void { + changetype(sound$).reset() +} + +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, + audio_LR$: i32, + begin: u32, + end: u32, + out$: usize +): void { + const sound = changetype(sound$) + sound.fill( + ops$, + audio_LR$, + begin, + end, + out$ + ) +} + +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]) +} + +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) +} + +export function setSoundLiterals(sound$: usize, literals$: usize): void { + changetype(sound$).literals = changetype>(literals$) +} + +export function getSoundScalars(sound$: usize): usize { + return changetype(changetype(sound$).scalars) +} + +export function getSoundLists(sound$: usize): usize { + return changetype(changetype(sound$).lists) +} + +export function createPlayer(sound$: usize, out$: usize): usize { + return changetype(new Player(sound$, out$)) +} + +export function getPlayerTrackOffset(): usize { + return offsetof('track$') +} + +export function setPlayerTrack(player$: usize, track$: usize): void { + changetype(player$).track$ = track$ +} + +export function playerProcess(player$: usize, begin: u32, end: u32): void { + const player = changetype(player$) + player.process(begin, end) +} + +export function createOut(): usize { + return changetype(new Out()) +} + +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 new file mode 100644 index 0000000..65475aa --- /dev/null +++ b/as/assembly/dsp/shared.ts @@ -0,0 +1,22 @@ +@unmanaged +export class Track { + run_ops$: usize = 0 + setup_ops$: usize = 0 + literals$: usize = 0 + lists$: usize = 0 + audio_LR$: i32 = 0 +} + +@unmanaged +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/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..c528ea6 --- /dev/null +++ b/as/assembly/dsp/vm/dsp-shared.ts @@ -0,0 +1,20 @@ +export enum SoundValueKind { + Null, + I32, + Floats, + Literal, + Scalar, + Audio, + Dynamic, +} + +export enum DspBinaryOp { + // math: commutative + Add, + Mul, + + // math: non-commutative + Sub, + Div, + Pow, +} diff --git a/as/assembly/dsp/vm/dsp.ts b/as/assembly/dsp/vm/dsp.ts new file mode 100644 index 0000000..6f90f8f --- /dev/null +++ b/as/assembly/dsp/vm/dsp.ts @@ -0,0 +1,274 @@ +import { Factory } from '../../../../generated/assembly/dsp-factory' +import { Offsets } from '../../../../generated/assembly/dsp-offsets' +import { modWrap } from '../../util' +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 } from './sound' + +export { DspBinaryOp } + +export class Dsp { + constructor() { } + + @inline + CreateGen(snd: Sound, kind_index: i32): void { + const Gen = Factory[kind_index] + 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)) + // } + } + @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/player.ts b/as/assembly/dsp/vm/player.ts new file mode 100644 index 0000000..06ca32d --- /dev/null +++ b/as/assembly/dsp/vm/player.ts @@ -0,0 +1,30 @@ +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() + 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 new file mode 100644 index 0000000..da526df --- /dev/null +++ b/as/assembly/dsp/vm/sound.ts @@ -0,0 +1,186 @@ +import { run as dspRun } from '../../../../generated/assembly/dsp-runner' +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, SoundValue, 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) +} + +// { 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) { } + + begin: u32 = 0 + end: u32 = 0 + pan: f32 = 0 + + gens: Gen[] = [] + prevGens: Gen[] = [] + offsets: usize[][] = [] + + 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) + + @inline + reset(): void { + this.gens.forEach(gen => { + gen._reset() + }) + this.literals.fill(0) + this.scalars.fill(0) + } + + @inline + clear(): void { + this.prevGens = this.gens + this.gens = [] + this.offsets = [] + // this.values = [] + // 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) + 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$ + + 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 = x + i + this.end = x + (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) + p, + chunkEnd << 2 + ) + // TODO: stereo + // 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 + + 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), 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 = x + i + this.end = x + (i + CHUNK_SIZE > chunkEnd ? chunkEnd - i : i + CHUNK_SIZE) + dspRun(changetype(this), 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 + + if (audio_LR$ < this.audios.length) { + const audio = this.audios[audio_LR$] + const p = (x << 2) + memory.copy( + out$ + p, + changetype(audio) + p, + chunkEnd << 2 + ) + } + } + } +} 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..584b590 100644 --- a/as/assembly/gfx/draw.ts +++ b/as/assembly/gfx/draw.ts @@ -1,6 +1,6 @@ -import { logf, logf2, logf3, logf4, logf6, logi } from '../env' +import { clamp11 } from '../util' 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 @@ -589,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 @@ -608,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/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/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 new file mode 100644 index 0000000..fba15a3 --- /dev/null +++ b/as/assembly/util.ts @@ -0,0 +1,255 @@ +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 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) +} + +// @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-nort.json b/asconfig-dsp-nort.json new file mode 100644 index 0000000..6ce91cc --- /dev/null +++ b/asconfig-dsp-nort.json @@ -0,0 +1,35 @@ +{ + "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 + } +} diff --git a/asconfig-dsp.json b/asconfig-dsp.json new file mode 100644 index 0000000..1a18b45 --- /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": 2000, + "maximumMemory": 2000, + "bindings": "raw", + "runtime": "incremental", + "exportRuntime": true + } +} 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/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/assembly/dsp-factory.ts b/generated/assembly/dsp-factory.ts new file mode 100644 index 0000000..3bcc227 --- /dev/null +++ b/generated/assembly/dsp-factory.ts @@ -0,0 +1,109 @@ +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] +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-offsets.ts b/generated/assembly/dsp-offsets.ts new file mode 100644 index 0000000..a0a0d65 --- /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('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('in')], + [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..a9d3203 --- /dev/null +++ b/generated/assembly/dsp-runner.ts @@ -0,0 +1,137 @@ +// 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(sound$: usize, ops$: usize): void { + const snd = changetype(sound$) + const ops = changetype>(ops$) + + let i: i32 = 0 + let op: i32 = 0 + + while (unchecked(op = ops[i++])) { + switch (op) { + + case Op.CreateGen: + dsp.CreateGen( + snd, + 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, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.LiteralToAudio: + dsp.LiteralToAudio( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.Pick: + dsp.Pick( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.Pan: + dsp.Pan( + snd, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.SetValue: + dsp.SetValue( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.SetValueDynamic: + dsp.SetValueDynamic( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.SetProperty: + dsp.SetProperty( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.UpdateGen: + dsp.UpdateGen( + snd, + changetype(unchecked(ops[i++])) + ) + continue + + case Op.ProcessAudio: + dsp.ProcessAudio( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.ProcessAudioStereo: + dsp.ProcessAudioStereo( + snd, + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])), + changetype(unchecked(ops[i++])) + ) + continue + + case Op.BinaryOp: + dsp.BinaryOp( + snd, + 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..9145254 --- /dev/null +++ b/generated/typescript/dsp-gens.ts @@ -0,0 +1,393 @@ +// +// auto-generated Tue Oct 22 2024 23:08:20 GMT+0300 (Eastern European Summer Time) + +import { Value } from '../../src/as/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: [ '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: [ '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 } +} 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 { + 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 { + in?: Value.Audio + } + export interface Tanha extends Gen { + 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, 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, 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/index.html b/index.html index 64b9c9e..bbe1d8f 100644 --- a/index.html +++ b/index.html @@ -21,13 +21,9 @@ Vasi -
+ - diff --git a/package.json b/package.json index a1ff49b..523135d 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,9 @@ "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" + "link": "link-local || exit 0" }, "devDependencies": { "@happy-dom/global-registrator": "^15.7.4", @@ -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/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/scripts/generate-dsp-vm.ts b/scripts/generate-dsp-vm.ts new file mode 100644 index 0000000..d2441a8 --- /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(sound$: usize, ops$: usize): void { + const snd = changetype(sound$) + 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}( + snd, +${indent(4, args.map(([, type]) => + `changetype<${type}>(unchecked(ops[i++]))` + ).join(',\n'))} + ) + continue`).join('\n\n') + '\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..f552bb8 --- /dev/null +++ b/scripts/update-dsp-factory.ts @@ -0,0 +1,38 @@ +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[] = [] +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)) { + 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}]`) +out.push(`export const Ctors: string[] = [${ctors}]`) + +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/build.ts b/src/as/dsp/build.ts new file mode 100644 index 0000000..a9f4cf1 --- /dev/null +++ b/src/as/dsp/build.ts @@ -0,0 +1,467 @@ +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' + +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 type TrackContext = ReturnType + +export function getTrackContext(view: MemoryView, track: Track) { + 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/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.ts b/src/as/dsp/dsp.ts new file mode 100644 index 0000000..64d9638 --- /dev/null +++ b/src/as/dsp/dsp.ts @@ -0,0 +1,54 @@ +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 createDsp(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 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()) + 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$, + audios$$, + values$$, + scalars, + tracks$$, + run_ops$$, + setup_ops$$, + literals$$, + lists$$, + } +} diff --git a/src/as/dsp/index.ts b/src/as/dsp/index.ts new file mode 100644 index 0000000..647c676 --- /dev/null +++ b/src/as/dsp/index.ts @@ -0,0 +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/node.ts b/src/as/dsp/node.ts new file mode 100644 index 0000000..1c01d6d --- /dev/null +++ b/src/as/dsp/node.ts @@ -0,0 +1,166 @@ +import { Sigui } from 'sigui' +import { getMemoryView, rpc, type MemoryView } from 'utils' +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( + 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() + +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[], + 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 + info.clock = Clock(dsp.memory.buffer, dsp.clock$) + }) + node.connect(ctx.destination) + } + + if (!registeredContexts.has(ctx)) { + registeredContexts.add(ctx) + ctx.audioWorklet + .addModule(dspWorkletUrl) + .then(createNode) + } + else { + createNode() + } + + const hashes: Map = new Map() + + function build(code: string) { + const { tracks, currTrack, nextTrack } = $.of(info) + let track = tracks[currTrack] + + const { tokens, hashId } = getTokens(code.replaceAll('\n', '\r\n')) + + const prevHashId = hashes.get(track) + const isNew = hashId !== prevHashId + + if (isNew) { + track = tracks[info.currTrack = nextTrack] + info.nextTrack = (nextTrack + 1) % tracks.length + hashes.set(track, hashId) + } + + const buildTrack = builds.get(track)! + + const { out } = buildTrack(tokens) + + track.audio_LR$ = out.LR?.audio$ || out.LR?.ptr || 0 + + return { track$: track.ptr } + } + + // setup tracks + $.fx(() => { + const { dsp } = $.of(info) + $() + const view = info.view = getMemoryView(dsp.memory) + + info.tracks = setupTracks( + view, + dsp.tracks$$, + dsp.run_ops$$, + dsp.setup_ops$$, + dsp.literals$$, + dsp.lists$$, + ) + }) + + // update player 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/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/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 new file mode 100644 index 0000000..2b681e0 --- /dev/null +++ b/src/as/dsp/preview-service.ts @@ -0,0 +1,47 @@ +import { Sigui } from 'sigui' +import { Deferred, getMemoryView, rpc, type MemoryView } 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>, + view: null as null | MemoryView + }) + + isReady.then(() => { + info.isReady = true + }) + + $.fx(() => { + const { isReady } = $.of(info) + $().then(async () => { + const dsp = await service.createDsp(ctx.sampleRate) + const view = getMemoryView(dsp.memory) + $.batch(() => { + info.dsp = dsp + info.view = view + }) + }) + }) + + 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..14334f0 --- /dev/null +++ b/src/as/dsp/preview-worker.ts @@ -0,0 +1,201 @@ +// @ts-ignore +self.document = { + querySelectorAll() { return [] as any }, + baseURI: location.origin +} + +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/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 + +interface WidgetInfo { + value$: number + bounds: Token.Bounds +} + +interface ListInfo { + list: Token.Bounds[] + value$: number + bounds: Token.Bounds +} + +const waveWidgets: WidgetInfo[] = [] +const rmsWidgets: WidgetInfo[] = [] +const listWidgets: ListInfo[] = [] + +const worker = { + 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) { + 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( + 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.audios$$, + values$$: dsp.values$$, + scalars: dsp.scalars, + } + }, + build(code: string) { + const { dsp, track } = this + if (!dsp || !track) throw new Error('Dsp not ready.') + + const { tokens } = getTokens(code.replaceAll('\n', '\r\n')) + + 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 } }) => + a.line === b.line + ? a.col - b.col + : a.line - b.line + ) + + let last: AstNode | null = null + const nodes = new Map() + + let waveCount = 0 + let rmsCount = 0 + let listCount = 0 + + for (const node of program.value.results) { + 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: WidgetInfo + + if ('genId' in node) { + info = (waveWidgets[waveCount] ??= { value$: -1, bounds }) + waveCount++ + } + else if ('op' in node) { + 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.value$ = (node.result.value as Value.Dynamic).value$ + assign(info.bounds, bounds) + + nodes.set(node.result, info) + + last = node.result + } + } + + let delta = waveWidgets.length - waveCount + while (delta-- > 0) waveWidgets.pop() + + delta = rmsWidgets.length - rmsCount + while (delta-- > 0) rmsWidgets.pop() + + delta = listWidgets.length - listCount + while (delta-- > 0) listWidgets.pop() + + const LR = out.LR.getAudio() + + return { + LR, + waves: waveWidgets as WidgetInfo[], + rmss: rmsWidgets as WidgetInfo[], + lists: listWidgets as ListInfo[], + } + }, + async renderSource(code: string) { + const { dsp, view, clock, track } = this + if (!dsp || !view || !clock || !track) throw new Error('Dsp not ready.') + + const info = this + + try { + wasm.resetSound(dsp.sound$) + wasm.clearSound(dsp.sound$) + + clock.time = 0 + clock.barTime = 0 + clock.bpm ||= 144 + + wasm.clockUpdate(clock.ptr) + + const { LR, waves, rmss, lists } = this.build(code) + + wasm.soundSetupTrack(dsp.sound$, track.ptr) + + wasm.fillTrack( + dsp.sound$, + track.ptr, + 0, + BUFFER_SIZE, + dsp.out$, + ) + + const floats = dsp.L + return { LR, floats, waves, rmss, lists } + } + 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/rms.ts b/src/as/dsp/rms.ts new file mode 100644 index 0000000..83098c5 --- /dev/null +++ b/src/as/dsp/rms.ts @@ -0,0 +1,21 @@ +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: { + log: console.log, + } +}) diff --git a/src/as/dsp/shared.ts b/src/as/dsp/shared.ts new file mode 100644 index 0000000..8a813e8 --- /dev/null +++ b/src/as/dsp/shared.ts @@ -0,0 +1,51 @@ +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 Track = typeof Track.type +export const Track = Struct({ + run_ops$: 'usize', + setup_ops$: 'usize', + literals$: 'usize', + lists$: 'usize', + 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', + scalar$: 'i32', + audio$: 'i32', +}) 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..8601076 --- /dev/null +++ b/src/as/dsp/value.ts @@ -0,0 +1,126 @@ +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/build.ts' + +type SoundPartial = { context: TrackContext, vm: DspVm } + +export class Value { + value$: number + ptr: number = 0 + scalar$: number = 0 + audio$: number = 0 + 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) { + 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, this.ptr) + } + } + getAudio() { + if (this.kind === Value.Kind.Dynamic) { + return this.audio$ + } + else { + return this.ptr + } + } + getScalar() { + if (this.kind === Value.Kind.Dynamic) { + return this.scalar$ + } + 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..764a61d --- /dev/null +++ b/src/as/dsp/wasm.ts @@ -0,0 +1,25 @@ +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: { + log: console.warn, + } +}) + +const { alloc } = initWasm(wasmInstance) + +export const wasm = Object.assign(wasmInstance, { alloc }) diff --git a/src/as/dsp/worklet.ts b/src/as/dsp/worklet.ts new file mode 100644 index 0000000..8451931 --- /dev/null +++ b/src/as/dsp/worklet.ts @@ -0,0 +1,187 @@ +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.ts' +import { Clock, DspWorkletMode } 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(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}`) + }, + 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 dsp = createDsp(sampleRate, wasm, memory) + + return { + wasm, + memory, + ...dsp, + } +} + +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 kernel: { player$: number, process: AudioProcess } = { + player$, + process: (_inputs, _outputs) => { + inputs = _inputs + outputs = _outputs + modes[mode[0]]() + } + } + + return kernel +} + +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/init-wasm.ts b/src/as/init-wasm.ts index d80a0b6..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 = 1024000 + const GC_EVERY = 819200 let allocs = 0 const funcs = new Map([ @@ -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/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/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..9cf8dfa 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 { 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/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/src/comp/DspEditor.tsx b/src/comp/DspEditor.tsx new file mode 100644 index 0000000..1ee97d2 --- /dev/null +++ b/src/comp/DspEditor.tsx @@ -0,0 +1,306 @@ +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' +import { screen } from '~/src/screen.ts' +import { theme } from '~/src/theme.ts' +import { Editor } from '~/src/ui/Editor.tsx' +import { ErrorSubWidget, HoverMarkWidget } from '~/src/ui/editor/widgets/index.ts' + +export type DspEditor = ReturnType + +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, + error: null as Error | null, + }) + + const mouse = { x: 0, y: 0 } + let number: RegExpMatchArray | undefined + let value: number + let digits: number + let isDot = false + + 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 + 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) { + if (pane.mouse.info.isDown) return + 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, + } + ) + 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 + } + } + + 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 + } + + 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 + 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 && clickCount <= 1) { + number = void 0 + updateNumberMark(pane) + return true + } + } + + const inputHandlers = { + onKeyDown, + onKeyUp, + onMouseWheel, + onMouseDown, + onMouseUp, + 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: 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) { + 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, + }) + + 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[]) + { + 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 () => { + 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/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.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 new file mode 100644 index 0000000..d7d8674 --- /dev/null +++ b/src/lang/interpreter.ts @@ -0,0 +1,440 @@ +import { dspGens } from '~/generated/typescript/dsp-gens.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' +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, + data: Partial = {}, + public captured: Token[] = [] + ) { + Object.assign(this, data) + } + id?: string + kind?: AstNode.ProcKind + scope: Scope = new Scope(null) + value?: + | { tokens: Token[] } + | Value.Audio + | [Value.Audio, Value.Audio] + | NumberInfo + | string + | number + | ProgramValue + + format?: NumberFormat + digits?: number + + 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: AstNode[] = [] + stackPop() { + return this.stack.pop() + } + stackPush(x: AstNode) { + this.stack.push(x) + } + stackUnshiftOfTypes(types: AstNode.Type[], climb?: boolean): AstNode | undefined { + 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: AstNode.Type[], climb?: boolean): AstNode | undefined { + 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(g: DspApi, data: Record, tokens: Token[]) { + const scope: Scope = new Scope(null, { ...ScopeNatives, ...ScopeSpecial, ...data }) + const results: ProgramValueResult[] = [] + 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 + + const uncaptured = new Set() + + function createContext(tokens: Token[]) { + let i = 0 + let t: Token + + function next() { + t = tokens[i++] + if (!uncaptured.has(t)) capturing.forEach(c => c.push(t)) + return t + } + function peek() { + 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()].filter(Boolean) } }) + } + 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)) + } + 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 string[] + + 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 }]) => + [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 = 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 }) + 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 && 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) + if (value?.value == null) throw new TypeError('Value expected.') + g.pan(value.value as 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: AstNode & { value: Value } + let r: Value = scope.stack.pop()?.value as Value + if (r == null) return + while (scope.stack.length) { + 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]) + return node + } + break + } + + case Token.Type.Op: + if (t.text === '?') { + const index = scope.stackPopOfTypes(ConsumeTypes) + 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 => 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 + } + 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: [t] } }) + } + if (!l) { + throw new Error('Missing left operand.', { cause: { nodes: [t] } }) + } + scope.vars[r.value as string] = l + return + } + switch (t.text) { + case '{': { + const op = t.text + const close = Token.Close[op] + 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 + } + 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, + } as ProgramValue + }) as AstNode & { value: ProgramValue } + } + + return program() +} 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/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 a5d0635..7de746b 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 { DspNodeDemo } from '~/src/pages/DspNodeDemo.tsx' import { EditorDemo } from '~/src/pages/EditorDemo.tsx' import { Home } from '~/src/pages/Home.tsx' import { OAuthRegister } from '~/src/pages/OAuthRegister.tsx' @@ -17,8 +18,9 @@ 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 } from '~/src/state.ts' +import { state, triggers } from '~/src/state.ts' import { go, Link } from '~/src/ui/Link.tsx' export function App() { @@ -29,7 +31,7 @@ export function App() { const info = $({ bg: 'transparent', canvasWidth: state.$.containerWidth, - canvasHeight: 800, + canvasHeight: state.$.containerHeight, }) const router = CachingRouter({ @@ -40,6 +42,8 @@ export function App() { '!/canvas': () => , '/webgl': () => , '/editor': () => , + '/dsp': () => , + '/worker-worklet': () => , '/asc': () => , '/qrcode': () => , '/about': () => , @@ -78,6 +82,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/DspNodeDemo.tsx b/src/pages/DspNodeDemo.tsx new file mode 100644 index 0000000..6742f33 --- /dev/null +++ b/src/pages/DspNodeDemo.tsx @@ -0,0 +1,323 @@ +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 { DspEditor } from '~/src/comp/DspEditor.tsx' +import { screen } from '~/src/screen.ts' +import { state } from '~/src/state.ts' +import { ListMarkWidget, RmsDecoWidget, WaveGlDecoWidget } 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* + +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] + +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()) + +export function DspNodeDemo() { + using $ = Sigui() + + const info = $({ + 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) 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] +`, + codeWorking: null as null | string, + audios: [] as Float32Array[], + values: [] as SoundValue[], + floats: new Float32Array(), + previewSound$: null as null | number, + previewAudios: [] as Float32Array[], + previewValues: [] as SoundValue[], + previewScalars: new Float32Array(), + error: null as null | Error, + }) + + const ctx = new AudioContext({ sampleRate: 48000, latencyHint: 0.00001 }) + $.fx(() => () => ctx.close()) + + const preview = PreviewService(ctx) + $.fx(() => preview.dispose) + + const dspNode = createDspNode(ctx) + $.fx(() => dspNode.dispose) + + $.fx(() => { + const { codeWorking } = info + $() + dspNode.info.code = codeWorking + }) + + $.fx(() => { + const { dsp, view } = $.of(dspNode.info) + $() + 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: { scalars } } = $.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], + 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) + } + + for (const rms of rmsWidgets) { + rms.update(values, audios, scalars) + } + + for (const list of listWidgets) { + list.update(values, audios, scalars) + } + + pane.view.anim.info.epoch++ + 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, info.$.width, info.$.height) + const matrix = Matrix() + const c = gfx.createContext(view, matrix) + const shapes = c.createShapes() + c.sketch.scene.add(shapes) + + 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, dsp, view: previewView } = $.of(preview.info) + $() + 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() { + const { previewSound$, previewAudios, previewValues, previewScalars } = info + if (!previewSound$) return + + const { pane } = dspEditor.editor.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(code) + + const { isPlaying } = dspNode.info + + pane.draw.widgets.deco.clear() + plot.info.floats.fill(0) + + if (result.error) { + throw new Error(result.error.message, { cause: result.error.cause }) + } + + $.batch(() => { + info.error = null + info.codeWorking = code + }) + + const end = $.batch() + + plot.info.index = result.LR + 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)) + + wave.info.floats = wave.info.floats.length + ? wave.info.floats + : getFloatsGfx(`${waveCount}`, BUFFER_SIZE) + + wave.info.previewFloats = wave.info.previewFloats.length + ? wave.info.previewFloats + : getFloatsGfx(`p:${waveCount}`, BUFFER_SIZE) + + wave.info.stabilizerTemp = wave.info.stabilizerTemp.length + ? wave.info.stabilizerTemp + : getFloatsGfx(`s:${waveCount}`, BUFFER_SIZE) + + 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, fixBounds(waveInfo.bounds)) + pane.draw.widgets.deco.add(wave.widget) + waveCount++ + } + + for (const rmsInfo of result.rmss) { + const rms = (rmsWidgets[rmsCount] ??= RmsDecoWidget(pane.draw.shapes)) + + rms.info.index = previewValues[rmsInfo.value$].ptr + rms.info.value$ = rmsInfo.value$ + if (!isPlaying) rms.update(previewValues, previewAudios, previewScalars) + + 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) { + if (err instanceof Error) { + info.error = err + } + else { + throw err + } + } + + let delta = waveWidgets.length - waveCount + while (delta-- > 0) waveWidgets.pop()?.dispose() + + delta = rmsWidgets.length - rmsCount + while (delta-- > 0) rmsWidgets.pop()?.dispose() + + delta = listWidgets.length - listCount + while (delta-- > 0) listWidgets.pop()?.dispose() + + pane.draw.info.triggerUpdateTokenDrawInfo++ + pane.view.anim.ticks.add(c.meshes.draw) + pane.view.anim.info.epoch++ + pane.draw.widgets.update() + } + + const buildThrottled = throttle(16, build) + + queueMicrotask(() => { + $.fx(() => { + const { previewSound$ } = $.of(info) + const { pane } = dspEditor.editor.info + const { codeVisual } = pane.buffer.info + const { isPlaying } = dspNode.info + $() + queueMicrotask(isPlaying ? buildThrottled : build) + }) + }) + + 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
+ + {dspEditor} + {canvas} +
+} diff --git a/src/pages/EditorDemo.tsx b/src/pages/EditorDemo.tsx index 7242f2b..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) @@ -314,8 +315,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/pages/Home.tsx b/src/pages/Home.tsx index ef8afe7..254abad 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -28,6 +28,8 @@ export function Home() { Canvas WebGL Editor + Dsp + 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/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/state.ts b/src/state.ts index 36a0b12..962fdf7 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 + 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 + } + // url url: typeof link.$.url get pathname(): string { 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