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) // 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 const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback) { export function nanFallback(value, fallback = 0) {
if (isNaN(Number(value))) { if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback; return fallback;

View File

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

View File

@ -1,33 +1,5 @@
import { getAudioContext } from './superdough.mjs'; import { getAudioContext } from './superdough.mjs';
import { clamp } from './util.mjs'; import { clamp, nanFallback } 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);
}
};
export function gainNode(value) { export function gainNode(value) {
const node = getAudioContext().createGain(); const node = getAudioContext().createGain();
@ -35,44 +7,13 @@ export function gainNode(value) {
return node; return node;
} }
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future const getSlope = (y1, y2, x1, x2) => {
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => { const denom = x2 - x1;
const gainNode = getAudioContext().createGain(); if (denom === 0) {
let phase = begin; return 0;
gainNode.gain.setValueAtTime(0, begin); }
phase += attack; return (y2 - y1) / (x2 - x1);
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;
},
};
}; };
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 = ( export const getParamADSR = (
param, param,
attack, attack,
@ -86,23 +27,47 @@ export const getParamADSR = (
//exponential works better for frequency modulations (such as filter cutoff) due to human ear perception //exponential works better for frequency modulations (such as filter cutoff) due to human ear perception
curve = 'exponential', curve = 'exponential',
) => { ) => {
attack = nanFallback(attack);
decay = nanFallback(decay);
sustain = nanFallback(sustain);
release = nanFallback(release);
const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime';
let phase = begin; if (curve === 'exponential') {
min = Math.max(0.0001, min);
}
const range = max - min; const range = max - min;
const peak = min + range; 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); param.setValueAtTime(min, begin);
phase += attack; if (attack > duration) {
//attack
//attack param[ramp](envValAtTime(duration), end);
param[ramp](peak, phase); } else if (attack + decay > duration) {
phase += decay; //attack
const sustainLevel = min + sustain * range; param[ramp](envValAtTime(attack), begin + attack);
//decay
//decay param[ramp](envValAtTime(duration), end);
param[ramp](sustainLevel, phase); } else {
//attack
setRelease(param, phase, sustainLevel, end, end + release, min, curve); 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) { export function getCompressor(ac, threshold, ratio, knee, attack, release) {

View File

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

View File

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

View File

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

View File

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