proper audio node cleanup

+ cleanup onTriggerSample
This commit is contained in:
Felix Roos 2023-03-09 10:46:45 +01:00
parent 65e48c05f0
commit be6b011204
3 changed files with 46 additions and 28 deletions

View file

@ -150,7 +150,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
}), }),
); );
} }
setSound(key, (t, hapValue) => onTriggerSample(t, hapValue, value), { setSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample', type: 'sample',
samples: value, samples: value,
baseUrl, baseUrl,
@ -161,7 +161,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = []; const cutGroups = [];
export async function onTriggerSample(t, value, bank) { export async function onTriggerSample(t, value, onended, bank) {
const { const {
s, s,
freq, freq,
@ -176,24 +176,19 @@ export async function onTriggerSample(t, value, bank) {
begin = 0, begin = 0,
end = 1, end = 1,
} = value; } = value;
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
// load sample // load sample
if (speed === 0) { if (speed === 0) {
// no playback // no playback
return; return;
} }
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
//const soundfont = getSoundfontKey(s); //const soundfont = getSoundfontKey(s);
let bufferSource; const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank);
//if (soundfont) {
// is soundfont
//bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
//} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank);
//}
// asny stuff above took too long? // asny stuff above took too long?
if (ac.currentTime > t) { if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
@ -201,7 +196,7 @@ export async function onTriggerSample(t, value, bank) {
return; return;
} }
if (!bufferSource) { if (!bufferSource) {
console.warn('no buffer source'); logger(`[sampler] could not load "${s}:${n}"`, 'error');
return; return;
} }
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
@ -212,7 +207,6 @@ export async function onTriggerSample(t, value, bank) {
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed, // rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5." // the midway point through a 10-second audio buffer is still 5."
const time = t + nudge;
const offset = begin * bufferSource.buffer.duration; const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset); bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
@ -229,19 +223,33 @@ export async function onTriggerSample(t, value, bank) {
}*/ }*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope); bufferSource.connect(envelope);
if (cut !== undefined) { bufferSource.onended = function () {
cutGroups[cut]?.stop(time); // fade out? bufferSource.disconnect();
cutGroups[cut] = bufferSource; envelope.disconnect();
} onended();
const stop = (endTime) => { };
const stop = (endTime, playWholeBuffer = !clip) => {
let releaseTime = endTime; let releaseTime = endTime;
if (!clip) { if (playWholeBuffer) {
releaseTime = t + (end - begin) * bufferDuration; releaseTime = t + (end - begin) * bufferDuration;
} }
bufferSource.stop(releaseTime + release); bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime); releaseEnvelope(releaseTime);
}; };
return { node: envelope, stop }; const handle = { node: envelope, bufferSource, stop };
// cut groups
// TODO: sometimes, the cutting won't work for very fast triggering...
// it worked before :-/
if (cut !== undefined) {
const prev = cutGroups[cut];
if (prev) {
prev.stop(time, false);
}
cutGroups[cut] = handle;
}
return handle;
} }
/*const getSoundfontKey = (s) => { /*const getSoundfontKey = (s) => {
@ -262,3 +270,4 @@ export async function onTriggerSample(t, value, bank) {
} }
return; return;
};*/ };*/
// bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);

View file

@ -6,7 +6,7 @@ export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => { ['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
setSound( setSound(
wave, wave,
(t, value) => { (t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples // destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value; const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let { n, note, freq } = value; let { n, note, freq } = value;
@ -25,6 +25,11 @@ export function registerSynthSounds() {
const g = gainNode(0.3); const g = gainNode(0.3);
// envelope // envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return { return {
node: o.connect(g).connect(envelope), node: o.connect(g).connect(envelope),
stop: (t) => { stop: (t) => {

View file

@ -155,8 +155,10 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
} = hap.value; } = hap.value;
const { velocity = 1 } = hap.context; const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const chain = []; const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) { if (bank && s) {
s = `${bank}_${s}`; s = `${bank}_${s}`;
} }
@ -166,7 +168,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
sourceNode = source(t, hap.value); sourceNode = source(t, hap.value);
} else if (soundMap.get()[s]) { } else if (soundMap.get()[s]) {
const { onTrigger } = soundMap.get()[s]; const { onTrigger } = soundMap.get()[s];
const soundHandle = await onTrigger(t, hap.value); const soundHandle = await onTrigger(t, hap.value, onended);
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration); soundHandle.stop(t + hapDuration);
@ -183,6 +185,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
logger('[webaudio] skip hap: still loading', ac.currentTime - t); logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return; return;
} }
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode); chain.push(sourceNode);
// gain stage // gain stage
@ -227,8 +230,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
// connect chain elements together // connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// disconnect all nodes when source node has ended: // toDisconnect = all the node that should be disconnected in onended callback
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect()); // this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend]);
}; };
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps); export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);