Single silencer node; postgain on the worklets

This commit is contained in:
Aria
2025-11-21 16:19:39 -06:00
parent ffb618dc97
commit 352344aa91
4 changed files with 45 additions and 30 deletions
+12 -8
View File
@@ -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 {
+13 -7
View File
@@ -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,
};
},
+3 -5
View File
@@ -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);
+17 -10
View File
@@ -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;