diff --git a/README.md b/README.md index a3b0c36..17e6504 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ This library wouldn't be possible with all the people writing books, blog posts - [Cmajor](https://github.com/SoundStacks/cmajor) - [VCVRack](https://github.com/VCVRack/Rack) - [The Synthesis ToolKit](https://github.com/thestk/stk) +- [Surge synth](https://github.com/surge-synthesizer/surge) +- [Surge Rust](https://github.com/klebs6/surge-rs) - https://github.com/jd-13/WE-Core - https://github.com/mhetrick/nonlinearcircuits - https://github.com/timowest/analogue diff --git a/package-lock.json b/package-lock.json index c0d3e32..cb8079e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2340,6 +2340,10 @@ "resolved": "packages/impulse", "link": true }, + "node_modules/@synthlet/karplus-strong": { + "resolved": "packages/karplus-strong", + "link": true + }, "node_modules/@synthlet/lfo": { "resolved": "packages/lfo", "link": true @@ -7764,17 +7768,18 @@ } }, "packages/chorus": { - "version": "0.0.0", + "name": "@synthlet/chorus", + "version": "0.1.0", "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.11", "jest": "^29.7.0", "ts-jest": "^29.1.1" } }, "packages/chorus-t": { "name": "@synthlet/chorus-t", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", @@ -7843,6 +7848,16 @@ "ts-jest": "^29.1.1" } }, + "packages/karplus-strong": { + "name": "@synthlet/karplus-strong", + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.11", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + } + }, "packages/lfo": { "name": "@synthlet/lfo", "version": "0.1.0", @@ -7875,7 +7890,7 @@ }, "packages/polyblep-oscillator": { "name": "@synthlet/polyblep-oscillator", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7885,7 +7900,7 @@ }, "packages/state-variable-filter": { "name": "@synthlet/state-variable-filter", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.11", @@ -7894,24 +7909,25 @@ } }, "packages/synthlet": { - "version": "0.4.0", + "version": "0.7.0", "license": "MIT", "dependencies": { "@synthlet/ad": "^0.1.0", "@synthlet/adsr": "^0.1.0", "@synthlet/arp": "^0.1.0", - "@synthlet/chorus": "^0.0.0", - "@synthlet/chorus-t": "^0.1.0", + "@synthlet/chorus": "^0.1.0", + "@synthlet/chorus-t": "^0.1.1", "@synthlet/clip-amp": "^0.1.0", "@synthlet/clock": "^0.1.0", "@synthlet/dattorro-reverb": "^0.1.0", "@synthlet/euclid": "^0.1.0", "@synthlet/impulse": "^0.1.0", + "@synthlet/karplus-strong": "^0.0.0", "@synthlet/lfo": "^0.1.0", "@synthlet/noise": "^0.1.0", "@synthlet/param": "^0.1.0", - "@synthlet/polyblep-oscillator": "^0.1.0", - "@synthlet/state-variable-filter": "^0.1.0", + "@synthlet/polyblep-oscillator": "^0.2.0", + "@synthlet/state-variable-filter": "^0.2.0", "@synthlet/wavetable-oscillator": "^0.1.0" } }, diff --git a/packages/karplus-strong/CHANGELOG.md b/packages/karplus-strong/CHANGELOG.md new file mode 100644 index 0000000..4493477 --- /dev/null +++ b/packages/karplus-strong/CHANGELOG.md @@ -0,0 +1,5 @@ +# @synthlet/karplus-strong-oscillator + +## 0.1.0 + +- Initial release diff --git a/packages/karplus-strong/README.md b/packages/karplus-strong/README.md new file mode 100644 index 0000000..63b7439 --- /dev/null +++ b/packages/karplus-strong/README.md @@ -0,0 +1,5 @@ +# @synthlet/karplus-strong-oscillator + +> An oscillator based on Karplus-Strong synthesis + +Part of [Synthlet](https://github.com/danigb/synthlet) diff --git a/packages/karplus-strong/package.json b/packages/karplus-strong/package.json new file mode 100644 index 0000000..df1d19f --- /dev/null +++ b/packages/karplus-strong/package.json @@ -0,0 +1,35 @@ +{ + "name": "@synthlet/karplus-strong", + "version": "0.0.0", + "description": "Karplus Strong generator audio worklet", + "keywords": [ + "karplus-strong-oscillator", + "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/karplus-strong/src/_worklet.ts b/packages/karplus-strong/src/_worklet.ts new file mode 100644 index 0000000..9a26891 --- /dev/null +++ b/packages/karplus-strong/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/karplus-strong/src/dsp.ts b/packages/karplus-strong/src/dsp.ts new file mode 100644 index 0000000..3b9bd33 --- /dev/null +++ b/packages/karplus-strong/src/dsp.ts @@ -0,0 +1,79 @@ +export function createKS(sampleRate: number, minFrequency: number) { + const targetAmplitude = 0.001; // Amplitude decays to 0.1% of initial value + const maxDelayLineLength = Math.ceil(sampleRate / minFrequency) + 2; // Extra samples for interpolation + + const delayLine = new Float32Array(maxDelayLineLength); + + let delayInSamples = maxDelayLineLength - 2; + let writeIndex = 0; + + let isPlaying = false; + let prevTrigger = 0; + + return ( + output: Float32Array, + trigger: number, + frequency: number, + decay: number + ) => { + const outputLength = output.length; + + const decayTimeInSamples = 0.1 * decay * sampleRate; + const filterCoefficient = Math.pow(targetAmplitude, 1 / decayTimeInSamples); + + if (trigger >= 1 && prevTrigger < 0.9) { + // Some hysterisis to avoid double triggering + delayInSamples = sampleRate / frequency; + delayInSamples = Math.min( + Math.max(delayInSamples, 1), + maxDelayLineLength - 2 + ); + + for (let i = 0; i < maxDelayLineLength; i++) { + delayLine[i] = Math.random() * 2 - 1; + } + writeIndex = 0; + isPlaying = true; + } + prevTrigger = trigger; + + if (isPlaying) { + for (let i = 0; i < outputLength; i++) { + let readIndex = writeIndex - delayInSamples; + if (readIndex < 0) { + readIndex += maxDelayLineLength; + } + + const readIndexInt = Math.floor(readIndex); + const frac = readIndex - readIndexInt; + + // Wrap around the delay line + const index0 = readIndexInt % maxDelayLineLength; + const index1 = (readIndexInt + 1) % maxDelayLineLength; + + // Linear interpolation between two samples + const sample0 = delayLine[index0]; + const sample1 = delayLine[index1]; + const currentSample = sample0 + frac * (sample1 - sample0); + + const nextSample = filterCoefficient * currentSample; + delayLine[writeIndex] = nextSample; + output[i] = currentSample; + + writeIndex = (writeIndex + 1) % maxDelayLineLength; + + // Stop playing if the signal has decayed below a threshold + if (Math.abs(currentSample) < 1e-8) { + isPlaying = false; + for (let j = i + 1; j < outputLength; j++) { + output[j] = 0; + } + break; + } + } + } else { + // Output silence when not playing + output.fill(0); + } + }; +} diff --git a/packages/karplus-strong/src/index.ts b/packages/karplus-strong/src/index.ts new file mode 100644 index 0000000..bbd7344 --- /dev/null +++ b/packages/karplus-strong/src/index.ts @@ -0,0 +1,36 @@ +import { + createRegistrar, + createWorkletConstructor, + ParamInput, +} from "./_worklet"; +import { PROCESSOR } from "./processor"; + +export const registerKarplusStrongWorklet = createRegistrar( + "KS-OSC", + PROCESSOR +); + +export type KarplusStrongInputs = { + trigger: ParamInput; + frequency: ParamInput; + decay: ParamInput; +}; + +export type KarplusStrongWorkletNode = AudioWorkletNode & { + trigger: AudioParam; + frequency: AudioParam; + decay: AudioParam; + dispose(): void; +}; + +export const KarplusStrong = createWorkletConstructor< + KarplusStrongWorkletNode, + KarplusStrongInputs +>({ + processorName: "KsProcessor", + paramNames: ["trigger", "frequency", "decay"], + workletOptions: () => ({ + numberOfInputs: 0, + numberOfOutputs: 1, + }), +}); diff --git a/packages/karplus-strong/src/processor.ts b/packages/karplus-strong/src/processor.ts new file mode 100644 index 0000000..7cb29d8 --- /dev/null +++ b/packages/karplus-strong/src/processor.ts @@ -0,0 +1 @@ +export const PROCESSOR = `"use strict";(()=>{function b(o,n){let e=Math.ceil(o/n)+2,t=new Float32Array(e),i=e-2,a=0,f=!1,h=0;return(c,y,S,A)=>{let g=c.length,I=.1*A*o,M=Math.pow(.001,1/I);if(y>=1&&h<.9){i=o/S,i=Math.min(Math.max(i,1),e-2);for(let r=0;r{switch(n.data.type){case"DISPOSE":this.r=!1;break}}}process(n,s,e){let t=s[0][0];return this.g(t,e.trigger[0],e.frequency[0],e.decay[0]),this.r}static get parameterDescriptors(){return[["trigger",0,0,1],["frequency",440,20,2e4],["decay",.1,.01,5]].map(([n,s,e,t])=>({name:n,defaultValue:s,minValue:e,maxValue:t,automationRate:"k-rate"}))}};registerProcessor("KsProcessor",d);})();`; diff --git a/packages/karplus-strong/src/worklet.ts b/packages/karplus-strong/src/worklet.ts new file mode 100644 index 0000000..ed0361c --- /dev/null +++ b/packages/karplus-strong/src/worklet.ts @@ -0,0 +1,42 @@ +import { createKS } from "./dsp"; + +export class KsProcessor extends AudioWorkletProcessor { + r: boolean; // running + g: ReturnType; + + constructor() { + super(); + this.r = true; + this.g = createKS(sampleRate, 100); + this.port.onmessage = (event) => { + switch (event.data.type) { + case "DISPOSE": + this.r = false; + break; + } + }; + } + + process(inputs: Float32Array[][], outputs: Float32Array[][], params: any) { + const output = outputs[0][0]; + this.g(output, params.trigger[0], params.frequency[0], params.decay[0]); + + return this.r; + } + + static get parameterDescriptors() { + return [ + ["trigger", 0, 0, 1], + ["frequency", 440, 20, 20000], + ["decay", 0.1, 0.01, 5], + ].map(([name, defaultValue, minValue, maxValue]) => ({ + name, + defaultValue, + minValue, + maxValue, + automationRate: "k-rate", + })); + } +} + +registerProcessor("KsProcessor", KsProcessor); diff --git a/packages/synthlet/package.json b/packages/synthlet/package.json index b135a83..695f680 100644 --- a/packages/synthlet/package.json +++ b/packages/synthlet/package.json @@ -30,6 +30,7 @@ "@synthlet/dattorro-reverb": "^0.1.0", "@synthlet/euclid": "^0.1.0", "@synthlet/impulse": "^0.1.0", + "@synthlet/karplus-strong": "^0.0.0", "@synthlet/lfo": "^0.1.0", "@synthlet/noise": "^0.1.0", "@synthlet/param": "^0.1.0", diff --git a/packages/synthlet/src/index.ts b/packages/synthlet/src/index.ts index 464fde0..9cd0809 100644 --- a/packages/synthlet/src/index.ts +++ b/packages/synthlet/src/index.ts @@ -8,6 +8,7 @@ import { registerClockWorklet } from "@synthlet/clock"; import { registerDattorroReverbWorklet } from "@synthlet/dattorro-reverb"; import { registerEuclidWorklet } from "@synthlet/euclid"; import { registerImpulseWorklet } from "@synthlet/impulse"; +import { registerKarplusStrongWorklet } from "@synthlet/karplus-strong"; import { registerLfoWorklet } from "@synthlet/lfo"; import { registerNoiseWorklet } from "@synthlet/noise"; import { registerParamWorklet } from "@synthlet/param"; @@ -25,6 +26,7 @@ export * from "@synthlet/clock"; export * from "@synthlet/dattorro-reverb"; export * from "@synthlet/euclid"; export * from "@synthlet/impulse"; +export * from "@synthlet/karplus-strong"; export * from "@synthlet/lfo"; export * from "@synthlet/noise"; export * from "@synthlet/param"; @@ -52,6 +54,7 @@ export function registerAllWorklets( registerDattorroReverbWorklet(context), registerEuclidWorklet(context), registerImpulseWorklet(context), + registerKarplusStrongWorklet(context), registerPolyblepOscillatorWorklet(context), registerLfoWorklet(context), registerNoiseWorklet(context), diff --git a/site/.source/index.js b/site/.source/index.js index 2b0934b..157caca 100644 --- a/site/.source/index.js +++ b/site/.source/index.js @@ -1,22 +1,22 @@ 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/(modifiers)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_2 from "../content/docs/(modulators)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_3 from "../content/docs/(sources)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_4 from "../content/docs/(effects)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_5 from "../content/docs/(sequencers)/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/(modulators)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_4 from "../content/docs/(sequencers)/meta.json?collection=meta&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_5 from "../content/docs/(sources)/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/(modifiers)/ad-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_12 from "../content/docs/(modifiers)/adsr-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_13 from "../content/docs/(modifiers)/clip-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_14 from "../content/docs/(modifiers)/state-variable-filter.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_15 from "../content/docs/(effects)/chorus-t.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_16 from "../content/docs/(effects)/chorus.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_17 from "../content/docs/(effects)/dattorro.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/(effects)/chorus.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_13 from "../content/docs/(effects)/dattorro.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_14 from "../content/docs/(modifiers)/ad-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_15 from "../content/docs/(modifiers)/adsr-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_16 from "../content/docs/(modifiers)/clip-amp.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_17 from "../content/docs/(modifiers)/state-variable-filter.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" import * as file_18 from "../content/docs/(modulators)/ad.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" import * as file_19 from "../content/docs/(modulators)/adsr.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" import * as file_20 from "../content/docs/(modulators)/lfo.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" @@ -25,8 +25,9 @@ import * as file_22 from "../content/docs/(sequencers)/arp.mdx?collection=docs&h 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" import * as file_25 from "../content/docs/(sources)/impulse.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_26 from "../content/docs/(sources)/noise.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_27 from "../content/docs/(sources)/polyblep.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" -import * as file_28 from "../content/docs/(sources)/wavetable.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":"(modifiers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/meta.json"}),toRuntime("meta", file_2, {"path":"(modulators)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/meta.json"}),toRuntime("meta", file_3, {"path":"(sources)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/meta.json"}),toRuntime("meta", file_4, {"path":"(effects)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/meta.json"}),toRuntime("meta", file_5, {"path":"(sequencers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/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":"(modifiers)/ad-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/ad-amp.mdx"}),toRuntime("doc", file_12, {"path":"(modifiers)/adsr-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/adsr-amp.mdx"}),toRuntime("doc", file_13, {"path":"(modifiers)/clip-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/clip-amp.mdx"}),toRuntime("doc", file_14, {"path":"(modifiers)/state-variable-filter.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/state-variable-filter.mdx"}),toRuntime("doc", file_15, {"path":"(effects)/chorus-t.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/chorus-t.mdx"}),toRuntime("doc", file_16, {"path":"(effects)/chorus.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/chorus.mdx"}),toRuntime("doc", file_17, {"path":"(effects)/dattorro.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/dattorro.mdx"}),toRuntime("doc", file_18, {"path":"(modulators)/ad.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/ad.mdx"}),toRuntime("doc", file_19, {"path":"(modulators)/adsr.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/adsr.mdx"}),toRuntime("doc", file_20, {"path":"(modulators)/lfo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/lfo.mdx"}),toRuntime("doc", file_21, {"path":"(modulators)/param.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/param.mdx"}),toRuntime("doc", file_22, {"path":"(sequencers)/arp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/arp.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"}),toRuntime("doc", file_25, {"path":"(sources)/impulse.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/impulse.mdx"}),toRuntime("doc", file_26, {"path":"(sources)/noise.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/noise.mdx"}),toRuntime("doc", file_27, {"path":"(sources)/polyblep.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/polyblep.mdx"}),toRuntime("doc", file_28, {"path":"(sources)/wavetable.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/wavetable.mdx"})] \ No newline at end of file +import * as file_26 from "../content/docs/(sources)/ks.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_27 from "../content/docs/(sources)/noise.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_28 from "../content/docs/(sources)/polyblep.mdx?collection=docs&hash=a0e5c83919940bc930420dd0f9f7d68e8c5dbe7f6983d8b3ed42ebccd021e4f6" +import * as file_29 from "../content/docs/(sources)/wavetable.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":"(modulators)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/meta.json"}),toRuntime("meta", file_4, {"path":"(sequencers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/meta.json"}),toRuntime("meta", file_5, {"path":"(sources)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/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":"(effects)/chorus.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/chorus.mdx"}),toRuntime("doc", file_13, {"path":"(effects)/dattorro.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(effects)/dattorro.mdx"}),toRuntime("doc", file_14, {"path":"(modifiers)/ad-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/ad-amp.mdx"}),toRuntime("doc", file_15, {"path":"(modifiers)/adsr-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/adsr-amp.mdx"}),toRuntime("doc", file_16, {"path":"(modifiers)/clip-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/clip-amp.mdx"}),toRuntime("doc", file_17, {"path":"(modifiers)/state-variable-filter.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modifiers)/state-variable-filter.mdx"}),toRuntime("doc", file_18, {"path":"(modulators)/ad.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/ad.mdx"}),toRuntime("doc", file_19, {"path":"(modulators)/adsr.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/adsr.mdx"}),toRuntime("doc", file_20, {"path":"(modulators)/lfo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/lfo.mdx"}),toRuntime("doc", file_21, {"path":"(modulators)/param.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(modulators)/param.mdx"}),toRuntime("doc", file_22, {"path":"(sequencers)/arp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sequencers)/arp.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"}),toRuntime("doc", file_25, {"path":"(sources)/impulse.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/impulse.mdx"}),toRuntime("doc", file_26, {"path":"(sources)/ks.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/ks.mdx"}),toRuntime("doc", file_27, {"path":"(sources)/noise.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/noise.mdx"}),toRuntime("doc", file_28, {"path":"(sources)/polyblep.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/polyblep.mdx"}),toRuntime("doc", file_29, {"path":"(sources)/wavetable.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/site/content/docs/(sources)/wavetable.mdx"})] \ No newline at end of file diff --git a/site/content/docs/(sources)/ks.mdx b/site/content/docs/(sources)/ks.mdx new file mode 100644 index 0000000..28e595e --- /dev/null +++ b/site/content/docs/(sources)/ks.mdx @@ -0,0 +1,31 @@ +--- +title: Karplus-Strong +description: A Karplus-Strong source node +--- + +`@synthlet/karplus-strong` + +import KsExample from "../../../examples/KsExample"; + +A (currently) very simple Karplus-Strong source node: + +```ts +import { registerKarplusStrongWorklet, KarplusStrong } from "synthlet"; + +const audioContext = new AudioContext(); +await registerKarplusStrongWorklet(audioContext); + +const osc = KarplusStrong(audioContext, {}); +osc.connect(audioContext.destination); + +// Trigger the source +osc.trigger.value = 1; +``` + + + +## Parameters + +- `trigger`: A trigger to start the sound (1 means start) +- `frequency`: The frequency of the generated sound. +- `decay`: The decay time of the generated sound. diff --git a/site/content/docs/(sources)/meta.json b/site/content/docs/(sources)/meta.json index 0f35d2e..13f8bc8 100644 --- a/site/content/docs/(sources)/meta.json +++ b/site/content/docs/(sources)/meta.json @@ -1,5 +1,4 @@ { "title": "Audio Sources", - "defaultOpen": true, - "pages": ["wavetable", "polyblep", "noise", "impulse"] + "defaultOpen": true } diff --git a/site/examples/KsExample.tsx b/site/examples/KsExample.tsx new file mode 100644 index 0000000..0cf6c4d --- /dev/null +++ b/site/examples/KsExample.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useState } from "react"; +import { ConnSerial, Gain, NoiseType, Param } from "synthlet"; +import { KarplusStrong } from "../../packages/karplus-strong/src"; +import { ExamplePane, TriggerButton } from "./components/ExamplePane"; +import { Slider } from "./components/Slider"; +import { useSynth } from "./useSynth"; + +function createSynth(ac: AudioContext) { + const trigger = Param.input(ac); + const volume = Param.db(ac, -24); + const ks = KarplusStrong(ac, { trigger }); + + return Object.assign(ConnSerial([ks, Gain.val(ac, volume)]), { + ks, + volume: volume.input, + trigger: trigger.input, + }); +} + +function Example() { + const [currentNoise, setCurrentNoise] = useState(NoiseType.White); + const synth = useSynth(createSynth); + if (!synth) return null; + + return ( +

+ + +
+ +
+ +
+ ); +} + +export default () => ( + + + +); diff --git a/site/examples/NoiseExample.tsx b/site/examples/NoiseExample.tsx index 395bf2c..b9ff3d0 100644 --- a/site/examples/NoiseExample.tsx +++ b/site/examples/NoiseExample.tsx @@ -1,24 +1,24 @@ "use client"; import { useState } from "react"; -import { getSynthlet, NoiseType } from "synthlet"; +import { ConnSerial, Gain, Noise, NoiseType, Param } from "synthlet"; import { ExamplePane } from "./components/ExamplePane"; import { Slider } from "./components/Slider"; import { useSynth } from "./useSynth"; -function NoiseSynth(context: AudioContext) { - const s = getSynthlet(context); - const volume = s.param.db(-100); - const noiseType = s.param(NoiseType.White); - return s.withParams( - s.conn.serial(s.noise({ type: noiseType }), s.amp(volume)), - { volume, noiseType } - ); +function createSynth(ac: AudioContext) { + const volume = Param.db(ac, -24); + const noise = Noise(ac, { type: NoiseType.White }); + const amp = Gain.val(ac, volume); + return Object.assign(ConnSerial([noise, amp]), { + noise, + volume: volume.input, + }); } function Example() { const [currentNoise, setCurrentNoise] = useState(NoiseType.White); - const synth = useSynth(NoiseSynth); + const synth = useSynth(createSynth); if (!synth) return null; return ( @@ -29,7 +29,7 @@ function Example() { onChange={(e) => { const type = parseInt(e.target.value); setCurrentNoise(type); - synth.noiseType.value = type; + synth.noise.type.value = type; }} >