From 99e485554520f5959084dbb72ceec0b16657bfe3 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 22 Sep 2025 16:39:12 -0500 Subject: [PATCH] Working version --- packages/core/controls.mjs | 2 + packages/core/repl.mjs | 2 +- packages/core/test/pattern.test.mjs | 4 +- packages/superdough/superdough.mjs | 45 ++++++--- packages/superdough/synth.mjs | 119 ++--------------------- packages/superdough/worklets.mjs | 50 +++++----- website/src/components/Header/Search.css | 4 +- website/src/pages/learn/samples.mdx | 2 +- 8 files changed, 76 insertions(+), 152 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 044cfa96d..6ca21b6db 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2108,3 +2108,5 @@ export const scrub = register( }, false, ); + +export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr'); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 458e7d9a2..c4e83704e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -5,7 +5,7 @@ import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; -import { SalatRepl } from "@kabelsalat/web"; +import { SalatRepl } from '@kabelsalat/web'; export function repl({ defaultOutput, diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..625f40756 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - it('Can apply a function', () => { + (it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - }); + })); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 40fa96dac..43e27a072 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -636,6 +636,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) analyze, // analyser wet fft = getDefaultValue('fft'), // fftSize 0 - 10 FX, + FXrelease, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -655,7 +656,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) delay = applyGainCurve(delay); const end = t + hapDuration; - const endWithRelease = end + release; + const fullRelease = Math.max(release, FXrelease ?? 0); + const endWithRelease = end + fullRelease; + value.release = fullRelease; const chainID = Math.round(Math.random() * 1000000); // oldest audio nodes will be destroyed if maximum polyphony is exceeded @@ -687,14 +690,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) sourceNode = source(t, value, hapDuration, cps); } else if (getSound(s)) { const { onTrigger } = getSound(s); - const onEnded = () => { - // audioNodes.forEach((n) => n?.disconnect()); - debugger; - earlyNodes.forEach((n) => n?.disconnect()); - disconnectNodeWhenQuiet(ac, finalDryNode, audioNodes); - activeSoundSources.delete(chainID); - }; - const soundHandle = await onTrigger(t, value, onEnded); + const soundHandle = await onTrigger(t, value, () => void 0); if (soundHandle) { sourceNode = soundHandle.node; @@ -881,9 +877,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) earlyNodes = [...chain]; chain.push(workletNode); let workletSrc = fx.workletSrc; - workletSrc = workletSrc.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i]); - workletSrc = workletSrc.replaceAll('sFreq', getFrequencyFromValue(value)); - workletSrc = workletSrc.replaceAll('sGate', `cc('strudel-gate-${chainID}')`); + workletSrc = workletSrc + .replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i]) + .replaceAll('sFreq', getFrequencyFromValue(value)) + .replaceAll('sGate', `cc('strudel-gate-${chainID}')`); + /* global compileKabel */ const { src, ugens, registers } = compileKabel(workletSrc); workletNode.port.postMessage({ src, schema: { ugens, registers }, start: t, end }); } @@ -934,6 +932,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) audioNodes.push(dryReverb, reverbNode, wetReverb); } } + const useFXRelease = FXrelease !== undefined && FXrelease > release; + if (useFXRelease) { + const releaseNode = gainNode(1); + releaseNode.gain.setValueAtTime(1, end + release); + releaseNode.gain.linearRampToValueAtTime(0, endWithRelease); + chain.push(releaseNode); + } // last gain const post = new GainNode(ac, { gain: postgain }); @@ -986,8 +991,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); audioNodes.push(...chain); - earlyNodes = earlyNodes.length > 0 ? earlyNodes : audioNodes; - audioNodes = audioNodes.filter((n) => earlyNodes.includes(n)); + // earlyNodes = []; + earlyNodes = earlyNodes.length > 0 && !useFXRelease ? earlyNodes : audioNodes; + audioNodes = audioNodes.filter((n) => !earlyNodes.includes(n)); + webAudioTimeout( + ac, + () => { + // audioNodes.forEach((n) => n?.disconnect()); + // earlyNodes.forEach((n) => n?.disconnect()); + disconnectNodeWhenQuiet(ac, finalDryNode, audioNodes); + activeSoundSources.delete(chainID); + }, + 0, + endWithRelease, + ); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index ac670edf2..48fa8a9ef 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,4 +1,4 @@ -import { clamp, getFrequencyFromValue} from './util.mjs'; +import { clamp, getFrequencyFromValue } from './util.mjs'; import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; import { applyFM, @@ -13,14 +13,6 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -function destroyAudioWorkletNode(node) { - if (node == null) { - return; - } - node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); -} - const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const waveformAliases = [ ['tri', 'triangle'], @@ -44,7 +36,7 @@ export function registerSynthSounds() { [...waveforms].forEach((s) => { registerSound( s, - (t, value, onended) => { + (t, value) => { const [attack, decay, sustain, release] = getADSRValues( [value.attack, value.decay, value.sustain, value.release], 'linear', @@ -52,31 +44,20 @@ export function registerSynthSounds() { ); let sound = getOscillator(s, t, value); - let { node: o, stop, triggerRelease } = sound; + let { node: o } = sound; // turn down const g = gainNode(0.3); const { duration } = value; - o.onended = () => { - o.disconnect(); - g.disconnect(); - onended(); - }; - const envGain = gainNode(1); let node = o.connect(g).connect(envGain); const holdEnd = t + duration; getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); const envEnd = holdEnd + release + 0.01; - triggerRelease?.(envEnd); - stop(envEnd); return { node, - stop: (endTime) => { - stop(endTime); - }, }; }, { type: 'synth', prebake: true }, @@ -85,7 +66,7 @@ export function registerSynthSounds() { registerSound( 'sbd', - (t, value, onended) => { + (t, value) => { const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; const ctx = getAudioContext(); const attackhold = 0.02; @@ -115,16 +96,6 @@ export function registerSynthSounds() { const mix = gainNode(mixGain); - o.onended = () => { - o.disconnect(); - g.disconnect(); - sat.disconnect(); - noise.node.disconnect(); - noiseGain.disconnect(); - mix.disconnect(); - onended(); - }; - const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); @@ -138,14 +109,8 @@ export function registerSynthSounds() { mix.gain.setValueAtTime(mixGain, end - 0.01); mix.gain.linearRampToValueAtTime(0, end); - o.stop(end); - noise.stop(end); - return { node, - stop: (endTime) => { - o.stop(endTime); - }, }; }, { type: 'synth', prebake: true }, @@ -153,7 +118,7 @@ export function registerSynthSounds() { registerSound( 'supersaw', - (begin, value, onended) => { + (begin, value) => { const ac = getAudioContext(); let { duration, n, unison = 5, spread = 0.6, detune } = value; detune = detune ?? n ?? 0.18; @@ -194,24 +159,8 @@ export function registerSynthSounds() { getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); - let timeoutNode = webAudioTimeout( - ac, - () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); - onended(); - fm?.stop(); - vibratoOscillator?.stop(); - }, - begin, - end, - ); - return { node: envGain, - stop: (time) => { - timeoutNode.stop(time); - }, }; }, { prebake: true, type: 'synth' }, @@ -219,7 +168,7 @@ export function registerSynthSounds() { registerSound( 'bytebeat', - (begin, value, onended) => { + (begin, value) => { const defaultBeats = [ '(t%255 >= t/255%255)*255', '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', @@ -272,22 +221,8 @@ export function registerSynthSounds() { getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - let timeoutNode = webAudioTimeout( - ac, - () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); - onended(); - }, - begin, - end, - ); - return { node: envGain, - stop: (time) => { - timeoutNode.stop(time); - }, }; }, { prebake: true, type: 'synth' }, @@ -295,7 +230,7 @@ export function registerSynthSounds() { registerSound( 'pulse', - (begin, value, onended) => { + (begin, value) => { const ac = getAudioContext(); let { pwrate, pwsweep } = value; if (pwsweep == null) { @@ -346,25 +281,9 @@ export function registerSynthSounds() { lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); lfo.connect(o.parameters.get('pulsewidth')); } - let timeoutNode = webAudioTimeout( - ac, - () => { - destroyAudioWorkletNode(o); - destroyAudioWorkletNode(lfo); - envGain.disconnect(); - onended(); - fm?.stop(); - vibratoOscillator?.stop(); - }, - begin, - end, - ); return { node: envGain, - stop: (time) => { - timeoutNode.stop(time); - }, }; }, { prebake: true, type: 'synth' }, @@ -373,7 +292,7 @@ export function registerSynthSounds() { [...noises].forEach((s) => { registerSound( s, - (t, value, onended) => { + (t, value) => { const [attack, decay, sustain, release] = getADSRValues( [value.attack, value.decay, value.sustain, value.release], 'linear', @@ -385,31 +304,20 @@ export function registerSynthSounds() { let { density } = value; sound = getNoiseOscillator(s, t, density); - let { node: o, stop, triggerRelease } = sound; + let { node: o } = sound; // turn down const g = gainNode(0.3); const { duration } = value; - o.onended = () => { - o.disconnect(); - g.disconnect(); - onended(); - }; - const envGain = gainNode(1); let node = o.connect(g).connect(envGain); const holdEnd = t + duration; getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); const envEnd = holdEnd + release + 0.01; - triggerRelease?.(envEnd); - stop(envEnd); return { node, - stop: (endTime) => { - stop(endTime); - }, }; }, { type: 'synth', prebake: true }, @@ -479,14 +387,5 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, - stop: (time) => { - fmModulator.stop(time); - vibratoOscillator?.stop(time); - noiseMix?.stop(time); - o.stop(time); - }, - triggerRelease: (time) => { - // envGain?.stop(time); - }, }; } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 5dd967c84..08b78f805 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -958,16 +958,23 @@ class MeterProcessor extends AudioWorkletProcessor { } registerProcessor('meter-processor', MeterProcessor); -class GenericProcessor extends AudioWorkletProcessor{ - constructor(){ +class GenericProcessor extends AudioWorkletProcessor { + constructor() { super(); this.playPos = 0; const channels = 16; this.outputs = new Array(channels).fill(0); this.sources = new Array(channels).fill(0); this.gateEnded = false; + this.started = false; this.port.onmessage = (event) => { - let { src, schema: { ugens, registers }, gate, start, end } = event.data; + let { + src, + schema: { ugens, registers }, + gate, + start, + end, + } = event.data; this.start = start; this.end = end; this.registers = new Array(registers).fill(0); @@ -984,36 +991,35 @@ class GenericProcessor extends AudioWorkletProcessor{ this.nodes[i] = node; } this.genSample = new Function( - "time", - "nodes", - "input", - "r", // registers - "o", // outputs - "s", // sources - this.src + 'time', + 'nodes', + 'input', + 'r', // registers + 'o', // outputs + 's', // sources + this.src, ); }; } - process(inputs, outputs){ + process(inputs, outputs) { + const input = inputs[0]?.[0]; if (this.genSample === undefined || currentTime < this.start) { + // pending return true; + } else if (input === undefined) { + // if no input and started, return false and close worklet + // else just keep waiting (true) + return !this.started; } + this.started = true; if (!this.gateEnded && currentTime > this.end) { this.gateNode?.setValue(0); this.gateEnded = true; } const outL = outputs[0][0]; const outR = outputs[0][1] ?? outputs[0][0]; - const input = inputs[0]?.[0]; - for(let n = 0; n < blockSize; n++) { - this.genSample( - this.playPos, - this.nodes, - input ? input[n] : 0, - this.registers, - this.outputs, - this.sources, - ); + for (let n = 0; n < blockSize; n++) { + this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources); outL[n] = this.outputs[0]; outR[n] = this.outputs[1]; this.playPos += 1 / sampleRate; @@ -1021,4 +1027,4 @@ class GenericProcessor extends AudioWorkletProcessor{ return true; } } -registerProcessor("generic-processor", GenericProcessor); \ No newline at end of file +registerProcessor('generic-processor', GenericProcessor); diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index 456ef9f6b..b14146cbd 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), - 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: + inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf7..201d57dfa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed