mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 21:58:37 +00:00
Merge pull request #868 from daslyfe/envelope_improvements
Further Envelope improvements
This commit is contained in:
commit
6d0ecb9f5e
@ -234,7 +234,7 @@ const generic_params = [
|
||||
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||
*
|
||||
*/
|
||||
['decay'],
|
||||
['decay', 'dec'],
|
||||
/**
|
||||
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
|
||||
*
|
||||
@ -270,7 +270,7 @@ const generic_params = [
|
||||
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
|
||||
*
|
||||
*/
|
||||
[['bandf', 'bandq'], 'bpf', 'bp'],
|
||||
[['bandf', 'bandq', 'bpenv'], 'bpf', 'bp'],
|
||||
// TODO: in tidal, it seems to be normalized
|
||||
/**
|
||||
* Sets the **b**and-**p**ass **q**-factor (resonance).
|
||||
@ -481,7 +481,7 @@ const generic_params = [
|
||||
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
|
||||
*
|
||||
*/
|
||||
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
|
||||
[['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp'],
|
||||
|
||||
/**
|
||||
* Sets the lowpass filter envelope modulation depth.
|
||||
@ -758,7 +758,7 @@ const generic_params = [
|
||||
* .vibmod("<.25 .5 1 2 12>:8")
|
||||
*/
|
||||
[['vibmod', 'vib'], 'vmod'],
|
||||
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
|
||||
[['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp'],
|
||||
/**
|
||||
* Controls the **h**igh-**p**ass **q**-value.
|
||||
*
|
||||
@ -1394,10 +1394,20 @@ controls.adsr = register('adsr', (adsr, pat) => {
|
||||
const [attack, decay, sustain, release] = adsr;
|
||||
return pat.set({ attack, decay, sustain, release });
|
||||
});
|
||||
controls.ds = register('ds', (ds, pat) => {
|
||||
ds = !Array.isArray(ds) ? [ds] : ds;
|
||||
const [decay, sustain] = ds;
|
||||
controls.ad = register('ad', (t, pat) => {
|
||||
t = !Array.isArray(t) ? [t] : t;
|
||||
const [attack, decay = attack] = t;
|
||||
return pat.attack(attack).decay(decay);
|
||||
});
|
||||
controls.ds = register('ds', (t, pat) => {
|
||||
t = !Array.isArray(t) ? [t] : t;
|
||||
const [decay, sustain = 0] = t;
|
||||
return pat.set({ decay, sustain });
|
||||
});
|
||||
controls.ds = register('ar', (t, pat) => {
|
||||
t = !Array.isArray(t) ? [t] : t;
|
||||
const [attack, release = attack] = t;
|
||||
return pat.set({ attack, release });
|
||||
});
|
||||
|
||||
export default controls;
|
||||
|
||||
@ -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 } from '@strudel.cycles/webaudio';
|
||||
import { getAudioContext, registerSound, getParamADSR, getADSRValues } from '@strudel.cycles/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
|
||||
let loadCache = {};
|
||||
@ -130,24 +130,33 @@ export function registerSoundfonts() {
|
||||
registerSound(
|
||||
name,
|
||||
async (time, value, onended) => {
|
||||
const [attack, decay, sustain, release] = getADSRValues([
|
||||
value.attack,
|
||||
value.decay,
|
||||
value.sustain,
|
||||
value.release,
|
||||
]);
|
||||
|
||||
const { duration } = value;
|
||||
const n = getSoundIndex(value.n, fonts.length);
|
||||
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
|
||||
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, 0.3, 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,5 +1,5 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
import { clamp, nanFallback } from './util.mjs';
|
||||
|
||||
export function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
@ -7,78 +7,68 @@ 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) => {
|
||||
// to make sure the release won't begin before sustain is reached
|
||||
phase = Math.max(t, phase);
|
||||
// see https://github.com/tidalcycles/strudel/issues/522
|
||||
gainNode.gain.setValueAtTime(sustainLevel, phase);
|
||||
phase += release;
|
||||
gainNode.gain.linearRampToValueAtTime(0, phase); // release
|
||||
return phase;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
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 getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||
// for some reason, using exponential ramping creates little cracklings
|
||||
/* let t = begin;
|
||||
gainNode.gain.setValueAtTime(0, t);
|
||||
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
|
||||
const sustainGain = Math.max(sustain * velocity, 0.001);
|
||||
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
|
||||
if (end - begin < attack + decay) {
|
||||
gainNode.gain.cancelAndHoldAtTime(end);
|
||||
} else {
|
||||
gainNode.gain.setValueAtTime(sustainGain, end);
|
||||
const getSlope = (y1, y2, x1, x2) => {
|
||||
const denom = x2 - x1;
|
||||
if (denom === 0) {
|
||||
return 0;
|
||||
}
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
|
||||
return gainNode;
|
||||
return (y2 - y1) / (x2 - x1);
|
||||
};
|
||||
export const getParamADSR = (
|
||||
param,
|
||||
attack,
|
||||
decay,
|
||||
sustain,
|
||||
release,
|
||||
min,
|
||||
max,
|
||||
begin,
|
||||
end,
|
||||
//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);
|
||||
|
||||
export const getParamADSR = (param, attack, decay, sustain, release, min, max, begin, end) => {
|
||||
const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime';
|
||||
if (curve === 'exponential') {
|
||||
min = Math.max(0.0001, min);
|
||||
}
|
||||
const range = max - min;
|
||||
const peak = min + range;
|
||||
const sustainLevel = min + sustain * range;
|
||||
const peak = max;
|
||||
const sustainVal = min + sustain * range;
|
||||
const duration = end - begin;
|
||||
|
||||
const envValAtTime = (time) => {
|
||||
if (attack > time) {
|
||||
let slope = getSlope(min, peak, 0, attack);
|
||||
return time * slope + (min > peak ? min : 0);
|
||||
} else {
|
||||
return (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
|
||||
}
|
||||
};
|
||||
|
||||
param.setValueAtTime(min, begin);
|
||||
param.linearRampToValueAtTime(peak, begin + attack);
|
||||
param.linearRampToValueAtTime(sustainLevel, begin + attack + decay);
|
||||
param.setValueAtTime(sustainLevel, end);
|
||||
param.linearRampToValueAtTime(min, end + Math.max(release, 0.1));
|
||||
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) {
|
||||
@ -92,38 +82,44 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||
return new DynamicsCompressorNode(ac, options);
|
||||
}
|
||||
|
||||
export function createFilter(
|
||||
context,
|
||||
type,
|
||||
frequency,
|
||||
Q,
|
||||
attack,
|
||||
decay,
|
||||
sustain,
|
||||
release,
|
||||
fenv,
|
||||
start,
|
||||
end,
|
||||
fanchor = 0.5,
|
||||
) {
|
||||
// changes the default values of the envelope based on what parameters the user has defined
|
||||
// so it behaves more like you would expect/familiar as other synthesis tools
|
||||
// ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc.
|
||||
|
||||
export const getADSRValues = (params, curve = 'linear', defaultValues) => {
|
||||
const envmin = curve === 'exponential' ? 0.001 : 0.001;
|
||||
const releaseMin = 0.01;
|
||||
const envmax = 1;
|
||||
const [a, d, s, r] = params;
|
||||
if (a == null && d == null && s == null && r == null) {
|
||||
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
|
||||
}
|
||||
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
|
||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||
};
|
||||
|
||||
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor) {
|
||||
const curve = 'exponential';
|
||||
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||
const filter = context.createBiquadFilter();
|
||||
|
||||
filter.type = type;
|
||||
filter.Q.value = Q;
|
||||
filter.frequency.value = frequency;
|
||||
|
||||
// envelope is active when any of these values is set
|
||||
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
|
||||
// Apply ADSR to filter frequency
|
||||
if (!isNaN(fenv) && fenv !== 0) {
|
||||
const offset = fenv * fanchor;
|
||||
|
||||
const min = clamp(2 ** -offset * frequency, 0, 20000);
|
||||
const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000);
|
||||
|
||||
// console.log('min', min, 'max', max);
|
||||
|
||||
getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end);
|
||||
if (hasEnvelope !== undefined) {
|
||||
fenv = nanFallback(fenv, 1, true);
|
||||
fanchor = nanFallback(fanchor, 0, true);
|
||||
const fenvAbs = Math.abs(fenv);
|
||||
const offset = fenvAbs * fanchor;
|
||||
let min = clamp(2 ** -offset * frequency, 0, 20000);
|
||||
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
|
||||
if (fenv < 0) [min, max] = [max, min];
|
||||
getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end, curve);
|
||||
return filter;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { 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;
|
||||
@ -254,7 +255,8 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
loop = s.startsWith('wt_') ? 1 : value.loop;
|
||||
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;
|
||||
|
||||
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
//const soundfont = getSoundfontKey(s);
|
||||
const time = t + nudge;
|
||||
|
||||
@ -298,26 +300,28 @@ 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);
|
||||
if (clip == null && loop == null && value.release == null) {
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
duration = (end - begin) * bufferDuration;
|
||||
}
|
||||
let 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();
|
||||
};
|
||||
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);
|
||||
};
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (endTime, playWholeBuffer) => {};
|
||||
const handle = { node: out, bufferSource, stop };
|
||||
|
||||
// cut groups
|
||||
|
||||
@ -280,26 +280,26 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
// low pass
|
||||
cutoff,
|
||||
lpenv,
|
||||
lpattack = 0.01,
|
||||
lpdecay = 0.01,
|
||||
lpsustain = 1,
|
||||
lprelease = 0.01,
|
||||
lpattack,
|
||||
lpdecay,
|
||||
lpsustain,
|
||||
lprelease,
|
||||
resonance = 1,
|
||||
// high pass
|
||||
hpenv,
|
||||
hcutoff,
|
||||
hpattack = 0.01,
|
||||
hpdecay = 0.01,
|
||||
hpsustain = 1,
|
||||
hprelease = 0.01,
|
||||
hpattack,
|
||||
hpdecay,
|
||||
hpsustain,
|
||||
hprelease,
|
||||
hresonance = 1,
|
||||
// band pass
|
||||
bpenv,
|
||||
bandf,
|
||||
bpattack = 0.01,
|
||||
bpdecay = 0.01,
|
||||
bpsustain = 1,
|
||||
bprelease = 0.01,
|
||||
bpattack,
|
||||
bpdecay,
|
||||
bpsustain,
|
||||
bprelease,
|
||||
bandq = 1,
|
||||
channels = [1, 2],
|
||||
//phaser
|
||||
@ -333,6 +333,7 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
compressorAttack,
|
||||
compressorRelease,
|
||||
} = value;
|
||||
|
||||
gain = nanFallback(gain, 1);
|
||||
|
||||
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { gainNode, 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') => {
|
||||
@ -29,8 +29,11 @@ export function registerSynthSounds() {
|
||||
registerSound(
|
||||
s,
|
||||
(t, value, onended) => {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
|
||||
const [attack, decay, sustain, release] = getADSRValues(
|
||||
[value.attack, value.decay, value.sustain, value.release],
|
||||
'linear',
|
||||
[0.001, 0.05, 0.6, 0.01],
|
||||
);
|
||||
|
||||
let sound;
|
||||
if (waveforms.includes(s)) {
|
||||
@ -45,21 +48,24 @@ export function registerSynthSounds() {
|
||||
// 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 },
|
||||
@ -112,13 +118,14 @@ export function getOscillator(
|
||||
// fm
|
||||
fmh: fmHarmonicity = 1,
|
||||
fmi: fmModulationIndex,
|
||||
fmenv: fmEnvelopeType = 'lin',
|
||||
fmenv: fmEnvelopeType = 'exp',
|
||||
fmattack: fmAttack,
|
||||
fmdecay: fmDecay,
|
||||
fmsustain: fmSustain,
|
||||
fmrelease: fmRelease,
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
duration,
|
||||
},
|
||||
) {
|
||||
let ac = getAudioContext();
|
||||
@ -148,26 +155,30 @@ 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);
|
||||
if (fmEnvelopeType === 'exp') {
|
||||
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
fmEnvelope.node.maxValue = fmModulationIndex * 2;
|
||||
fmEnvelope.node.minValue = 0.00001;
|
||||
}
|
||||
modulator.connect(fmEnvelope.node);
|
||||
fmEnvelope.node.connect(o.frequency);
|
||||
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
|
||||
const holdEnd = t + duration;
|
||||
getParamADSR(
|
||||
envGain.gain,
|
||||
attack,
|
||||
decay,
|
||||
sustain,
|
||||
release,
|
||||
0,
|
||||
1,
|
||||
t,
|
||||
holdEnd,
|
||||
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
||||
);
|
||||
modulator.connect(envGain);
|
||||
envGain.connect(o.frequency);
|
||||
}
|
||||
stopFm = stop;
|
||||
}
|
||||
@ -199,7 +210,7 @@ export function getOscillator(
|
||||
o.stop(time);
|
||||
},
|
||||
triggerRelease: (time) => {
|
||||
fmEnvelope?.stop(time);
|
||||
// envGain?.stop(time);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,9 +54,9 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
return fallbackValue;
|
||||
};
|
||||
|
||||
export function nanFallback(value, fallback) {
|
||||
export function nanFallback(value, fallback = 0, silent) {
|
||||
if (isNaN(Number(value))) {
|
||||
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
|
||||
!silent && logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
|
||||
@ -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