diff --git a/CHANGELOG.md b/CHANGELOG.md index a9259d8..362d93a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ # synthlet +## 0.11.0 + +- Granite effect module `@synthlet/granite` + +## 0.10.0 + +- VirtualAnalogFilter effect module `@synthlet/virtual-analog-filter` + ## 0.9.0 -- New ReverbDelay effect `@synthlet/reverb-delay` +- ReverbDelay effect module `@synthlet/reverb-delay` ## 0.8.0 -- New KarplusStrong audio source `@synthlet/karplus-strong` +- KarplusStrong source module `@synthlet/karplus-strong` ## 0.7.0 diff --git a/package-lock.json b/package-lock.json index 523b042..582357b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2044,6 +2044,10 @@ "resolved": "packages/euclid", "link": true }, + "node_modules/@synthlet/granite": { + "resolved": "packages/granite", + "link": true + }, "node_modules/@synthlet/impulse": { "resolved": "packages/impulse", "link": true @@ -6506,6 +6510,7 @@ } }, "packages/ad": { + "name": "@synthlet/ad", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6515,6 +6520,7 @@ } }, "packages/adsr": { + "name": "@synthlet/adsr", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6524,6 +6530,7 @@ } }, "packages/arp": { + "name": "@synthlet/arp", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6533,6 +6540,7 @@ } }, "packages/chorus": { + "name": "@synthlet/chorus", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6542,6 +6550,7 @@ } }, "packages/chorus-t": { + "name": "@synthlet/chorus-t", "version": "0.1.1", "license": "MIT", "devDependencies": { @@ -6551,6 +6560,7 @@ } }, "packages/clip-amp": { + "name": "@synthlet/clip-amp", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6560,6 +6570,7 @@ } }, "packages/clock": { + "name": "@synthlet/clock", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6569,6 +6580,7 @@ } }, "packages/dattorro-reverb": { + "name": "@synthlet/dattorro-reverb", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6578,6 +6590,16 @@ } }, "packages/euclid": { + "name": "@synthlet/euclid", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.13", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + } + }, + "packages/granite": { "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6587,6 +6609,7 @@ } }, "packages/impulse": { + "name": "@synthlet/impulse", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6596,6 +6619,7 @@ } }, "packages/karplus-strong": { + "name": "@synthlet/karplus-strong", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6605,6 +6629,7 @@ } }, "packages/lfo": { + "name": "@synthlet/lfo", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6614,6 +6639,7 @@ } }, "packages/noise": { + "name": "@synthlet/noise", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6623,6 +6649,7 @@ } }, "packages/param": { + "name": "@synthlet/param", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6632,6 +6659,7 @@ } }, "packages/polyblep-oscillator": { + "name": "@synthlet/polyblep-oscillator", "version": "0.2.0", "license": "MIT", "devDependencies": { @@ -6641,6 +6669,7 @@ } }, "packages/reverb-delay": { + "name": "@synthlet/reverb-delay", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6650,6 +6679,7 @@ } }, "packages/state-variable-filter": { + "name": "@synthlet/state-variable-filter", "version": "0.2.0", "license": "MIT", "devDependencies": { @@ -6671,6 +6701,7 @@ "@synthlet/clock": "^0.1.0", "@synthlet/dattorro-reverb": "^0.1.0", "@synthlet/euclid": "^0.1.0", + "@synthlet/granite": "^0.1.0", "@synthlet/impulse": "^0.1.0", "@synthlet/karplus-strong": "^0.1.0", "@synthlet/lfo": "^0.1.0", @@ -6684,6 +6715,7 @@ } }, "packages/virtual-analog-filter": { + "name": "@synthlet/virtual-analog-filter", "version": "0.1.0", "license": "MIT", "devDependencies": { @@ -6693,6 +6725,7 @@ } }, "packages/wavetable-oscillator": { + "name": "@synthlet/wavetable-oscillator", "version": "0.1.0", "license": "MIT", "devDependencies": { diff --git a/packages/granite/CHANGELOG.md b/packages/granite/CHANGELOG.md new file mode 100644 index 0000000..3bcfde6 --- /dev/null +++ b/packages/granite/CHANGELOG.md @@ -0,0 +1,5 @@ +# @synthlet/granite + +# 0.1.0 + +Initial release diff --git a/packages/granite/README.md b/packages/granite/README.md new file mode 100644 index 0000000..ea4c5d8 --- /dev/null +++ b/packages/granite/README.md @@ -0,0 +1,5 @@ +# @synthlet/granite + +> Granular processor + +Part of [Synthlet](https://github.com/danigb/synthlet) diff --git a/packages/granite/package.json b/packages/granite/package.json new file mode 100644 index 0000000..2dc9896 --- /dev/null +++ b/packages/granite/package.json @@ -0,0 +1,34 @@ +{ + "name": "@synthlet/granite", + "version": "0.1.0", + "description": "Granular processor worklet", + "keywords": [ + "granular", + "processor", + "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.13", + "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/granite/src/_worklet.ts b/packages/granite/src/_worklet.ts new file mode 100644 index 0000000..9a26891 --- /dev/null +++ b/packages/granite/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/granite/src/dsp.ts b/packages/granite/src/dsp.ts new file mode 100644 index 0000000..b16249a --- /dev/null +++ b/packages/granite/src/dsp.ts @@ -0,0 +1,205 @@ +export type UpdateFn = ReturnType["update"]; +export type ComputeFn = ReturnType["compute"]; + +export function createDsp(sampleRate: number) { + const GRAIN_DURATION_MS = 200; + const NUMBER_OF_GRAINS = 16; + + let wet = 0; + let freq = 0; + let freqPhasorInc = 0; + let freqPhasor = 0; + let density = 0; + let densityPhasorInc = 0; + let densityPhasor = 0; + let spread = 0; + + let currentGrain = 0; + const grainLength = Math.floor(sampleRate * (GRAIN_DURATION_MS / 1000)); + const window = createWindow(grainLength); + const grains = Array.from( + { length: NUMBER_OF_GRAINS }, + () => new Grain(grainLength) + ); + + function update( + wetParam: number, + freqParam: number, + densityParam: number, + spreadParam: number + ) { + wet = wetParam; + freq = freqParam; + density = densityParam; + spread = spreadParam; + freqPhasorInc = freq / sampleRate; + densityPhasorInc = density / sampleRate; + } + + function compute( + inputs: Float32Array[], + outputs: Float32Array[], + count: number + ) { + const input = inputs[0]; + const outLeft = outputs[0]; + const outRight = outputs[1]; + + // Update the read speed phasor and trigger grain read + freqPhasor += freqPhasorInc * count; + if (freqPhasor > 1) { + freqPhasor -= 1; + grains[currentGrain].startRead(); + currentGrain = (currentGrain + 1) % NUMBER_OF_GRAINS; + } + + // Update the write speed phasor + densityPhasor += densityPhasorInc * count; + if (densityPhasor > 1) { + densityPhasor -= 1; + const randomIndex = + Math.floor(Math.random() * NUMBER_OF_GRAINS) % NUMBER_OF_GRAINS; + const grain = grains[randomIndex]; + if (grain.startWrite()) { + grain.pan(Math.random() * spread * 2 - 1); + grain.filter(Math.random() * 2000); + } + } + + // Read and write the grains + for (let i = 0; i < NUMBER_OF_GRAINS; i++) { + grains[i].read(input, window); + grains[i].write(wet, outLeft, outRight); + } + + // Add the dry signal to the output + const dry = 1 - wet; + for (let i = 0; i < count; i++) { + outLeft[i] += dry * input[i]; + outRight[i] += dry * input[i]; + } + } + + return { + update, + compute, + }; + + function createWindow(length: number) { + const window = new Float32Array(length); + for (let i = 0; i < length; i++) { + window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (length - 1))); + } + + let sum = 0; + for (let i = 0; i < length; i++) { + sum += window[i]; + } + const normalizationFactor = length / sum; + + for (let i = 0; i < length; i++) { + window[i] *= normalizationFactor; + } + + return window; + } +} + +function createHiPassFilter(sampleRate: number) { + let cutoffFrequency: number = 0; + let alpha: number = 0; + let previousInput: number = 0; + let previousOutput: number = 0; + + function freq(speed: number): void { + cutoffFrequency = speed; + const rc = 1 / (2 * Math.PI * cutoffFrequency); + alpha = rc / (rc + 1 / sampleRate); + } + + function process(buffer: Float32Array, length: number) { + for (let i = 0; i < length; i++) { + const input = buffer[i]; + const output = alpha * (previousOutput + input - previousInput); + buffer[i] = output; + previousInput = input; + previousOutput = output; + } + } + freq(1000); + return { + freq, + process, + }; +} + +class Grain { + private buffer: Float32Array; + private readSamples = 0; + private writeSamples = 0; + private leftGain = 0; + private rightGain = 0; + private readPosition = 0; + private writePosition = 0; + private hipass = createHiPassFilter(sampleRate); + + constructor(grainLength: number) { + this.buffer = new Float32Array(grainLength); + } + + startRead() { + if (this.readSamples > 0 || this.writeSamples > 0) return; + this.readSamples = this.buffer.length; + this.readPosition = 0; + } + + startWrite() { + if (this.writeSamples > 0) { + return false; + } + this.writeSamples = this.buffer.length; + this.writePosition = 0; + return true; + } + + pan(pan: number) { + this.leftGain = Math.cos((Math.PI / 4) * (pan + 1)); + this.rightGain = Math.sin((Math.PI / 4) * (pan + 1)); + } + + filter(speed: number) { + this.hipass.freq(speed); + } + + read(input: Float32Array, window: Float32Array) { + if (this.readSamples === 0) return; + const samples = Math.min(this.readSamples, input.length); + + for (let i = 0; i < samples; i++) { + this.buffer[this.readPosition] = input[i] * window[this.readPosition]; + this.readPosition++; + } + + this.readSamples -= samples; + + if (this.readSamples == 0) { + this.hipass.process(this.buffer, this.buffer.length); + } + } + + write(wet: number, outLeft: Float32Array, outRight: Float32Array) { + if (this.writeSamples === 0) return; + const samples = Math.min(this.writeSamples, outLeft.length); + + const leftVol = this.leftGain * wet; + const rightVol = this.rightGain * wet; + + for (let i = 0; i < samples; i++) { + outLeft[i] += this.buffer[this.writePosition] * leftVol; + outRight[i] += this.buffer[this.writePosition] * rightVol; + this.writePosition++; + } + + this.writeSamples -= samples; + } +} diff --git a/packages/granite/src/index.ts b/packages/granite/src/index.ts new file mode 100644 index 0000000..c60855f --- /dev/null +++ b/packages/granite/src/index.ts @@ -0,0 +1,36 @@ +import { + createRegistrar, + createWorkletConstructor, + ParamInput, +} from "./_worklet"; +import { PROCESSOR } from "./processor"; + +export const registerGraniteWorklet = createRegistrar("GRANITE", PROCESSOR); + +export type GraniteInputs = { + wet?: ParamInput; + speed?: ParamInput; + density?: ParamInput; + spread?: ParamInput; +}; + +export type GraniteWorkletNode = AudioWorkletNode & { + wet: AudioParam; + speed: AudioParam; + density: AudioParam; + spread: AudioParam; + dispose(): void; +}; + +export const Granite = createWorkletConstructor< + GraniteWorkletNode, + GraniteInputs +>({ + processorName: "GraniteProcessor", + paramNames: ["wet", "speed", "density", "spread"], + workletOptions: () => ({ + numberOfInputs: 1, + numberOfOutputs: 1, + outputChannelCount: [2], + }), +}); diff --git a/packages/granite/src/processor.ts b/packages/granite/src/processor.ts new file mode 100644 index 0000000..acad46d --- /dev/null +++ b/packages/granite/src/processor.ts @@ -0,0 +1 @@ +export const PROCESSOR = `"use strict";(()=>{function I(c){let e=0,r=0,o=0,f=0,n=0,m=0,l=0,y=0,b=0,M=Math.floor(c*(200/1e3)),g=G(M),F=Array.from({length:16},()=>new A(M));function N(h,u,p,d){e=h,r=u,n=p,y=d,o=r/c,m=n/c}function _(h,u,p){let d=h[0],s=u[0],P=u[1];if(f+=o*p,f>1&&(f-=1,F[b].startRead(),b=(b+1)%16),l+=m*p,l>1){l-=1;let a=Math.floor(Math.random()*16)%16,w=F[a];w.startWrite()&&(w.pan(Math.random()*y*2-1),w.filter(Math.random()*2e3))}for(let a=0;a<16;a++)F[a].read(d,g),F[a].write(e,s,P);let R=1-e;for(let a=0;a0||this.writeSamples>0||(this.readSamples=this.buffer.length,this.readPosition=0)}startWrite(){return this.writeSamples>0?!1:(this.writeSamples=this.buffer.length,this.writePosition=0,!0)}pan(t){this.leftGain=Math.cos(Math.PI/4*(t+1)),this.rightGain=Math.sin(Math.PI/4*(t+1))}filter(t){this.hipass.freq(t)}read(t,i){if(this.readSamples===0)return;let e=Math.min(this.readSamples,t.length);for(let r=0;r{switch(e.data.type){case"DISPOSE":this.r=!1;break}}}process(t,i,e){this.u(e.wet[0],e.speed[0],e.density[0],e.spread[0]);let r=t[0],o=i[0];return r.length===0||o.length===0?this.r:(this.c(r,o,r[0].length),this.r)}static get parameterDescriptors(){return[["wet",.5,0,1],["speed",10,0,100],["density",15,0,30],["spread",.5,0,1]].map(([t,i,e,r])=>({name:t,defaultValue:i,minValue:e,maxValue:r,automationRate:"k-rate"}))}};registerProcessor("GraniteProcessor",S);})();`; diff --git a/packages/granite/src/worklet.ts b/packages/granite/src/worklet.ts new file mode 100644 index 0000000..39a2426 --- /dev/null +++ b/packages/granite/src/worklet.ts @@ -0,0 +1,53 @@ +import { ComputeFn, createDsp, UpdateFn } from "./dsp"; + +export class GraniteProcessor 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.wet[0], p.speed[0], p.density[0], p.spread[0]); + const in1 = inputs[0]; + const out1 = outputs[0]; + + if (in1.length === 0 || out1.length === 0) { + return this.r; + } + + this.c(in1, out1, in1[0].length); + + return this.r; + } + + static get parameterDescriptors() { + return [ + ["wet", 0.5, 0, 1], + ["speed", 10, 0, 100], + ["density", 15, 0, 30], + ["spread", 0.5, 0, 1], + ].map(([name, defaultValue, minValue, maxValue]) => ({ + name, + defaultValue, + minValue, + maxValue, + automationRate: "k-rate", + })); + } +} + +registerProcessor("GraniteProcessor", GraniteProcessor); diff --git a/packages/synthlet/CHANGELOG.md b/packages/synthlet/CHANGELOG.md index 1d32e6d..362d93a 100644 --- a/packages/synthlet/CHANGELOG.md +++ b/packages/synthlet/CHANGELOG.md @@ -1,16 +1,20 @@ # synthlet +## 0.11.0 + +- Granite effect module `@synthlet/granite` + ## 0.10.0 -- New VirtualAnalogFilter module `@synthlet/virtual-analog-filter` +- VirtualAnalogFilter effect module `@synthlet/virtual-analog-filter` ## 0.9.0 -- New ReverbDelay effect `@synthlet/reverb-delay` +- ReverbDelay effect module `@synthlet/reverb-delay` ## 0.8.0 -- New KarplusStrong audio source `@synthlet/karplus-strong` +- KarplusStrong source module `@synthlet/karplus-strong` ## 0.7.0 diff --git a/packages/synthlet/package.json b/packages/synthlet/package.json index 85f010f..1f3bb89 100644 --- a/packages/synthlet/package.json +++ b/packages/synthlet/package.json @@ -1,6 +1,6 @@ { "name": "synthlet", - "version": "0.10.0", + "version": "0.11.0", "description": "Modular synthesis in the browser", "keywords": [ "modular", @@ -29,6 +29,7 @@ "@synthlet/clock": "^0.1.0", "@synthlet/dattorro-reverb": "^0.1.0", "@synthlet/euclid": "^0.1.0", + "@synthlet/granite": "^0.1.0", "@synthlet/impulse": "^0.1.0", "@synthlet/karplus-strong": "^0.1.0", "@synthlet/lfo": "^0.1.0", diff --git a/packages/synthlet/src/index.ts b/packages/synthlet/src/index.ts index f274c77..6e6cb82 100644 --- a/packages/synthlet/src/index.ts +++ b/packages/synthlet/src/index.ts @@ -7,6 +7,7 @@ import { registerClipAmpWorklet } from "@synthlet/clip-amp"; import { registerClockWorklet } from "@synthlet/clock"; import { registerDattorroReverbWorklet } from "@synthlet/dattorro-reverb"; import { registerEuclidWorklet } from "@synthlet/euclid"; +import { registerGraniteWorklet } from "@synthlet/granite/src"; import { registerImpulseWorklet } from "@synthlet/impulse"; import { registerKarplusStrongWorklet } from "@synthlet/karplus-strong"; import { registerLfoWorklet } from "@synthlet/lfo"; @@ -27,6 +28,7 @@ export * from "@synthlet/clip-amp"; export * from "@synthlet/clock"; export * from "@synthlet/dattorro-reverb"; export * from "@synthlet/euclid"; +export * from "@synthlet/granite"; export * from "@synthlet/impulse"; export * from "@synthlet/karplus-strong"; export * from "@synthlet/lfo"; @@ -57,6 +59,7 @@ export function registerAllWorklets( registerClockWorklet(context), registerDattorroReverbWorklet(context), registerEuclidWorklet(context), + registerGraniteWorklet(context), registerImpulseWorklet(context), registerKarplusStrongWorklet(context), registerLfoWorklet(context), diff --git a/site/.source/index.js b/site/.source/index.js index 125ad92..c3556c1 100644 --- a/site/.source/index.js +++ b/site/.source/index.js @@ -7,29 +7,30 @@ import * as file_4 from "../content/docs/troubleshoo.mdx?collection=docs&hash=2b import * as file_5 from "../content/docs/(effects)/chorus-t.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" import * as file_6 from "../content/docs/(effects)/chorus.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" import * as file_7 from "../content/docs/(effects)/dattorro.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_8 from "../content/docs/(effects)/reverb-delay.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_9 from "../content/docs/(modifiers)/ad-amp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_10 from "../content/docs/(modifiers)/adsr-amp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_11 from "../content/docs/(modifiers)/clip-amp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_12 from "../content/docs/(modifiers)/state-variable-filter.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_13 from "../content/docs/(modifiers)/vaf.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_14 from "../content/docs/(modulators)/ad.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_15 from "../content/docs/(modulators)/adsr.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_16 from "../content/docs/(modulators)/lfo.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_17 from "../content/docs/(modulators)/param.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_18 from "../content/docs/(sources)/impulse.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_19 from "../content/docs/(sources)/ks.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_20 from "../content/docs/(sources)/noise.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_21 from "../content/docs/(sources)/polyblep.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_22 from "../content/docs/(sources)/wavetable.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_23 from "../content/docs/(sequencers)/arp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_24 from "../content/docs/(sequencers)/clock.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_25 from "../content/docs/(sequencers)/euclid.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_26 from "../content/docs/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_27 from "../content/docs/(effects)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_28 from "../content/docs/(modifiers)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_29 from "../content/docs/(modulators)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_30 from "../content/docs/(sequencers)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -import * as file_31 from "../content/docs/(sources)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" -export const docs = [toRuntime("doc", file_0, {"path":"dsl.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/dsl.mdx"}),toRuntime("doc", file_1, {"path":"guide.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/guide.mdx"}),toRuntime("doc", file_2, {"path":"quick-start.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/quick-start.mdx"}),toRuntime("doc", file_3, {"path":"synths.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/synths.mdx"}),toRuntime("doc", file_4, {"path":"troubleshoo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/troubleshoo.mdx"}),toRuntime("doc", file_5, {"path":"(effects)/chorus-t.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/chorus-t.mdx"}),toRuntime("doc", file_6, {"path":"(effects)/chorus.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/chorus.mdx"}),toRuntime("doc", file_7, {"path":"(effects)/dattorro.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/dattorro.mdx"}),toRuntime("doc", file_8, {"path":"(effects)/reverb-delay.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/reverb-delay.mdx"}),toRuntime("doc", file_9, {"path":"(modifiers)/ad-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/ad-amp.mdx"}),toRuntime("doc", file_10, {"path":"(modifiers)/adsr-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/adsr-amp.mdx"}),toRuntime("doc", file_11, {"path":"(modifiers)/clip-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/clip-amp.mdx"}),toRuntime("doc", file_12, {"path":"(modifiers)/state-variable-filter.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/state-variable-filter.mdx"}),toRuntime("doc", file_13, {"path":"(modifiers)/vaf.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/vaf.mdx"}),toRuntime("doc", file_14, {"path":"(modulators)/ad.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/ad.mdx"}),toRuntime("doc", file_15, {"path":"(modulators)/adsr.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/adsr.mdx"}),toRuntime("doc", file_16, {"path":"(modulators)/lfo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/lfo.mdx"}),toRuntime("doc", file_17, {"path":"(modulators)/param.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/param.mdx"}),toRuntime("doc", file_18, {"path":"(sources)/impulse.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/impulse.mdx"}),toRuntime("doc", file_19, {"path":"(sources)/ks.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/ks.mdx"}),toRuntime("doc", file_20, {"path":"(sources)/noise.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/noise.mdx"}),toRuntime("doc", file_21, {"path":"(sources)/polyblep.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/polyblep.mdx"}),toRuntime("doc", file_22, {"path":"(sources)/wavetable.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/wavetable.mdx"}),toRuntime("doc", file_23, {"path":"(sequencers)/arp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/arp.mdx"}),toRuntime("doc", file_24, {"path":"(sequencers)/clock.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/clock.mdx"}),toRuntime("doc", file_25, {"path":"(sequencers)/euclid.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/euclid.mdx"})] -export const meta = [toRuntime("meta", file_26, {"path":"meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/meta.json"}),toRuntime("meta", file_27, {"path":"(effects)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/meta.json"}),toRuntime("meta", file_28, {"path":"(modifiers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/meta.json"}),toRuntime("meta", file_29, {"path":"(modulators)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/meta.json"}),toRuntime("meta", file_30, {"path":"(sequencers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/meta.json"}),toRuntime("meta", file_31, {"path":"(sources)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/meta.json"})] \ No newline at end of file +import * as file_8 from "../content/docs/(effects)/granite.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_9 from "../content/docs/(effects)/reverb-delay.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_10 from "../content/docs/(modulators)/ad.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_11 from "../content/docs/(modulators)/adsr.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_12 from "../content/docs/(modulators)/lfo.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_13 from "../content/docs/(modulators)/param.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_14 from "../content/docs/(modifiers)/ad-amp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_15 from "../content/docs/(modifiers)/adsr-amp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_16 from "../content/docs/(modifiers)/clip-amp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_17 from "../content/docs/(modifiers)/state-variable-filter.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_18 from "../content/docs/(modifiers)/vaf.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_19 from "../content/docs/(sources)/impulse.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_20 from "../content/docs/(sources)/ks.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_21 from "../content/docs/(sources)/noise.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_22 from "../content/docs/(sources)/polyblep.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_23 from "../content/docs/(sources)/wavetable.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_24 from "../content/docs/(sequencers)/arp.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_25 from "../content/docs/(sequencers)/clock.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_26 from "../content/docs/(sequencers)/euclid.mdx?collection=docs&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_27 from "../content/docs/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_28 from "../content/docs/(effects)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_29 from "../content/docs/(modifiers)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_30 from "../content/docs/(modulators)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_31 from "../content/docs/(sequencers)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +import * as file_32 from "../content/docs/(sources)/meta.json?collection=meta&hash=2b527dd5b30464f2ddc4c049fb69d30b9fcc145ae96eece40109d80d98642180" +export const docs = [toRuntime("doc", file_0, {"path":"dsl.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/dsl.mdx"}),toRuntime("doc", file_1, {"path":"guide.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/guide.mdx"}),toRuntime("doc", file_2, {"path":"quick-start.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/quick-start.mdx"}),toRuntime("doc", file_3, {"path":"synths.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/synths.mdx"}),toRuntime("doc", file_4, {"path":"troubleshoo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/troubleshoo.mdx"}),toRuntime("doc", file_5, {"path":"(effects)/chorus-t.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/chorus-t.mdx"}),toRuntime("doc", file_6, {"path":"(effects)/chorus.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/chorus.mdx"}),toRuntime("doc", file_7, {"path":"(effects)/dattorro.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/dattorro.mdx"}),toRuntime("doc", file_8, {"path":"(effects)/granite.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/granite.mdx"}),toRuntime("doc", file_9, {"path":"(effects)/reverb-delay.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/reverb-delay.mdx"}),toRuntime("doc", file_10, {"path":"(modulators)/ad.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/ad.mdx"}),toRuntime("doc", file_11, {"path":"(modulators)/adsr.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/adsr.mdx"}),toRuntime("doc", file_12, {"path":"(modulators)/lfo.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/lfo.mdx"}),toRuntime("doc", file_13, {"path":"(modulators)/param.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/param.mdx"}),toRuntime("doc", file_14, {"path":"(modifiers)/ad-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/ad-amp.mdx"}),toRuntime("doc", file_15, {"path":"(modifiers)/adsr-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/adsr-amp.mdx"}),toRuntime("doc", file_16, {"path":"(modifiers)/clip-amp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/clip-amp.mdx"}),toRuntime("doc", file_17, {"path":"(modifiers)/state-variable-filter.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/state-variable-filter.mdx"}),toRuntime("doc", file_18, {"path":"(modifiers)/vaf.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/vaf.mdx"}),toRuntime("doc", file_19, {"path":"(sources)/impulse.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/impulse.mdx"}),toRuntime("doc", file_20, {"path":"(sources)/ks.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/ks.mdx"}),toRuntime("doc", file_21, {"path":"(sources)/noise.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/noise.mdx"}),toRuntime("doc", file_22, {"path":"(sources)/polyblep.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/polyblep.mdx"}),toRuntime("doc", file_23, {"path":"(sources)/wavetable.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/wavetable.mdx"}),toRuntime("doc", file_24, {"path":"(sequencers)/arp.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/arp.mdx"}),toRuntime("doc", file_25, {"path":"(sequencers)/clock.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/clock.mdx"}),toRuntime("doc", file_26, {"path":"(sequencers)/euclid.mdx","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/euclid.mdx"})] +export const meta = [toRuntime("meta", file_27, {"path":"meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/meta.json"}),toRuntime("meta", file_28, {"path":"(effects)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(effects)/meta.json"}),toRuntime("meta", file_29, {"path":"(modifiers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modifiers)/meta.json"}),toRuntime("meta", file_30, {"path":"(modulators)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(modulators)/meta.json"}),toRuntime("meta", file_31, {"path":"(sequencers)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sequencers)/meta.json"}),toRuntime("meta", file_32, {"path":"(sources)/meta.json","absolutePath":"/Users/danigb/Projects/Synthlet/synthlet/site/content/docs/(sources)/meta.json"})] \ No newline at end of file diff --git a/site/app/demos/granite/GraniteDemo.tsx b/site/app/demos/granite/GraniteDemo.tsx new file mode 100644 index 0000000..1a263ab --- /dev/null +++ b/site/app/demos/granite/GraniteDemo.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { createSynthAudioContext } from "@/app/audio-context"; +import { Slider } from "@/examples/components/Slider"; +import { useMemo, useState } from "react"; +import { Granite, type GraniteWorkletNode } from "synthlet"; + +export default function GraniteDemo() { + const [status, setStatus] = useState("loading"); + const player = useMemo(() => new GranitePlayer(setStatus), [setStatus]); + return ( +

