fixed all the things

This commit is contained in:
Jade (Rose) Rowland 2024-01-05 01:00:22 -05:00
parent a2974099c1
commit 2ee392be9b
7 changed files with 145 additions and 150 deletions

View File

@ -86,7 +86,7 @@ export const midi2note = (n) => {
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback) {
export function nanFallback(value, fallback = 0) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;

View File

@ -1,5 +1,5 @@
import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope, getADSRValues } from '@strudel.cycles/webaudio';
import { getAudioContext, registerSound, getParamADSR, getADSRValues } from '@strudel.cycles/webaudio';
import gm from './gm.mjs';
let loadCache = {};
@ -136,23 +136,27 @@ export function registerSoundfonts() {
value.sustain,
value.release,
]);
const { duration } = value;
const n = getSoundIndex(value.n, fonts.length);
const font = fonts[n];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
const silentAt = releaseEnvelope(releaseTime);
bufferSource.stop(silentAt);
};
const envGain = ctx.createGain();
const node = bufferSource.connect(envGain);
const holdEnd = time + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, time, holdEnd, 'linear');
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (releaseTime) => {};
bufferSource.onended = () => {
bufferSource.disconnect();
envelope.disconnect();
node.disconnect();
onended();
};
return { node: envelope, stop };
return { node, stop };
},
{ type: 'soundfont', prebake: true, fonts },
);

View File

@ -1,33 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp } from './util.mjs';
const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = 'linear') => {
const ctx = getAudioContext();
const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime';
// if the decay stage is complete before the note event is done, we don't need to do anything special
if (phase < startTime) {
param.setValueAtTime(sustain, startTime);
param[ramp](endValue, endTime);
} else if (param.cancelAndHoldAtTime == null) {
//this replicates cancelAndHoldAtTime behavior for Firefox
setTimeout(
() => {
//sustain at current value
const currValue = param.value;
param.cancelScheduledValues(0);
param.setValueAtTime(currValue, 0);
//release
param[ramp](endValue, endTime);
},
(startTime - ctx.currentTime) * 1000,
);
} else {
//stop the envelope, hold the value, and then set the release stage
param.cancelAndHoldAtTime(startTime);
param[ramp](endValue, endTime);
}
};
import { clamp, nanFallback } from './util.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
@ -35,44 +7,13 @@ export function gainNode(value) {
return node;
}
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
let phase = begin;
gainNode.gain.setValueAtTime(0, begin);
phase += attack;
gainNode.gain.linearRampToValueAtTime(velocity, phase); // attack
phase += decay;
let sustainLevel = sustain * velocity;
gainNode.gain.linearRampToValueAtTime(sustainLevel, phase); // decay / sustain
// sustain end
return {
node: gainNode,
stop: (t) => {
const endTime = t + release;
setRelease(gainNode.gain, phase, sustainLevel, t, endTime, 0);
// helps prevent pops from overlapping sounds
return endTime;
},
};
const getSlope = (y1, y2, x1, x2) => {
const denom = x2 - x1;
if (denom === 0) {
return 0;
}
return (y2 - y1) / (x2 - x1);
};
export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) => {
sustain = Math.max(0.001, sustain);
velocity = Math.max(0.001, velocity);
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0.0001, begin);
gainNode.gain.exponentialRampToValueAtTime(velocity, begin + attack);
gainNode.gain.exponentialRampToValueAtTime(sustain * velocity, begin + attack + decay);
return {
node: gainNode,
stop: (t) => {
// similar to getEnvelope, this will glitch if sustain level has not been reached
gainNode.gain.exponentialRampToValueAtTime(0.0001, t + release);
},
};
};
export const getParamADSR = (
param,
attack,
@ -86,23 +27,47 @@ export const getParamADSR = (
//exponential works better for frequency modulations (such as filter cutoff) due to human ear perception
curve = 'exponential',
) => {
attack = nanFallback(attack);
decay = nanFallback(decay);
sustain = nanFallback(sustain);
release = nanFallback(release);
const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime';
let phase = begin;
if (curve === 'exponential') {
min = Math.max(0.0001, min);
}
const range = max - min;
const peak = min + range;
const sustainVal = min + sustain * range;
const duration = end - begin;
const envValAtTime = (time) => {
if (attack > time) {
return time * getSlope(min, peak, 0, attack) + 0;
} else {
return (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
}
};
param.setValueAtTime(min, begin);
phase += attack;
//attack
param[ramp](peak, phase);
phase += decay;
const sustainLevel = min + sustain * range;
//decay
param[ramp](sustainLevel, phase);
setRelease(param, phase, sustainLevel, end, end + release, min, curve);
if (attack > duration) {
//attack
param[ramp](envValAtTime(duration), end);
} else if (attack + decay > duration) {
//attack
param[ramp](envValAtTime(attack), begin + attack);
//decay
param[ramp](envValAtTime(duration), end);
} else {
//attack
param[ramp](envValAtTime(attack), begin + attack);
//decay
param[ramp](envValAtTime(attack + decay), begin + attack + decay);
//sustain
param.setValueAtTime(sustainVal, end);
}
//release
param[ramp](min, end + release);
};
export function getCompressor(ac, threshold, ratio, knee, attack, release) {

View File

@ -1,6 +1,6 @@
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { getADSRValues, getEnvelope } from './helpers.mjs';
import { getADSRValues, getParamADSR } from './helpers.mjs';
import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
@ -243,6 +243,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
begin = 0,
loopEnd = 1,
end = 1,
duration,
vib,
vibmod = 0.5,
} = value;
@ -255,7 +256,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
@ -299,25 +300,29 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
bufferSource.start(time, offset);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const envGain = ac.createGain();
const node = bufferSource.connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
envelope.connect(out);
node.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
vibratoOscillator?.stop();
envelope.disconnect();
node.disconnect();
out.disconnect();
onended();
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
releaseTime = t + (end - begin) * bufferDuration;
}
const silentAt = releaseEnvelope(releaseTime);
bufferSource.stop(silentAt);
// did not reimplement this behavior, because it mostly seems to confuse people...
// if (playWholeBuffer) {
// const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
// envEnd = t + (end - begin) * bufferDuration;
// }
// bufferSource.stop(envEnd);
};
const handle = { node: out, bufferSource, stop };

