diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index f24b79092..2659abf75 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -339,6 +339,17 @@ export function scheduleAtTime(callback, targetTime, audioContext = getAudioCont const currentTime = audioContext.currentTime; webAudioTimeout(audioContext, callback, currentTime, targetTime); } + +let silencer; +function getSilencer() { + const ac = getAudioContext(); + if (!silencer) { + silencer = gainNode(0); + silencer.connect(ac.destination); + } + return silencer; +} + // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // a bit of a hack, but it works very well :) export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { @@ -346,18 +357,11 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { // Certain browsers requires audio nodes to be connected in order for their onended events // to fire, so we _mute it_ and then connect it to the destination - const zeroGain = gainNode(0); - zeroGain.connect(audioContext.destination); + const zeroGain = getSilencer(); constantNode.connect(zeroGain); // Schedule the `onComplete` callback to occur at `stopTime` constantNode.onended = () => { - // Ensure garbage collection - try { - zeroGain.disconnect(); - } catch { - // pass - } try { constantNode.disconnect(); } catch { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index bf1d11a2b..3fb41ec38 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -183,17 +183,23 @@ export function registerSynthSounds() { getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const fm = applyFM(o.parameters.get('frequency'), value, begin); - let envGain = gainNode(1); - envGain = o.connect(envGain); - - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); - + getParamADSR( + o.parameters.get('postgain'), + attack, + decay, + sustain, + release, + 0, + 0.3 * gainAdjustment, + begin, + holdend, + 'linear', + ); const timeoutNode = webAudioTimeout( ac, () => { destroyAudioWorkletNode(o); releaseVoice('supersaw', o); - envGain.disconnect(); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -203,7 +209,7 @@ export function registerSynthSounds() { ); return { - node: envGain, + node: o, stop: timeoutNode.stop, }; }, diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 0d07368d3..9ab469379 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -249,6 +249,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { source.port.postMessage({ type: 'table', payload }); if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); + destroyAudioWorkletNode(source); releaseVoice(voiceKey, source); return; } @@ -316,17 +317,14 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { ); const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); const fm = applyFM(source.parameters.get('frequency'), value, t); - const envGain = ac.createGain(); - const node = source.connect(envGain); - getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); + getParamADSR(source.parameters.get('postgain'), attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); - const handle = { node, source }; + const handle = { node: source }; const timeoutNode = webAudioTimeout( ac, () => { vibratoOscillator?.stop(); fm?.stop(); - node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); destroyAudioWorkletNode(source); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index e06f6b116..fdd7672ce 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -498,13 +498,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { defaultValue: 0, min: 0, }, - { name: 'voices', defaultValue: 5, min: 1, automationRate: 'k-rate', }, + { + name: 'postgain', + defaultValue: 1, + }, ]; } process(_input, outputs, params) { @@ -520,10 +523,11 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const voices = params.voices[0]; // k-rate for (let i = 0; i < output[0].length; i++) { const detune = pv(params.detune, i); + const postgain = pv(params.postgain, i); const freqspread = pv(params.freqspread, i); const panspread = pv(params.panspread, i) * 0.5 + 0.5; - let gainL = Math.sqrt(1 - panspread); - let gainR = Math.sqrt(panspread); + let gainL = Math.sqrt(1 - panspread) * postgain; + let gainR = Math.sqrt(panspread) * postgain; let freq = pv(params.frequency, i); // Main detuning freq = applySemitoneDetuneToFrequency(freq, detune / 100); @@ -1129,6 +1133,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' }, { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, { name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, + { name: 'postgain', defaultValue: 1, min: 0 }, ]; } @@ -1318,22 +1323,23 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return level; } - _zeroOutputs(outL, outR) { - outL.fill(0); - outR.fill(0); + _zeroOutputs(outputs) { + outputs[0][0].fill(0); + outputs[0][1].fill(0); } process(_inputs, outputs, parameters) { - const outL = outputs[0][0]; - const outR = outputs[0][1] || outputs[0][0]; const begin = parameters.begin[0]; const end = parameters.end[0]; if (!this.tables || currentTime <= begin || currentTime >= end) { - this._zeroOutputs(); + this._zeroOutputs(outputs); return true; } + const outL = outputs[0][0]; + const outR = outputs[0][1] || outputs[0][0]; const voices = parameters.voices[0]; // k-rate + const voicesDenom = 1 / Math.sqrt(voices); for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); const freqspread = pv(parameters.freqspread, i); @@ -1343,13 +1349,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const interpT = idx - fIdx; const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpMode = pv(parameters.warpMode, i); + const postgain = pv(parameters.postgain, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; const gain1 = Math.sqrt(0.5 - 0.5 * panspread); const gain2 = Math.sqrt(0.5 + 0.5 * panspread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune - const normalizer = 1 / Math.sqrt(voices); + const normalizer = postgain * voicesDenom; const detuner = getDetuner(voices, freqspread); for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1;