+ + + {player.granite && ( +
+ + + + +
+ )} +
+ ); +} + +class GranitePlayer { + buffer: AudioBuffer | null = null; + source: AudioBufferSourceNode | null = null; + ac: AudioContext | null = null; + granite: GraniteWorkletNode | null = null; + + constructor(private readonly onChange: (status: string) => void) { + onChange("loading"); + createSynthAudioContext() + .then((ac) => { + this.ac = ac; + this.granite = Granite(this.ac, {}); + this.granite.connect(this.ac.destination); + return ac; + }) + .then((ac) => { + fetch("/synthlet/track14.mp3") + .then((r) => r.arrayBuffer()) + .then((buffer) => ac.decodeAudioData(buffer)) + .then((buffer) => { + this.buffer = buffer; + onChange("ready"); + }); + }); + } + + isReady() { + return !!this.ac && !!this.buffer; + } + + isPlaying() { + return !!this.source; + } + + start() { + if (!this.ac || !this.granite) return; + if (!this.buffer) return; + if (this.source) return; + + const source = this.ac.createBufferSource(); + source.buffer = this.buffer; + source.connect(this.granite); + source.start(); + this.onChange("playing"); + this.source = source; + } + + stop() { + if (!this.source) return; + this.source.stop(); + this.source = null; + this.onChange("ready"); + } +} diff --git a/site/app/demos/granite/page.tsx b/site/app/demos/granite/page.tsx new file mode 100644 index 0000000..9eb50a5 --- /dev/null +++ b/site/app/demos/granite/page.tsx @@ -0,0 +1,12 @@ +import dynamic from "next/dynamic"; + +const GraniteDemo = dynamic(() => import("./GraniteDemo"), { ssr: false }); + +export default function GranitePage() { + return ( +
+

Granite

+ +
+ ); +} diff --git a/site/content/docs/(effects)/granite.mdx b/site/content/docs/(effects)/granite.mdx new file mode 100644 index 0000000..bac4c21 --- /dev/null +++ b/site/content/docs/(effects)/granite.mdx @@ -0,0 +1,26 @@ +--- +title: Granite +description: A granular effect +package: granite +--- + +This is a mono-to-stereo granular effect. See [demo](/demos/granite) + +```ts +import { Granite } from "synthlet"; + +const osc = new OscillatorNode(audioContext); +const granite = Granite(audioContext, { + speed: 10, + density: 15, + spread: 0.5, +}); + +osc.connect(granite).connect(audioContext.destination); +``` + +## Parameters + +- `speed`: The sampling of the grains [0, 100] +- `density`: The density of the grains [0, 30] +- `spread`: The spread of the grains [0, 1] diff --git a/site/public/track14.mp3 b/site/public/track14.mp3 new file mode 100644 index 0000000..c4a3c2d Binary files /dev/null and b/site/public/track14.mp3 differ