mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
fixed all the things
This commit is contained in:
parent
a2974099c1
commit
2ee392be9b
@ -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;
|
||||
|
||||
@ -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 },
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user