mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-23 03:28:33 +00:00
encapsulate out
+ encapsulate destination + error handling to simplify merge scheduler branch
This commit is contained in:
parent
82cb6c93cf
commit
6dbb2cb98a
@ -22,6 +22,21 @@ export const getAudioContext = () => {
|
|||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let destination;
|
||||||
|
export const getDestination = () => {
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
if (!destination) {
|
||||||
|
destination = ctx.createGain();
|
||||||
|
destination.connect(ctx.destination);
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const panic = () => {
|
||||||
|
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||||
|
destination = null;
|
||||||
|
};
|
||||||
|
|
||||||
const getFilter = (type, frequency, Q) => {
|
const getFilter = (type, frequency, Q) => {
|
||||||
const filter = getAudioContext().createBiquadFilter();
|
const filter = getAudioContext().createBiquadFilter();
|
||||||
filter.type = type;
|
filter.type = type;
|
||||||
@ -87,7 +102,7 @@ const getSampleBufferSource = async (s, n, note) => {
|
|||||||
}
|
}
|
||||||
const bank = samples?.[s];
|
const bank = samples?.[s];
|
||||||
if (!bank) {
|
if (!bank) {
|
||||||
throw new Error('sample not found:', s, 'try one of ' + Object.keys(samples));
|
throw new Error(`sample not found: "${s}", try one of ${Object.keys(samples).join(', ')}`);
|
||||||
}
|
}
|
||||||
if (typeof bank !== 'object') {
|
if (typeof bank !== 'object') {
|
||||||
throw new Error('wrong format for sample bank:', s);
|
throw new Error('wrong format for sample bank:', s);
|
||||||
@ -155,170 +170,174 @@ try {
|
|||||||
|
|
||||||
const cutGroups = [];
|
const cutGroups = [];
|
||||||
|
|
||||||
Pattern.prototype.out = function () {
|
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||||
return this.onTrigger(async (t, hap, ct, cps) => {
|
export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||||
const hapDuration = hap.duration / cps;
|
try {
|
||||||
try {
|
const ac = getAudioContext();
|
||||||
const ac = getAudioContext();
|
if (typeof hap.value !== 'object') {
|
||||||
// calculate correct time (tone.js workaround)
|
throw new Error(
|
||||||
t = ac.currentTime + t - ct;
|
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||||
// destructure value
|
);
|
||||||
let {
|
|
||||||
freq,
|
|
||||||
s,
|
|
||||||
sf,
|
|
||||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
|
||||||
n = 0,
|
|
||||||
note,
|
|
||||||
gain = 1,
|
|
||||||
cutoff,
|
|
||||||
resonance = 1,
|
|
||||||
hcutoff,
|
|
||||||
hresonance = 1,
|
|
||||||
bandf,
|
|
||||||
bandq = 1,
|
|
||||||
coarse,
|
|
||||||
crush,
|
|
||||||
shape,
|
|
||||||
pan,
|
|
||||||
attack = 0.001,
|
|
||||||
decay = 0.001,
|
|
||||||
sustain = 1,
|
|
||||||
release = 0.001,
|
|
||||||
speed = 1, // sample playback speed
|
|
||||||
begin = 0,
|
|
||||||
end = 1,
|
|
||||||
vowel,
|
|
||||||
unit,
|
|
||||||
nudge = 0, // TODO: is this in seconds?
|
|
||||||
cut,
|
|
||||||
loop,
|
|
||||||
} = hap.value;
|
|
||||||
const { velocity = 1 } = hap.context;
|
|
||||||
gain *= velocity; // legacy fix for velocity
|
|
||||||
// the chain will hold all audio nodes that connect to each other
|
|
||||||
const chain = [];
|
|
||||||
if (typeof s === 'string') {
|
|
||||||
[s, n] = splitSN(s, n);
|
|
||||||
}
|
|
||||||
if (typeof note === 'string') {
|
|
||||||
[note, n] = splitSN(note, n);
|
|
||||||
}
|
|
||||||
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
|
||||||
// with synths, n and note are the same thing
|
|
||||||
n = note || n || 36;
|
|
||||||
if (typeof n === 'string') {
|
|
||||||
n = toMidi(n); // e.g. c3 => 48
|
|
||||||
}
|
|
||||||
// get frequency
|
|
||||||
if (!freq && typeof n === 'number') {
|
|
||||||
freq = fromMidi(n); // + 48);
|
|
||||||
}
|
|
||||||
// make oscillator
|
|
||||||
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
|
|
||||||
chain.push(o);
|
|
||||||
// level down oscillators as they are really loud compared to samples i've tested
|
|
||||||
const g = ac.createGain();
|
|
||||||
g.gain.value = 0.3;
|
|
||||||
chain.push(g);
|
|
||||||
// TODO: make adsr work with samples without pops
|
|
||||||
// envelope
|
|
||||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
|
|
||||||
chain.push(adsr);
|
|
||||||
} else {
|
|
||||||
// load sample
|
|
||||||
if (speed === 0) {
|
|
||||||
// no playback
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!s) {
|
|
||||||
console.warn('no sample specified');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const soundfont = getSoundfontKey(s);
|
|
||||||
let bufferSource;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (soundfont) {
|
|
||||||
// is soundfont
|
|
||||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
|
||||||
} else {
|
|
||||||
// is sample from loaded samples(..)
|
|
||||||
bufferSource = await getSampleBufferSource(s, n, note);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// asny stuff above took too long?
|
|
||||||
if (ac.currentTime > t) {
|
|
||||||
console.warn('sample still loading:', s, n);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!bufferSource) {
|
|
||||||
console.warn('no buffer source');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
|
||||||
if (unit === 'c') {
|
|
||||||
// are there other units?
|
|
||||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
|
|
||||||
}
|
|
||||||
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
|
||||||
// "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,
|
|
||||||
// the midway point through a 10-second audio buffer is still 5."
|
|
||||||
const offset = begin * duration * bufferSource.playbackRate.value;
|
|
||||||
duration = (end - begin) * duration;
|
|
||||||
if (loop) {
|
|
||||||
bufferSource.loop = true;
|
|
||||||
bufferSource.loopStart = offset;
|
|
||||||
bufferSource.loopEnd = offset + duration;
|
|
||||||
duration = loop * duration;
|
|
||||||
}
|
|
||||||
t += nudge;
|
|
||||||
|
|
||||||
bufferSource.start(t, offset);
|
|
||||||
if (cut !== undefined) {
|
|
||||||
cutGroups[cut]?.stop(t); // fade out?
|
|
||||||
cutGroups[cut] = bufferSource;
|
|
||||||
}
|
|
||||||
chain.push(bufferSource);
|
|
||||||
bufferSource.stop(t + duration + release);
|
|
||||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
|
||||||
chain.push(adsr);
|
|
||||||
}
|
|
||||||
// master out
|
|
||||||
const master = ac.createGain();
|
|
||||||
master.gain.value = gain;
|
|
||||||
chain.push(master);
|
|
||||||
|
|
||||||
// filters
|
|
||||||
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
|
||||||
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
|
||||||
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
|
||||||
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
|
||||||
// TODO delay / delaytime / delayfeedback
|
|
||||||
// panning
|
|
||||||
if (pan !== undefined) {
|
|
||||||
const panner = ac.createStereoPanner();
|
|
||||||
panner.pan.value = 2 * pan - 1;
|
|
||||||
chain.push(panner);
|
|
||||||
}
|
|
||||||
// master out
|
|
||||||
/* const master = ac.createGain();
|
|
||||||
master.gain.value = 0.8 * gain;
|
|
||||||
chain.push(master); */
|
|
||||||
chain.push(ac.destination);
|
|
||||||
// connect chain elements together
|
|
||||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
|
||||||
// disconnect all nodes when source node has ended:
|
|
||||||
chain[0].onended = () => chain.forEach((n) => n.disconnect());
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('.out error:', e);
|
|
||||||
}
|
}
|
||||||
});
|
// calculate correct time (tone.js workaround)
|
||||||
|
let t = ac.currentTime + deadline;
|
||||||
|
// destructure value
|
||||||
|
let {
|
||||||
|
freq,
|
||||||
|
s,
|
||||||
|
sf,
|
||||||
|
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||||
|
n = 0,
|
||||||
|
note,
|
||||||
|
gain = 1,
|
||||||
|
cutoff,
|
||||||
|
resonance = 1,
|
||||||
|
hcutoff,
|
||||||
|
hresonance = 1,
|
||||||
|
bandf,
|
||||||
|
bandq = 1,
|
||||||
|
coarse,
|
||||||
|
crush,
|
||||||
|
shape,
|
||||||
|
pan,
|
||||||
|
attack = 0.001,
|
||||||
|
decay = 0.001,
|
||||||
|
sustain = 1,
|
||||||
|
release = 0.001,
|
||||||
|
speed = 1, // sample playback speed
|
||||||
|
begin = 0,
|
||||||
|
end = 1,
|
||||||
|
vowel,
|
||||||
|
unit,
|
||||||
|
nudge = 0, // TODO: is this in seconds?
|
||||||
|
cut,
|
||||||
|
loop,
|
||||||
|
} = hap.value;
|
||||||
|
const { velocity = 1 } = hap.context;
|
||||||
|
gain *= velocity; // legacy fix for velocity
|
||||||
|
// the chain will hold all audio nodes that connect to each other
|
||||||
|
const chain = [];
|
||||||
|
if (typeof s === 'string') {
|
||||||
|
[s, n] = splitSN(s, n);
|
||||||
|
}
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
[note, n] = splitSN(note, n);
|
||||||
|
}
|
||||||
|
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||||
|
// with synths, n and note are the same thing
|
||||||
|
n = note || n || 36;
|
||||||
|
if (typeof n === 'string') {
|
||||||
|
n = toMidi(n); // e.g. c3 => 48
|
||||||
|
}
|
||||||
|
// get frequency
|
||||||
|
if (!freq && typeof n === 'number') {
|
||||||
|
freq = fromMidi(n); // + 48);
|
||||||
|
}
|
||||||
|
// make oscillator
|
||||||
|
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
|
||||||
|
chain.push(o);
|
||||||
|
// level down oscillators as they are really loud compared to samples i've tested
|
||||||
|
const g = ac.createGain();
|
||||||
|
g.gain.value = 0.3;
|
||||||
|
chain.push(g);
|
||||||
|
// TODO: make adsr work with samples without pops
|
||||||
|
// envelope
|
||||||
|
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
|
||||||
|
chain.push(adsr);
|
||||||
|
} else {
|
||||||
|
// load sample
|
||||||
|
if (speed === 0) {
|
||||||
|
// no playback
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!s) {
|
||||||
|
console.warn('no sample specified');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const soundfont = getSoundfontKey(s);
|
||||||
|
let bufferSource;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (soundfont) {
|
||||||
|
// is soundfont
|
||||||
|
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
||||||
|
} else {
|
||||||
|
// is sample from loaded samples(..)
|
||||||
|
bufferSource = await getSampleBufferSource(s, n, note);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// asny stuff above took too long?
|
||||||
|
if (ac.currentTime > t) {
|
||||||
|
console.warn('sample still loading:', s, n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!bufferSource) {
|
||||||
|
console.warn('no buffer source');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||||
|
if (unit === 'c') {
|
||||||
|
// are there other units?
|
||||||
|
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
|
||||||
|
}
|
||||||
|
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||||
|
// "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,
|
||||||
|
// the midway point through a 10-second audio buffer is still 5."
|
||||||
|
const offset = begin * duration * bufferSource.playbackRate.value;
|
||||||
|
duration = (end - begin) * duration;
|
||||||
|
if (loop) {
|
||||||
|
bufferSource.loop = true;
|
||||||
|
bufferSource.loopStart = offset;
|
||||||
|
bufferSource.loopEnd = offset + duration;
|
||||||
|
duration = loop * duration;
|
||||||
|
}
|
||||||
|
t += nudge;
|
||||||
|
|
||||||
|
bufferSource.start(t, offset);
|
||||||
|
if (cut !== undefined) {
|
||||||
|
cutGroups[cut]?.stop(t); // fade out?
|
||||||
|
cutGroups[cut] = bufferSource;
|
||||||
|
}
|
||||||
|
chain.push(bufferSource);
|
||||||
|
bufferSource.stop(t + duration + release);
|
||||||
|
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
||||||
|
chain.push(adsr);
|
||||||
|
}
|
||||||
|
// master out
|
||||||
|
const master = ac.createGain();
|
||||||
|
master.gain.value = gain;
|
||||||
|
chain.push(master);
|
||||||
|
|
||||||
|
// filters
|
||||||
|
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
||||||
|
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
||||||
|
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
||||||
|
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
||||||
|
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
|
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
||||||
|
// TODO delay / delaytime / delayfeedback
|
||||||
|
// panning
|
||||||
|
if (pan !== undefined) {
|
||||||
|
const panner = ac.createStereoPanner();
|
||||||
|
panner.pan.value = 2 * pan - 1;
|
||||||
|
chain.push(panner);
|
||||||
|
}
|
||||||
|
// connect chain elements together
|
||||||
|
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||||
|
chain[chain.length - 1].connect(getDestination());
|
||||||
|
// disconnect all nodes when source node has ended:
|
||||||
|
chain[0].onended = () => chain.forEach((n) => n.disconnect());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('.out error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.out = function () {
|
||||||
|
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
||||||
|
return this.onTrigger((t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps));
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user