View File

@ -1,6 +1,6 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getADSRValues, getEnvelope, getExpEnvelope } from './helpers.mjs';
import { gainNode, getADSRValues, getParamADSR } from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
@ -43,26 +43,30 @@ export function registerSynthSounds() {
let { density } = value;
sound = getNoiseOscillator(s, t, density);
}
let { node: o, stop, triggerRelease } = sound;
// turn down
const g = gainNode(0.3);
// gain envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
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: o.connect(g).connect(envelope),
stop: (releaseTime) => {
const silentAt = releaseEnvelope(releaseTime);
triggerRelease?.(releaseTime);
stop(silentAt);
},
node,
stop: (releaseTime) => {},
};
},
{ type: 'synth', prebake: true },
@ -122,6 +126,7 @@ export function getOscillator(
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
},
) {
let ac = getAudioContext();
@ -151,26 +156,38 @@ export function getOscillator(
o.start(t);
// FM
let stopFm, fmEnvelope;
let stopFm;
let envGain = ac.createGain();
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = t + duration;
// let envEnd = holdEnd + release + 0.01;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
t,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
envGain.maxValue = fmModulationIndex * 2;
envGain.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
modulator.connect(envGain);
envGain.connect(o.frequency);
}
stopFm = stop;
}
@ -202,7 +219,7 @@ export function getOscillator(
o.stop(time);
},
triggerRelease: (time) => {
fmEnvelope?.stop(time);
// envGain?.stop(time);
},
};
}

View File

@ -54,7 +54,7 @@ export const valueToMidi = (value, fallbackValue) => {
return fallbackValue;
};
export function nanFallback(value, fallback) {
export function nanFallback(value, fallback = 0) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;

View File

@ -57,32 +57,36 @@ export function SoundsTab() {
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
{soundEntries.map(([name, { data, onTrigger }]) => (
<span
key={name}
className="cursor-pointer hover:opacity-50"
onMouseDown={async () => {
const ctx = getAudioContext();
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
clip: 1,
release: 0.5,
};
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
trigRef.current.then((ref) => {
connectToDestination(ref?.node);
});
}}
>
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span>
))}
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
key={name}
className="cursor-pointer hover:opacity-50"
onMouseDown={async () => {
const ctx = getAudioContext();
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
clip: 1,
release: 0.5,
sustain: 1,
duration: 0.5,
};
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
trigRef.current.then((ref) => {
connectToDestination(ref?.node);
});
}}
>
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span>
);
})}
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
</div>
</div>