diff --git a/CHANGELOG.md b/CHANGELOG.md index eac3204..f1fe724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # synthlet +## 0.3.0 + +- Function `registerSynthlet` renamed to `registerAllWorklets` + +- Initial release of: + - @synthlet/dattorro-reverb@0.1.0 + +## 0.2.0 + +- Initial release of the following modules: + + - @synthlet/param@0.1.0 + - @synthlet/ad@0.1.0 + - @synthlet/adsr@0.1.0 + - @synthlet/chorus-t@0.1.0 + - @synthlet/clip-amp@0.1.0 + - @synthlet/clock@0.1.0 + - @synthlet/euclid@0.1.0 + - @synthlet/impulse@0.1.0 + - @synthlet/lfo@0.1.0 + - @synthlet/noise@0.1.0 + - @synthlet/polyblep-oscillator@0.1.0 + - @synthlet/state-variable-filter@0.1.0 + - @synthlet/wavetable-oscillator@0.1.0 + ## 0.1.0 Initial implementation of: diff --git a/README.md b/README.md index 739f421..1947bc9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Collection of synth modules implemented as AudioWorklets. ```ts import { - registerSynthlet, + registerAllWorklets, AdsrAmp, StateVariableFilter, StateVariableFilterType, @@ -14,7 +14,7 @@ import { } from "synthlet"; const ac = new AudioContext(); -await registerSynthlet(ac); +await registerAllWorklets(ac); // Simplest synth: Oscillator -> Filter -> Amplifier const osc = PolyblepOscillator(ac, { frequency: 440 }); diff --git a/package-lock.json b/package-lock.json index 027a0cd..a453b9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -706,12 +706,12 @@ } }, "node_modules/@changesets/logger": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.0.tgz", - "integrity": "sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", "dev": true, "dependencies": { - "chalk": "^2.1.0" + "picocolors": "^1.1.0" } }, "node_modules/@changesets/parse": { @@ -754,12 +754,11 @@ } }, "node_modules/@changesets/should-skip-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.0.tgz", - "integrity": "sha512-FxG6Mhjw7yFStlSM7Z0Gmg3RiyQ98d/9VpQAZ3Fzr59dCOM9G6ZdYbjiSAt0XtFr9JR5U2tBaJWPjrkGGc618g==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.1.tgz", + "integrity": "sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg==", "dev": true, "dependencies": { - "@babel/runtime": "^7.20.1", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3" } @@ -2321,8 +2320,8 @@ "resolved": "packages/clock", "link": true }, - "node_modules/@synthlet/drum8": { - "resolved": "packages/drum8", + "node_modules/@synthlet/dattorro-reverb": { + "resolved": "packages/dattorro-reverb", "link": true }, "node_modules/@synthlet/euclid": { @@ -3669,7 +3668,7 @@ } }, "node_modules/get-package-type": { - "version": "0.0.0", + "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, @@ -6286,9 +6285,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true }, "node_modules/picomatch": { @@ -7715,7 +7714,7 @@ } }, "node_modules/yocto-queue": { - "version": "0.0.0", + "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, @@ -7728,7 +7727,7 @@ }, "packages/ad": { "name": "@synthlet/ad", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7738,7 +7737,7 @@ }, "packages/adsr": { "name": "@synthlet/adsr", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", @@ -7758,7 +7757,7 @@ }, "packages/chorus-t": { "name": "@synthlet/chorus-t", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", @@ -7768,7 +7767,7 @@ }, "packages/clip-amp": { "name": "@synthlet/clip-amp", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7778,6 +7777,15 @@ }, "packages/clock": { "name": "@synthlet/clock", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.11", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + } + }, + "packages/dattorro-reverb": { "version": "0.0.0", "license": "MIT", "devDependencies": { @@ -7789,6 +7797,7 @@ "packages/drum8": { "name": "@synthlet/drum8", "version": "0.0.0", + "extraneous": true, "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7798,7 +7807,7 @@ }, "packages/euclid": { "name": "@synthlet/euclid", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7808,7 +7817,7 @@ }, "packages/impulse": { "name": "@synthlet/impulse", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7818,7 +7827,7 @@ }, "packages/lfo": { "name": "@synthlet/lfo", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", @@ -7828,7 +7837,7 @@ }, "packages/noise": { "name": "@synthlet/noise", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7838,7 +7847,7 @@ }, "packages/param": { "name": "@synthlet/param", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7848,7 +7857,7 @@ }, "packages/polyblep-oscillator": { "name": "@synthlet/polyblep-oscillator", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7858,7 +7867,7 @@ }, "packages/state-variable-filter": { "name": "@synthlet/state-variable-filter", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7875,7 +7884,7 @@ "@synthlet/chorus-t": "^0.1.0", "@synthlet/clip-amp": "^0.1.0", "@synthlet/clock": "^0.1.0", - "@synthlet/drum8": "^0.1.0", + "@synthlet/dattorro-reverb": "^0.0.0", "@synthlet/euclid": "^0.1.0", "@synthlet/impulse": "^0.1.0", "@synthlet/lfo": "^0.1.0", @@ -7888,7 +7897,7 @@ }, "packages/wavetable-oscillator": { "name": "@synthlet/wavetable-oscillator", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", diff --git a/package.json b/package.json index 9384295..64e1f2b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "api": "python scripts/export-api.py", "test": "jest --coverage", "test:ci": "npm run api && npm run build && npm run test", - "processors": "find ./packages -name 'processor.ts' -exec wc -m {} +" + "processors": "find ./packages -name 'processor.ts' -exec wc -m {} +", + "publish:prepare": "cp README.md packages/synthlet && cp packages/synthlet/CHANGELOG.md CHANGELOG.md" }, "packageManager": "npm@7.21.0" } diff --git a/packages/dattorro-reverb/CHANGELOG.md b/packages/dattorro-reverb/CHANGELOG.md new file mode 100644 index 0000000..fee5c4c --- /dev/null +++ b/packages/dattorro-reverb/CHANGELOG.md @@ -0,0 +1,5 @@ +# @synthlet/dattorro-reverb + +## 0.1.0 + +- Initial release diff --git a/packages/dattorro-reverb/README.md b/packages/dattorro-reverb/README.md new file mode 100644 index 0000000..51472b8 --- /dev/null +++ b/packages/dattorro-reverb/README.md @@ -0,0 +1,7 @@ +# @synthlet/dattorro-reverb + +> DattorroReverb + +Part of [Synthlet](https://github.com/danigb/synthlet) + +This is reverb is generated using Faust. All credits to Jakob Zerbian. diff --git a/packages/dattorro-reverb/package.json b/packages/dattorro-reverb/package.json new file mode 100644 index 0000000..e024740 --- /dev/null +++ b/packages/dattorro-reverb/package.json @@ -0,0 +1,36 @@ +{ + "name": "@synthlet/dattorro-reverb", + "version": "0.1.0", + "description": "Dattorro reverb as an audio worklet", + "keywords": [ + "reverb", + "dattorro", + "modular", + "synthesis", + "synthlet" + ], + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "author": "danigb@gmail.com", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + }, + "jest": { + "preset": "ts-jest" + }, + "scripts": { + "worklet": "esbuild src/worklet.ts --bundle --minify | sed -e 's/^/export const PROCESSOR = \\`/' -e 's/$/\\`;/' > src/processor.ts", + "lib": "tsup src/index.ts --sourcemap --dts --format esm,cjs", + "build": "npm run worklet && npm run lib" + } +} diff --git a/packages/dattorro-reverb/src/_worklet.ts b/packages/dattorro-reverb/src/_worklet.ts new file mode 100644 index 0000000..9a26891 --- /dev/null +++ b/packages/dattorro-reverb/src/_worklet.ts @@ -0,0 +1,119 @@ +// DON'T EDIT THIS FILE unless inside scripts/_worklet.ts +// use ./scripts/copy_files.ts to copy this file to the right place +// the goal is to avoid external dependencies on packages + +// A "Connector" is a function that takes an AudioContext and returns an AudioNode +// or an custom object with a connect method (that returns a disconnect method) +export type Connector = (context: AudioContext) => N; + +export type ParamInput = number | Connector | AudioNode; + +type CreateWorkletOptions = { + processorName: string; + paramNames: readonly string[]; + workletOptions: (params: Partial

) => AudioWorkletNodeOptions; + postCreate?: (node: N) => void; +}; + +export type Disposable = N & { dispose: () => void }; + +export function createWorkletConstructor< + N extends AudioWorkletNode, + P extends Record +>(options: CreateWorkletOptions) { + return ( + audioContext: AudioContext, + inputs: Partial

= {} + ): Disposable => { + const node = new AudioWorkletNode( + audioContext, + options.processorName, + options.workletOptions(inputs) + ) as N; + + (node as any).__PROCESSOR_NAME__ = options.processorName; + const connected = connectParams(node, options.paramNames, inputs); + options.postCreate?.(node); + return disposable(node, connected); + }; +} + +type ConnectedUnit = AudioNode | (() => void); + +export function connectParams( + node: any, + paramNames: readonly string[], + inputs: any +): ConnectedUnit[] { + const connected: ConnectedUnit[] = []; + + for (const paramName of paramNames) { + if (node.parameters) { + node[paramName] = node.parameters.get(paramName); + } + const param = node[paramName]; + if (!param) throw Error("Invalid param name: " + paramName); + const input = inputs[paramName]; + if (typeof input === "number") { + param.value = input; + } else if (input instanceof AudioNode) { + param.value = 0; + input.connect(param); + connected.push(input); + } else if (typeof input === "function") { + param.value = 0; + const source = input(node.context); + source.connect(param); + connected.push(source); + } + } + + return connected; +} + +export function disposable( + node: N, + dependencies?: ConnectedUnit[] +): Disposable { + let disposed = false; + return Object.assign(node, { + dispose() { + if (disposed) return; + disposed = true; + + node.disconnect(); + (node as any).port?.postMessage({ type: "DISPOSE" }); + if (!dependencies) return; + + while (dependencies.length) { + const conn = dependencies.pop(); + if (conn instanceof AudioNode) { + if (typeof (conn as any).dispose === "function") { + (conn as any).dispose?.(); + } else { + conn.disconnect(); + } + } else if (typeof conn === "function") { + conn(); + } + } + }, + }); +} + +export function createRegistrar(processorName: string, processor: string) { + return function (context: AudioContext): Promise { + const key = "__" + processorName + "__"; + if (key in context) return (context as any)[key]; + + if (!context.audioWorklet || !context.audioWorklet.addModule) { + throw Error("AudioWorklet not supported"); + } + + const blob = new Blob([processor], { type: "application/javascript" }); + const url = URL.createObjectURL(blob); + const promise = context.audioWorklet.addModule(url); + (context as any)[key] = promise; + return promise; + }; +} diff --git a/packages/dattorro-reverb/src/dsp.ts b/packages/dattorro-reverb/src/dsp.ts new file mode 100644 index 0000000..5355ada --- /dev/null +++ b/packages/dattorro-reverb/src/dsp.ts @@ -0,0 +1,189 @@ +export type UpdateFn = ReturnType["update"]; +export type ComputeFn = ReturnType["compute"]; + +// This is a dsp code generated from Faust to test if the Faust -> Rust -> Typescript conversion works +export function createDsp(sampleRate: number) { + let fConst0 = 44.1 / sampleRate; + let fConst1 = 1 - fConst0; + let fVslider0 = -6.0; + let fVslider1 = 0.0; + let fVslider2 = 0.7; + let fVslider3 = 0.625; + let fVslider4 = 0.625; + let fVslider5 = 0.7; + let fVslider6 = 0.625; + let fVslider7 = 0.625; + let fVslider8 = 0.625; + let IOTA0 = 0; + let fRec0 = [0.0, 0.0]; + let fRec1 = [0.0, 0.0]; + let fRec13 = [0.0, 0.0]; + let fRec12 = [0.0, 0.0, 0.0]; + let fRec14 = [0.0, 0.0]; + let fVec0 = new Array(256).fill(0.0); + let fRec10 = [0.0, 0.0]; + let fVec1 = new Array(128).fill(0.0); + let fRec8 = [0.0, 0.0]; + let fRec15 = [0.0, 0.0]; + let fVec2 = new Array(512).fill(0.0); + let fRec6 = [0.0, 0.0]; + let fVec3 = new Array(512).fill(0.0); + let fRec4 = [0.0, 0.0]; + let fRec16 = [0.0, 0.0]; + let fRec20 = [0.0, 0.0]; + let fRec23 = [0.0, 0.0]; + let fVec4 = new Array(1024).fill(0.0); + let fRec21 = [0.0, 0.0]; + let fVec5 = new Array(8192).fill(0.0); + let fRec19 = [0.0, 0.0]; + let fRec24 = [0.0, 0.0]; + let fVec6 = new Array(4096).fill(0.0); + let fRec17 = [0.0, 0.0]; + let fVec7 = new Array(4096).fill(0.0); + let fRec2 = [0.0, 0.0]; + let fVec8 = new Array(1024).fill(0.0); + let fRec28 = [0.0, 0.0]; + let fVec9 = new Array(8192).fill(0.0); + let fRec27 = [0.0, 0.0]; + let fVec10 = new Array(2048).fill(0.0); + let fRec25 = [0.0, 0.0]; + let fVec11 = new Array(2048).fill(0.0); + let fRec3 = [0.0, 0.0]; + + function update( + lpFilter: number, + diffussion1: number, + diffussion2: number, + feedback1: number, + feedback2: number, + decayRate: number, + damping: number, + dryWet: number, + volume: number + ) { + fVslider0 = lpFilter; + fVslider1 = diffussion1; + fVslider2 = diffussion2; + fVslider3 = feedback1; + fVslider4 = feedback2; + fVslider5 = decayRate; + fVslider6 = damping; + fVslider7 = dryWet; + fVslider8 = volume; + } + + function compute( + inputs: Float32Array[], + outputs: Float32Array[], + count: number + ) { + let inputs0 = inputs[0]; + let inputs1 = inputs.length === 1 ? inputs[0] : inputs[1]; + let outputs0 = outputs[0]; + let outputs1 = outputs[1]; + + let fSlow0 = fConst0 * Math.pow(10, 0.05 * fVslider0); + let fSlow1 = fConst0 * fVslider1; + let fSlow2 = fConst0 * fVslider2; + let fSlow3 = fConst0 * fVslider3; + let fSlow4 = fConst0 * fVslider4; + let fSlow5 = fConst0 * fVslider5; + let fSlow6 = fConst0 * fVslider6; + let fSlow7 = fConst0 * fVslider7; + let fSlow8 = fConst0 * fVslider8; + + for (let i = 0; i < count; i++) { + let input0 = inputs0[i]; + let input1 = inputs1[i]; + + fRec0[0] = fSlow0 + fConst1 * fRec0[1]; + let fTemp0 = input0; + fRec1[0] = fSlow1 + fConst1 * fRec1[1]; + let fTemp1 = fRec1[0] + 1.0; + let fTemp2 = 1.0 - 0.5 * fTemp1; + fRec13[0] = fSlow2 + fConst1 * fRec13[1]; + let fTemp3 = input1; + fRec12[0] = + (1.0 - fRec13[0]) * fRec12[2] + 0.5 * (fTemp0 + fTemp3) * fRec13[0]; + fRec14[0] = fSlow3 + fConst1 * fRec14[1]; + let fTemp4 = fRec12[0] - fRec14[0] * fRec10[1]; + fVec0[IOTA0 & 255] = fTemp4; + fRec10[0] = fVec0[(IOTA0 - 142) & 255]; + let fRec11 = fRec14[0] * fTemp4; + let fTemp5 = fRec11 + fRec10[1] - fRec14[0] * fRec8[1]; + fVec1[IOTA0 & 127] = fTemp5; + fRec8[0] = fVec1[(IOTA0 - 107) & 127]; + let fRec9 = fRec14[0] * fTemp5; + fRec15[0] = fSlow4 + fConst1 * fRec15[1]; + let fTemp6 = fRec9 + fRec8[1] - fRec15[0] * fRec6[1]; + fVec2[IOTA0 & 511] = fTemp6; + fRec6[0] = fVec2[(IOTA0 - 379) & 511]; + let fRec7 = fRec15[0] * fTemp6; + let fTemp7 = fRec7 + fRec6[1] - fRec15[0] * fRec4[1]; + fVec3[IOTA0 & 511] = fTemp7; + fRec4[0] = fVec3[(IOTA0 - 277) & 511]; + let fRec5 = fRec15[0] * fTemp7; + + fRec16[0] = fSlow5 + fConst1 * fRec16[1]; + fRec20[0] = fSlow6 + fConst1 * fRec20[1]; + let fTemp8 = 1.0 - fRec20[0]; + fRec23[0] = fSlow7 + fConst1 * fRec23[1]; + let fTemp9 = fRec23[0] * fRec21[1] + fRec3[1]; + fVec4[IOTA0 & 1023] = fTemp9; + fRec21[0] = fVec4[(IOTA0 - 908) & 1023]; + let fRec22 = -(fRec23[0] * fTemp9); + fVec5[IOTA0 & 8191] = fRec22 + fRec21[1]; + fRec19[0] = fRec20[0] * fRec19[1] + fTemp8 * fVec5[(IOTA0 - 4217) & 8191]; + fRec24[0] = fSlow8 + fConst1 * fRec24[1]; + let fTemp10 = fRec19[0] * fRec16[0] - fRec24[0] * fRec17[1]; + fVec6[IOTA0 & 4095] = fTemp10; + fRec17[0] = fVec6[(IOTA0 - 2656) & 4095]; + let fRec18 = fRec24[0] * fTemp10; + fVec7[IOTA0 & 4095] = fRec18 + fRec17[1]; + fRec2[0] = fRec5 + fRec16[0] * fVec7[(IOTA0 - 2656) & 4095] + fRec4[1]; + let fTemp11 = fRec23[0] * fRec28[1] + fRec2[1]; + fVec8[IOTA0 & 1023] = fTemp11; + fRec28[0] = fVec8[(IOTA0 - 672) & 1023]; + let fRec29 = -(fRec23[0] * fTemp11); + fVec9[IOTA0 & 8191] = fRec29 + fRec28[1]; + fRec27[0] = fRec20[0] * fRec27[1] + fTemp8 * fVec9[(IOTA0 - 4453) & 8191]; + let fTemp12 = fRec16[0] * fRec27[0] - fRec24[0] * fRec25[1]; + fVec10[IOTA0 & 2047] = fTemp12; + fRec25[0] = fVec10[(IOTA0 - 1800) & 2047]; + let fRec26 = fRec24[0] * fTemp12; + fVec11[IOTA0 & 2047] = fRec26 + fRec25[1]; + fRec3[0] = fRec5 + fRec4[1] + fRec16[0] * fVec11[(IOTA0 - 1800) & 2047]; + + outputs0[i] = fRec0[0] * (fTemp0 * fTemp2 + 0.5 * fTemp1 * fRec2[0]); + outputs1[i] = fRec0[0] * (fTemp3 * fTemp2 + 0.5 * fTemp1 * fRec3[0]); + + // Update history + fRec0[1] = fRec0[0]; + fRec1[1] = fRec1[0]; + fRec13[1] = fRec13[0]; + fRec12[2] = fRec12[1]; + fRec12[1] = fRec12[0]; + fRec14[1] = fRec14[0]; + IOTA0++; + fRec10[1] = fRec10[0]; + fRec8[1] = fRec8[0]; + fRec15[1] = fRec15[0]; + fRec6[1] = fRec6[0]; + fRec4[1] = fRec4[0]; + fRec16[1] = fRec16[0]; + fRec20[1] = fRec20[0]; + fRec23[1] = fRec23[0]; + fRec21[1] = fRec21[0]; + fRec19[1] = fRec19[0]; + fRec24[1] = fRec24[0]; + fRec17[1] = fRec17[0]; + fRec2[1] = fRec2[0]; + fRec28[1] = fRec28[0]; + fRec27[1] = fRec27[0]; + fRec25[1] = fRec25[0]; + fRec3[1] = fRec3[0]; + } + } + + return { update, compute }; +} diff --git a/packages/dattorro-reverb/src/index.ts b/packages/dattorro-reverb/src/index.ts new file mode 100644 index 0000000..e07d1a6 --- /dev/null +++ b/packages/dattorro-reverb/src/index.ts @@ -0,0 +1,59 @@ +import { + createRegistrar, + createWorkletConstructor, + ParamInput, +} from "./_worklet"; +import { PROCESSOR } from "./processor"; + +export const registerDattorroReverbWorklet = createRegistrar( + "DATTORRO_REVERB", + PROCESSOR +); + +export type DattorroReverbInputs = { + filter?: ParamInput; + inputDiffusion1?: ParamInput; + inputDiffusion2?: ParamInput; + decayDiffusion1?: ParamInput; + decayDiffusion2?: ParamInput; + decay?: ParamInput; + damping?: ParamInput; + // dryWet?: ParamInput; + // level?: ParamInput; +}; + +export type DattorroReverbWorkletNode = AudioWorkletNode & { + filter: AudioParam; + inputDiffusion1: AudioParam; + inputDiffusion2: AudioParam; + decayDiffusion1: AudioParam; + decayDiffusion2: AudioParam; + decay: AudioParam; + damping: AudioParam; + // dryWet: AudioParam; + // level: AudioParam; + dispose(): void; +}; + +export const DattorroReverb = createWorkletConstructor< + DattorroReverbWorkletNode, + DattorroReverbInputs +>({ + processorName: "DattorroReverbProcessor", + paramNames: [ + "filter", + "inputDiffusion1", + "inputDiffusion2", + "decayDiffusion1", + "decayDiffusion2", + "decay", + "damping", + // "dryWet", + // "level", + ], + workletOptions: () => ({ + numberOfInputs: 1, + numberOfOutputs: 1, + outputChannelCount: [2], + }), +}); diff --git a/packages/dattorro-reverb/src/processor.ts b/packages/dattorro-reverb/src/processor.ts new file mode 100644 index 0000000..0899ed0 --- /dev/null +++ b/packages/dattorro-reverb/src/processor.ts @@ -0,0 +1 @@ +export const PROCESSOR = `"use strict";(()=>{function V0(z){let t=44.1/z,f=1-t,l=-6,w=0,s=.7,B=.625,G=.625,H=.7,J=.625,K=.625,L=.625,e=0,p=[0,0],V=[0,0],m=[0,0],o=[0,0,0],r=[0,0],N=new Array(256).fill(0),A=[0,0],Q=new Array(128).fill(0),T=[0,0],c=[0,0],X=new Array(512).fill(0),h=[0,0],Y=new Array(512).fill(0),R=[0,0],n=[0,0],u=[0,0],i=[0,0],Z=new Array(1024).fill(0),b=[0,0],_=new Array(8192).fill(0),D=[0,0],a=[0,0],$=new Array(4096).fill(0),S=[0,0],e0=new Array(4096).fill(0),F=[0,0],t0=new Array(1024).fill(0),g=[0,0],l0=new Array(8192).fill(0),C=[0,0],f0=new Array(2048).fill(0),k=[0,0],r0=new Array(2048).fill(0),x=[0,0];function A0(y,O,P,U,W,v,I,j,E){l=y,w=O,s=P,B=U,G=W,H=v,J=I,K=j,L=E}function T0(y,O,P){let U=y[0],W=y.length===1?y[0]:y[1],v=O[0],I=O[1],j=t*Math.pow(10,.05*l),E=t*w,h0=t*s,b0=t*B,D0=t*G,S0=t*H,F0=t*J,g0=t*K,C0=t*L;for(let d=0;d{switch(l.data.type){case"DISPOSE":this.r=!1;break}}}process(t,f,l){this.u(l.filter[0],l.inputDiffusion1[0],l.inputDiffusion2[0],l.decayDiffusion1[0],l.decayDiffusion2[0],l.decay[0],l.damping[0],l.dryWet[0],l.level[0]);let w=t[0],s=f[0];return w.length===0||s.length===0?this.r:(this.c(w,s,s[0].length),this.r)}static get parameterDescriptors(){return[["filter",.7,0,1],["inputDiffusion1",.75,0,1],["inputDiffusion2",.625,0,1],["decayDiffusion1",.7,0,.999999],["decayDiffusion2",.5,0,.999999],["decay",.5,0,1],["damping",.25,0,1],["dryWet",1,-1,1],["level",0,0,1]].map(t=>new Object({name:t[0],defaultValue:t[1],minValue:t[2],maxValue:t[3],automationRate:"k-rate"}))}};registerProcessor("DattorroReverbProcessor",q);})();`; diff --git a/packages/dattorro-reverb/src/worklet.ts b/packages/dattorro-reverb/src/worklet.ts new file mode 100644 index 0000000..c3e9b97 --- /dev/null +++ b/packages/dattorro-reverb/src/worklet.ts @@ -0,0 +1,72 @@ +import { ComputeFn, createDsp, UpdateFn } from "./dsp"; + +export class DattorroReverbProcessor extends AudioWorkletProcessor { + r: boolean; // running + u: UpdateFn; + c: ComputeFn; + + constructor() { + super(); + this.r = true; + const { update, compute } = createDsp(sampleRate); + this.u = update; + this.c = compute; + + this.port.onmessage = (event) => { + switch (event.data.type) { + case "DISPOSE": + this.r = false; + break; + } + }; + } + + process(inputs: Float32Array[][], outputs: Float32Array[][], p: any) { + this.u( + p.filter[0], + p.inputDiffusion1[0], + p.inputDiffusion2[0], + p.decayDiffusion1[0], + p.decayDiffusion2[0], + p.decay[0], + p.damping[0], + p.dryWet[0], + p.level[0] + ); + + const in1 = inputs[0]; + const out1 = outputs[0]; + + if (in1.length === 0 || out1.length === 0) { + return this.r; + } + + this.c(in1, out1, out1[0].length); + return this.r; + } + + static get parameterDescriptors() { + return [ + ["filter", 0.7, 0, 1], + ["inputDiffusion1", 0.75, 0, 1], + ["inputDiffusion2", 0.625, 0, 1], + ["decayDiffusion1", 0.7, 0, 0.999999], + ["decayDiffusion2", 0.5, 0, 0.999999], + ["decay", 0.5, 0, 1], + ["damping", 0.25, 0, 1], + ["dryWet", 1, -1, 1], // -1 dry, 0 equal, 1 wet + ["level", 0.0, 0, 1], // decibels + ].map( + (x) => + new Object({ + name: x[0], + defaultValue: x[1], + minValue: x[2], + maxValue: x[3], + automationRate: "k-rate", + }) + ); + } +} + +registerProcessor("DattorroReverbProcessor", DattorroReverbProcessor); diff --git a/packages/synthlet/CHANGELOG.md b/packages/synthlet/CHANGELOG.md index f985df3..f1fe724 100644 --- a/packages/synthlet/CHANGELOG.md +++ b/packages/synthlet/CHANGELOG.md @@ -1,5 +1,12 @@ # synthlet +## 0.3.0 + +- Function `registerSynthlet` renamed to `registerAllWorklets` + +- Initial release of: + - @synthlet/dattorro-reverb@0.1.0 + ## 0.2.0 - Initial release of the following modules: @@ -20,4 +27,9 @@ ## 0.1.0 -Initial implementation +Initial implementation of: + +- ADSR +- White Noise +- State Variable Filter +- Wavetable Oscillator diff --git a/packages/synthlet/README.md b/packages/synthlet/README.md index cd4079b..1947bc9 100644 --- a/packages/synthlet/README.md +++ b/packages/synthlet/README.md @@ -2,6 +2,108 @@ [![npm version](https://img.shields.io/npm/v/synthlet)](https://www.npmjs.com/package/synthlet) -This is the _facade_ package that exports all other @synthlet packages just for convenience. +Collection of synth modules implemented as AudioWorklets. -Read [main readme](/README.md) for details. +```ts +import { + registerAllWorklets, + AdsrAmp, + StateVariableFilter, + StateVariableFilterType, + PolyblepOscillator +} from "synthlet"; + +const ac = new AudioContext(); +await registerAllWorklets(ac); + +// Simplest synth: Oscillator -> Filter -> Amplifier +const osc = PolyblepOscillator(ac, { frequency: 440 }); +const filter = StateVariableFilter(ac, { + type: StateVariableFilterType.LowPass + frequency: 4000, +}); +const amp = AdsrAmp(ac, { attack: 0.1, release: 0.5 }); +osc.connect(filter).connect(amp).connect(ac.destination); + +// Change parameters +osc.frequency.value = 1200; + +// Start sound +amp.gate.value = 1; + +// Stop sound +vca.gate.value = 0; +``` + +## Install + +Install `synthlet` to install all modules: + +```bash +npm i synthlet +``` + +Or each module individually: + +```bash +npm i @synthlet/adsr +``` + +## Documentation + +Documentation and examples are [here](https://danigb.github.io/synthlet/docs/quick-start) + +### Why? + +Mostly, because I want to learn dsp. + +### Is it a good idea to write dsp in Typescript? + +Probably not, but I've seen [a talk at WAC 2022](https://zenodo.org/records/6767468) about it that made me think it is possible (thanks to JS engine optimizations). + +### Should I use it? + +Just for fun and curiosity. If you want to make music, [Tone.js](https://github.com/Tonejs/Tone.js) is probably the way to go. + +If you want to deploy dsp modules to web in production, currently [Faust](https://faustdoc.grame.fr/) and [Cmajor](https://github.com/cmajor-lang/cmajor) or [Elementary Audio](https://github.com/elemaudio/elementary) are better alternatives. + +## References + +This library wouldn't be possible with all the people writing books, blog posts and awesome libraries... and making music! Thanks to all 💚 + +### Books + +- [Designing Synth Plugins 2nd Edition](https://www.willpirkle.com/) +- [Developing Virtual Synthesizers with VCV Rack](https://www.routledge.com/Developing-Virtual-Synthesizers-with-VCV-Rack/Gabrielli/p/book/9780367077730) +- [BasicSynth: Creating a Music Synthesizer in Software](http://basicsynth.com/) +- [Generating Sound and Organizing Time](https://cycling74.com/books/go) +- [Designing Audio FX Plugins 2nd Edition](https://www.willpirkle.com/) + +### Posts and papers + +- https://github.com/BillyDM/awesome-audio-dsp +- https://paulbatchelor.github.io/sndkit/algos/ +- https://www.musicdsp.org/ +- [Cytomic technical papers](https://cytomic.com/technical-papers/) specially famous for its [State Variable Filters](https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf) +- [Signalsmith Audio blog](https://signalsmith-audio.co.uk/writing/) +- [Valhalla DSP Blog](https://valhalladsp.com/category/learn/plugin-design-learn/) +- http://synthworks.eu/ - DIY Synthetizers +- [Karplus-Strong original paper](https://users.soe.ucsc.edu/~karplus/papers/digitar.pdf) + +### Open source repositories + +- https://github.com/VCVRack/Rack +- https://github.com/grame-cncm/faust +- https://github.com/SoundStacks/cmajor +- https://github.com/jd-13/WE-Core +- https://github.com/mhetrick/nonlinearcircuits +- https://github.com/timowest/analogue +- https://github.com/pichenettes/stmlib/tree/master/dsp + +### Other + +- [Wave Edit wavetable editor](https://waveeditonline.com/) + +## License + +MIT License diff --git a/packages/synthlet/package.json b/packages/synthlet/package.json index 671da19..42958f2 100644 --- a/packages/synthlet/package.json +++ b/packages/synthlet/package.json @@ -1,6 +1,6 @@ { "name": "synthlet", - "version": "0.2.0", + "version": "0.2.1", "description": "Modular synthesis in the browser", "keywords": [ "modular", @@ -25,7 +25,7 @@ "@synthlet/chorus-t": "^0.1.0", "@synthlet/clip-amp": "^0.1.0", "@synthlet/clock": "^0.1.0", - "@synthlet/drum8": "^0.0.0", + "@synthlet/dattorro-reverb": "^0.1.0", "@synthlet/euclid": "^0.1.0", "@synthlet/impulse": "^0.1.0", "@synthlet/lfo": "^0.1.0", diff --git a/packages/synthlet/src/index.ts b/packages/synthlet/src/index.ts index d78ac97..3720771 100644 --- a/packages/synthlet/src/index.ts +++ b/packages/synthlet/src/index.ts @@ -3,6 +3,7 @@ import { registerAdsrWorklet } from "@synthlet/adsr"; import { registerChorusTWorklet } from "@synthlet/chorus-t"; import { registerClipAmpWorklet } from "@synthlet/clip-amp"; import { registerClockWorklet } from "@synthlet/clock"; +import { registerDattorroReverbWorklet } from "@synthlet/dattorro-reverb"; import { registerEuclidWorklet } from "@synthlet/euclid"; import { registerImpulseWorklet } from "@synthlet/impulse"; import { registerLfoWorklet } from "@synthlet/lfo"; @@ -17,6 +18,7 @@ export * from "@synthlet/adsr"; export * from "@synthlet/chorus-t"; export * from "@synthlet/clip-amp"; export * from "@synthlet/clock"; +export * from "@synthlet/dattorro-reverb"; export * from "@synthlet/euclid"; export * from "@synthlet/impulse"; export * from "@synthlet/lfo"; @@ -32,13 +34,16 @@ export * from "./synths/drums"; export * from "./synths/mono"; export * from "./waa"; -export function registerSynthlet(context: AudioContext): Promise { +export function registerAllWorklets( + context: AudioContext +): Promise { return Promise.all([ registerAdWorklet(context), registerAdsrWorklet(context), registerChorusTWorklet(context), registerClipAmpWorklet(context), registerClockWorklet(context), + registerDattorroReverbWorklet(context), registerEuclidWorklet(context), registerImpulseWorklet(context), registerPolyblepOscillatorWorklet(context), diff --git a/site/.source/index.js b/site/.source/index.js index b0756d1..a2ff136 100644 --- a/site/.source/index.js +++ b/site/.source/index.js @@ -1,28 +1,29 @@ import { toRuntime } from "fumadocs-mdx" -import * as file_0 from "../content/docs/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_1 from "../content/docs/(effects)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_2 from "../content/docs/(modifiers)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_3 from "../content/docs/(sequencers)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_4 from "../content/docs/(sources)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_5 from "../content/docs/(modulators)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_6 from "../content/docs/dsl.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_7 from "../content/docs/guide.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_8 from "../content/docs/quick-start.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_9 from "../content/docs/synths.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_10 from "../content/docs/troubleshoo.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_11 from "../content/docs/(effects)/chorus-t.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_12 from "../content/docs/(modifiers)/clip-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_13 from "../content/docs/(modifiers)/state-variable-filter.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_14 from "../content/docs/(modifiers)/vca.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_15 from "../content/docs/(modulators)/ad.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_16 from "../content/docs/(modulators)/adsr.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_17 from "../content/docs/(modulators)/lfo.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_18 from "../content/docs/(modulators)/param.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_19 from "../content/docs/(sources)/impulse.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_20 from "../content/docs/(sources)/noise.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_21 from "../content/docs/(sources)/polyblep.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_22 from "../content/docs/(sources)/wavetable.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_23 from "../content/docs/(sequencers)/clock.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_24 from "../content/docs/(sequencers)/euclid.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -export const meta = [toRuntime("meta", file_0, {"path":"meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/meta.json"}),toRuntime("meta", file_1, {"path":"(effects)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/meta.json"}),toRuntime("meta", file_2, {"path":"(modifiers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/meta.json"}),toRuntime("meta", file_3, {"path":"(sequencers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/meta.json"}),toRuntime("meta", file_4, {"path":"(sources)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/meta.json"}),toRuntime("meta", file_5, {"path":"(modulators)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/meta.json"})] -export const docs = [toRuntime("doc", file_6, {"path":"dsl.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/dsl.mdx"}),toRuntime("doc", file_7, {"path":"guide.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/guide.mdx"}),toRuntime("doc", file_8, {"path":"quick-start.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/quick-start.mdx"}),toRuntime("doc", file_9, {"path":"synths.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/synths.mdx"}),toRuntime("doc", file_10, {"path":"troubleshoo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/troubleshoo.mdx"}),toRuntime("doc", file_11, {"path":"(effects)/chorus-t.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/chorus-t.mdx"}),toRuntime("doc", file_12, {"path":"(modifiers)/clip-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/clip-amp.mdx"}),toRuntime("doc", file_13, {"path":"(modifiers)/state-variable-filter.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/state-variable-filter.mdx"}),toRuntime("doc", file_14, {"path":"(modifiers)/vca.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/vca.mdx"}),toRuntime("doc", file_15, {"path":"(modulators)/ad.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/ad.mdx"}),toRuntime("doc", file_16, {"path":"(modulators)/adsr.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/adsr.mdx"}),toRuntime("doc", file_17, {"path":"(modulators)/lfo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/lfo.mdx"}),toRuntime("doc", file_18, {"path":"(modulators)/param.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/param.mdx"}),toRuntime("doc", file_19, {"path":"(sources)/impulse.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/impulse.mdx"}),toRuntime("doc", file_20, {"path":"(sources)/noise.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/noise.mdx"}),toRuntime("doc", file_21, {"path":"(sources)/polyblep.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/polyblep.mdx"}),toRuntime("doc", file_22, {"path":"(sources)/wavetable.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/wavetable.mdx"}),toRuntime("doc", file_23, {"path":"(sequencers)/clock.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/clock.mdx"}),toRuntime("doc", file_24, {"path":"(sequencers)/euclid.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/euclid.mdx"})] \ No newline at end of file +import * as file_0 from "../content/docs/dsl.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_1 from "../content/docs/guide.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_2 from "../content/docs/quick-start.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_3 from "../content/docs/synths.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_4 from "../content/docs/troubleshoo.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_5 from "../content/docs/(effects)/chorus-t.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_6 from "../content/docs/(effects)/dattorro.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_7 from "../content/docs/(sequencers)/clock.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_8 from "../content/docs/(sequencers)/euclid.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_9 from "../content/docs/(modifiers)/clip-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_10 from "../content/docs/(modifiers)/state-variable-filter.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_11 from "../content/docs/(modifiers)/vca.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_12 from "../content/docs/(modulators)/ad.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_13 from "../content/docs/(modulators)/adsr.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_14 from "../content/docs/(modulators)/lfo.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_15 from "../content/docs/(modulators)/param.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_16 from "../content/docs/(sources)/impulse.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_17 from "../content/docs/(sources)/noise.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_18 from "../content/docs/(sources)/polyblep.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_19 from "../content/docs/(sources)/wavetable.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_20 from "../content/docs/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_21 from "../content/docs/(effects)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_22 from "../content/docs/(modifiers)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_23 from "../content/docs/(modulators)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_24 from "../content/docs/(sequencers)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_25 from "../content/docs/(sources)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +export const docs = [toRuntime("doc", file_0, {"path":"dsl.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/dsl.mdx"}),toRuntime("doc", file_1, {"path":"guide.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/guide.mdx"}),toRuntime("doc", file_2, {"path":"quick-start.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/quick-start.mdx"}),toRuntime("doc", file_3, {"path":"synths.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/synths.mdx"}),toRuntime("doc", file_4, {"path":"troubleshoo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/troubleshoo.mdx"}),toRuntime("doc", file_5, {"path":"(effects)/chorus-t.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/chorus-t.mdx"}),toRuntime("doc", file_6, {"path":"(effects)/dattorro.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/dattorro.mdx"}),toRuntime("doc", file_7, {"path":"(sequencers)/clock.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/clock.mdx"}),toRuntime("doc", file_8, {"path":"(sequencers)/euclid.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/euclid.mdx"}),toRuntime("doc", file_9, {"path":"(modifiers)/clip-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/clip-amp.mdx"}),toRuntime("doc", file_10, {"path":"(modifiers)/state-variable-filter.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/state-variable-filter.mdx"}),toRuntime("doc", file_11, {"path":"(modifiers)/vca.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/vca.mdx"}),toRuntime("doc", file_12, {"path":"(modulators)/ad.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/ad.mdx"}),toRuntime("doc", file_13, {"path":"(modulators)/adsr.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/adsr.mdx"}),toRuntime("doc", file_14, {"path":"(modulators)/lfo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/lfo.mdx"}),toRuntime("doc", file_15, {"path":"(modulators)/param.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/param.mdx"}),toRuntime("doc", file_16, {"path":"(sources)/impulse.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/impulse.mdx"}),toRuntime("doc", file_17, {"path":"(sources)/noise.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/noise.mdx"}),toRuntime("doc", file_18, {"path":"(sources)/polyblep.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/polyblep.mdx"}),toRuntime("doc", file_19, {"path":"(sources)/wavetable.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/wavetable.mdx"})] +export const meta = [toRuntime("meta", file_20, {"path":"meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/meta.json"}),toRuntime("meta", file_21, {"path":"(effects)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/meta.json"}),toRuntime("meta", file_22, {"path":"(modifiers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/meta.json"}),toRuntime("meta", file_23, {"path":"(modulators)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/meta.json"}),toRuntime("meta", file_24, {"path":"(sequencers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/meta.json"}),toRuntime("meta", file_25, {"path":"(sources)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/meta.json"})] \ No newline at end of file diff --git a/site/app/audio-context.ts b/site/app/audio-context.ts index 2be5910..8d5174d 100644 --- a/site/app/audio-context.ts +++ b/site/app/audio-context.ts @@ -1,10 +1,10 @@ -import { registerSynthlet } from "synthlet"; +import { registerAllWorklets } from "synthlet"; let promise: Promise; export function createSynthAudioContext(): Promise { if (!promise) { - promise = registerSynthlet(new AudioContext()); + promise = registerAllWorklets(new AudioContext()); } return promise; } diff --git a/site/content/docs/(effects)/dattorro.mdx b/site/content/docs/(effects)/dattorro.mdx new file mode 100644 index 0000000..aaace3c --- /dev/null +++ b/site/content/docs/(effects)/dattorro.mdx @@ -0,0 +1,27 @@ +--- +title: Dattorro Reverb +description: A Dattorro reverb effect from Faust +--- + +import DattorroExample from "../../../examples/DattorroExample"; + +This is a port of the Dattorro reverb from Faust to the Web Audio API. All credits to Jakob Zerbian. + +```ts +import { ClaveDrum, DattorroReverb, registerWorklets } from "synthlet"; + +const audioContext = await registerWorklets(new AudioContext()); +const reverb = DattorroReverb(audioContext, { + mix: 0.5, + time: 0.5, + decay: 0.5, + lowpass: 20000, + highpass: 20, +}); + +const clave = ClaveDrum(audioContext); +clave.connect(reverb).connect(audioContext.destination); +clave.trigger = 1; +``` + + diff --git a/site/content/docs/(sequencers)/clock.mdx b/site/content/docs/(sequencers)/clock.mdx index e3e9f5b..f78c32c 100644 --- a/site/content/docs/(sequencers)/clock.mdx +++ b/site/content/docs/(sequencers)/clock.mdx @@ -24,10 +24,10 @@ const clock = Clock(audioContext, { bpm: 10 }); Trigger a kick with a clock: ```ts -import { registerSynthlet, KickDrum } from "synthlet"; +import { registerAllWorklets, KickDrum } from "synthlet"; const audioContext = new AudioContext(); -await registerSynthlet(audioContext); +await registerAllWorklets(audioContext); const clock = Clock(audioContext, { bpm: 120 }); const kick = createKickDrum(audioContext, { trigger: clock }); kick.connect(audioContext.destination); diff --git a/site/content/docs/(sequencers)/euclid.mdx b/site/content/docs/(sequencers)/euclid.mdx index 425916c..a6638a6 100644 --- a/site/content/docs/(sequencers)/euclid.mdx +++ b/site/content/docs/(sequencers)/euclid.mdx @@ -11,14 +11,14 @@ Generates an euclidean rhythm when connected to a clock: ```ts import { - registerSynthlet, + registerAllWorklets, ClockNode, EuclidNode, ClaveDrumNode, } from "synthlet"; const ac = new AudioContext(); -await registerSynthlet(ac); +await registerAllWorklets(ac); const clock = ClockNode(ac, { bpm: 60 }); const euclid = EuclidNode(ac, { steps: 16, beats: 4, clock }); diff --git a/site/content/docs/dsl.mdx b/site/content/docs/dsl.mdx index 37954fa..7d054e5 100644 --- a/site/content/docs/dsl.mdx +++ b/site/content/docs/dsl.mdx @@ -6,9 +6,9 @@ description: Using operators to create synths `synthlet` package exposes the audio modules with a set of operators that allow to create complex synths and comining them in a functional way. ```js -import { createOperators, registerSynthlet } from "synthlet"; +import { createOperators, registerAllWorklets } from "synthlet"; -const context = await registerSynthlet(new AudioContext()); +const context = await registerAllWorklets(new AudioContext()); const { osc, bq, param, amp } = createOperators(context); diff --git a/site/content/docs/quick-start.mdx b/site/content/docs/quick-start.mdx index c1a14c8..dfbf6c9 100644 --- a/site/content/docs/quick-start.mdx +++ b/site/content/docs/quick-start.mdx @@ -20,24 +20,24 @@ synthlet ## Register worklet processors -Before you can create any module, you need to register the worklet processors. You can do this by calling the `registerSynthlet` to register all modules at once: +Before you can create any module, you need to register the worklet processors. You can do this by calling the `registerAllWorklets` to register all modules at once: ```js -import { registerSynthlet } from "synthlet"; +import { registerAllWorklets } from "synthlet"; const audioContext = new AudioContext(); -await registerSynthlet(audioContext); +await registerAllWorklets(audioContext); ``` - `registerSynthlet` (and all register functions) are async, so ensure you await - for the result before creating any modules. + `registerAllWorklets` (and all register functions) are async, so ensure you + await for the result before creating any modules. -By convenience, `registerSynthlet` returns the `AudioContext` so you can write it more concise: +By convenience, `registerAllWorklets` returns the `AudioContext` so you can write it more concise: ```js -const audioContext = await registerSynthlet(new AudioContext()); +const audioContext = await registerAllWorklets(new AudioContext()); ``` ## Create the first synth @@ -50,13 +50,13 @@ Here's the full gist: ```js import { - registerSynthlet + registerAllWorklets PolyblepOscillator, StateVariableFilter, AdsrAmp, } from "synthlet"; -const ac = await registerSynthlet(new AudioContext()); +const ac = await registerAllWorklets(new AudioContext()); const osc = PolyblepOscillator(ac, {frequency: 440 }); const filter = StateVariableFilter(ac, { @@ -99,17 +99,23 @@ The `synthlet` package wraps the modules in a set of operators to create complex import { synthlet } from "synthlet"; // Register all modules and create the operators -const s = await synthlet(new AudioContext()); +const audioContext = await registerAllWorklets(new AudioContext()); +const s = getSynthlet(audioContext); -// Use the operators to create audio graph -const gate = s.param(audioContext); +// Create the parameters you want to control +const gate = s.param(); +const filterFreq = s.param(1500); +// Create and connect the modules const out = s.conn( s.polyblep.saw(440), - s.svf.lp(5000, 2), + s.svf.lp(filterFreq), s.amp.adsr(gate, 0.01, 0.1, 0.7, 0.3) ); +// Start the gate gate.value = 1; +// Change the filter frequency +filterFreq.value = 5000; ``` Read more about how to use the operators in the [operators guide](/docs/dsl). diff --git a/site/content/docs/synths.mdx b/site/content/docs/synths.mdx index 9647819..7d40236 100644 --- a/site/content/docs/synths.mdx +++ b/site/content/docs/synths.mdx @@ -14,9 +14,9 @@ Some synthesizers are provided for convenience. They are implemented by connecti A monophonic synthesizer with a single PolyBLEP oscillator and an ADSR envelope on the filter and amplifier. ```ts -import { registerSynthlet, MonoSynth } from "synthlet"; +import { registerAllWorklets, MonoSynth } from "synthlet"; -const ac = registerSynthlet(new AudioContext()); +const ac = registerAllWorklets(new AudioContext()); const synth = MonoSynth(ac); synth.gate.value = 1; @@ -44,9 +44,9 @@ synth.filterEnv.attack.value = 0.1; Several drum synthesizers are available, all with the same constructor: `Drum(audioContext, inputs)`: ```ts -import { registerSynthlet, KickDrum } from "synthlet"; +import { registerAllWorklets, KickDrum } from "synthlet"; -const ac = registerSynthlet(new AudioContext()); +const ac = registerAllWorklets(new AudioContext()); const kick = KickDrum(ac, { tone: 0.2, decay: 0.7 }); kick.trigger.value = 1; kick.tone.value = 0.5; diff --git a/site/examples/DattorroExample.tsx b/site/examples/DattorroExample.tsx new file mode 100644 index 0000000..70749b0 --- /dev/null +++ b/site/examples/DattorroExample.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { ClaveDrum, DattorroReverb } from "synthlet"; +import { + ExamplePane, + ModulePane, + TriggerButton, +} from "./components/ExamplePane"; +import { Slider } from "./components/Slider"; +import { useSynth } from "./useSynth"; + +function DattorroSynth(context: AudioContext) { + const clave = ClaveDrum(context); + const reverb = DattorroReverb(context); + reverb.connect(context.destination); + clave.connect(reverb); + + return Object.assign(clave, { reverb }); +} + +function Example() { + const synth = useSynth(DattorroSynth); + if (!synth) return null; + + return ( + <> + + + + + + + + + + + + + + + ); +} + +export default () => ( + + + +); diff --git a/site/examples/components/ExamplePane.tsx b/site/examples/components/ExamplePane.tsx index 27ac1f8..f74a84a 100644 --- a/site/examples/components/ExamplePane.tsx +++ b/site/examples/components/ExamplePane.tsx @@ -70,9 +70,15 @@ export function ModulePane({ ); } -export function TriggerButton({ trigger }: { trigger: { value: number } }) { +export function TriggerButton({ + trigger, + className, +}: { + className?: string; + trigger: { value: number }; +}) { return ( -

+