diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 826174d8f..5cc53de60 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2870,36 +2870,33 @@ registerSubControls('bmod', [ ]); Pattern.prototype.modulate = function (type, config, idx) { - if (config == null || typeof config !== 'object') { - return this; - } + config ??= { control: undefined }; const modulatorKeys = ['lfo', 'env', 'bmod']; if (!modulatorKeys.includes(type)) { logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); return this; } let output = this; - let defaultValue = {}; - let defaultSet = 'control' in config; + let defaultValue = undefined; for (const [rawKey, value] of Object.entries(config)) { const key = getMainSubcontrolName(type, rawKey); const valuePat = reify(value); output = output .fmap((v) => (c) => { - if (!defaultSet) { + if (defaultValue === undefined) { // default control to the control set just before this in the chain // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO let control = getControlName(Object.keys(v).at(-1)); if (modulatorKeys.includes(control)) { control = `${control}${v[control].length - 1}`; } - defaultValue = { control }; - defaultSet = true; + defaultValue = control; } v[type] ??= []; const t = v[type]; idx ??= t.length; - t[idx] ??= defaultValue; + t[idx] ??= { control: defaultValue }; + if (c === undefined) return v; if (key === 'control' || key === 'subControl') { t[idx][key] = getControlName(c); } else { diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 81b9eb508..b4ae59828 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -166,7 +166,7 @@ export function registerSoundfonts() { let envEnd = holdEnd + release + 0.01; // vibrato - let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time); + const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time); // pitch envelope getPitchEnvelope(bufferSource.detune, value, time, holdEnd); @@ -174,10 +174,10 @@ export function registerSoundfonts() { const stop = (releaseTime) => {}; onceEnded(bufferSource, () => { releaseAudioNode(bufferSource); - releaseAudioNode(vibratoOscillator); + vibratoHandle?.stop(); onended(); }); - return { node, stop, source: bufferSource }; + return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } }; }, { type: 'soundfont', prebake: true, fonts }, ); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index fc7734ea3..8bd8b067b 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -351,7 +351,7 @@ export function getVibratoOscillator(param, value, t) { releaseAudioNode(vibratoOscillator); }); vibratoOscillator.start(t); - return vibratoOscillator; + return { stop: (t) => vibratoOscillator.stop(t), nodes: { vib: [vibratoOscillator], vib_gain: [gain] } }; } } @@ -407,6 +407,7 @@ export function applyFM(param, value, begin) { const ac = getAudioContext(); const toStop = []; // fm oscillators we will expose `stop` for const fms = {}; + const nodes = {}; // Matrix for (let i = 1; i <= 8; i++) { for (let j = 0; j <= 8; j++) { @@ -457,11 +458,13 @@ export function applyFM(param, value, begin) { output = osc.connect(envGain); } fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup }; + nodes[`fm_${idx}`] = [osc]; } const { input, output, freq, osc, toCleanup } = fms[idx]; const g = gainNode(amt * freq); io.push(isMod ? output.connect(g) : input); cleanupOnEnd(osc, [...toCleanup, g]); + nodes[`fm_${idx}_gain`] = [g]; } if (!io[1]) { logger( @@ -474,6 +477,7 @@ export function applyFM(param, value, begin) { } } return { + nodes, stop: (t) => toStop.forEach((m) => m?.stop(t)), }; } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 55f081568..819de3c78 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -301,7 +301,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { } // vibrato - let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); + const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t); const time = t + nudge; bufferSource.start(time, offset); @@ -324,7 +324,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { node.connect(out); onceEnded(bufferSource, function () { releaseAudioNode(bufferSource); - releaseAudioNode(vibratoOscillator); + vibratoHandle?.stop(); releaseAudioNode(node); releaseAudioNode(out); onended(); @@ -334,7 +334,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const stop = (endTime) => { bufferSource.stop(endTime); }; - const handle = { node: out, source: bufferSource, stop }; + const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop }; // cut groups if (cut !== undefined) { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fd198c788..8bca5b30c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -532,7 +532,7 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { - const nodes = {}; + let nodes = {}; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -690,7 +690,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (soundHandle) { sourceNode = soundHandle.node; activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC - nodes['source'] = [soundHandle.source]; + nodes = { ...nodes, ...soundHandle.nodes }; } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -709,22 +709,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); - transient !== undefined && - chain.push( - getWorklet( - ac, - 'transient-processor', - {}, - { - processorOptions: { - attack: transient, - sustain: transsustain, - begin: t, - end: endWithRelease, - }, + if (transient !== undefined) { + const transProcessor = getWorklet( + ac, + 'transient-processor', + {}, + { + processorOptions: { + attack: transient, + sustain: transsustain, + begin: t, + end: endWithRelease, }, - ), + }, ); + chain.push(transProcessor); + nodes['transient'] = transProcessor; + } // gain stage const initialGain = gainNode(gain); diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index cab829752..2ae03101a 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -23,6 +23,9 @@ const CONTROL_TARGETS = { lfo_sync: { node: 'lfo', param: 'frequency' }, lfo_depth: { node: 'lfo', param: 'depth' }, lfo_depthabs: { node: 'lfo', param: 'depth' }, + lfo_skew: { node: 'lfo', param: 'skew' }, + lfo_curve: { node: 'lfo', param: 'curve' }, + lfo_dcoffset: { node: 'lfo', param: 'dcoffset' }, env: { node: 'env', param: 'depth' }, env_attack: { node: 'env', param: 'attack' }, env_decay: { node: 'env', param: 'decay' }, @@ -107,6 +110,40 @@ const CONTROL_TARGETS = { warp: { node: 'source', param: 'warp' }, freq: { node: 'source', param: 'frequency' }, note: { node: 'source', param: 'frequency' }, + wtdc: { node: 'wt_lfo', param: 'dc' }, + wtskew: { node: 'wt_lfo', param: 'skew' }, + wtrate: { node: 'wt_lfo', param: 'frequency' }, + wtsync: { node: 'wt_lfo', param: 'frequency' }, + wtdepth: { node: 'wt_lfo', param: 'depth' }, + warpdc: { node: 'warp_lfo', param: 'dc' }, + warpskew: { node: 'warp_lfo', param: 'skew' }, + warprate: { node: 'warp_lfo', param: 'frequency' }, + warpsync: { node: 'warp_lfo', param: 'frequency' }, + warpdepth: { node: 'warp_lfo', param: 'depth' }, + fmi: { node: 'fm_1_gain', param: 'gain' }, + fmi2: { node: 'fm_2_gain', param: 'gain' }, + fmi3: { node: 'fm_3_gain', param: 'gain' }, + fmi4: { node: 'fm_4_gain', param: 'gain' }, + fmi5: { node: 'fm_5_gain', param: 'gain' }, + fmi6: { node: 'fm_6_gain', param: 'gain' }, + fmi7: { node: 'fm_7_gain', param: 'gain' }, + fmi8: { node: 'fm_8_gain', param: 'gain' }, + fmh: { node: 'fm_1', param: 'frequency' }, + fmh2: { node: 'fm_2', param: 'frequency' }, + fmh3: { node: 'fm_3', param: 'frequency' }, + fmh4: { node: 'fm_4', param: 'frequency' }, + fmh5: { node: 'fm_5', param: 'frequency' }, + fmh6: { node: 'fm_6', param: 'frequency' }, + fmh7: { node: 'fm_7', param: 'frequency' }, + fmh8: { node: 'fm_8', param: 'frequency' }, + pw: { node: 'source', param: 'pulsewidth' }, + pwrate: { node: 'pw_lfo', param: 'frequency' }, + pwsweep: { node: 'pw_lfo', param: 'depth' }, + vib: { node: 'vib_gain', param: 'gain' }, + vibmod: { node: 'vib', param: 'frequency' }, + byteBeatStartTime: { node: 'source', param: 'byteBeatStartTime' }, + spread: { node: 'source', param: 'panspread' }, + transient: { node: 'transient', param: 'attack' }, }; export function getSuperdoughControlTargets() { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 40e62f86f..d20692729 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -52,17 +52,17 @@ export function registerSynthSounds() { // turn down const g = gainNode(0.3); - let sound = getOscillator(s, t, value, () => { + const sound = getOscillator(s, t, value, () => { releaseAudioNode(g); onended(); }); - let { node: o, stop, triggerRelease } = sound; + const { node: o, nodes, stop, triggerRelease } = sound; const { duration } = value; const envGain = gainNode(1); - let node = o.connect(g).connect(envGain); + const 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; @@ -70,7 +70,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, - source: o, + nodes, stop: (endTime) => { stop(endTime); }, @@ -140,7 +140,7 @@ export function registerSynthSounds() { return { node, - source: o, + nodes: { source: [o] }, stop: (endTime) => { o.stop(endTime); }, @@ -185,8 +185,8 @@ export function registerSynthSounds() { const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); - const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); - const fm = applyFM(o.parameters.get('frequency'), value, begin); + const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); @@ -197,8 +197,8 @@ export function registerSynthSounds() { () => { releaseAudioNode(o); onended(); - fm?.stop(); - vibratoOscillator?.stop(); + fmHandle?.stop(); + vibratoHandle?.stop(); }, begin, end, @@ -206,7 +206,7 @@ export function registerSynthSounds() { return { node: envGain, - source: o, + nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes }, stop: (time) => { timeoutNode.stop(time); }, @@ -333,25 +333,25 @@ export function registerSynthSounds() { ); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); - const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); - const fm = applyFM(o.parameters.get('frequency'), value, begin); + const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - let lfo; + let pw_lfo; if (pwsweep != 0) { - lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); - lfo.connect(o.parameters.get('pulsewidth')); + pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); + pw_lfo.connect(o.parameters.get('pulsewidth')); } let timeoutNode = webAudioTimeout( ac, () => { releaseAudioNode(o); - releaseAudioNode(lfo); + releaseAudioNode(pw_lfo); onended(); - fm?.stop(); - vibratoOscillator?.stop(); + fmHandle?.stop(); + vibratoHandle?.stop(); }, begin, end, @@ -359,7 +359,7 @@ export function registerSynthSounds() { return { node: envGain, - source: o, + nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes }, stop: (time) => { timeoutNode.stop(time); }, @@ -394,7 +394,7 @@ export function registerSynthSounds() { return { node: envGain, - source: bus, + nodes: { source: [bus] }, stop: (time) => { timeoutNode.stop(time); }, @@ -440,7 +440,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, - source: o, + nodes: { source: [o] }, stop: (endTime) => { stop(endTime); }, @@ -520,11 +520,11 @@ export function getOscillator(s, t, value, onended) { // set frequency o.frequency.value = getFrequencyFromValue(value); - let vibratoOscillator = getVibratoOscillator(o.detune, value, t); + const vibratoHandle = getVibratoOscillator(o.detune, value, t); // pitch envelope getPitchEnvelope(o.detune, value, t, t + duration); - const fmModulator = applyFM(o.frequency, value, t); + const fmHandle = applyFM(o.frequency, value, t); let noiseMix; if (noise) { @@ -541,10 +541,10 @@ export function getOscillator(s, t, value, onended) { return { node: noiseMix?.node || o, - source: o, + nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes }, stop: (time) => { - fmModulator.stop(time); - vibratoOscillator?.stop(time); + fmHandle.stop(time); + vibratoHandle?.stop(time); noiseMix?.stop(time); o.stop(time); }, diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index ebf951a49..b4fc401f1 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -314,19 +314,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { dcoffset: value.warpdc ?? 0, }, ); - const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); - const fm = applyFM(source.parameters.get('frequency'), value, t); + const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fmHandle = applyFM(source.parameters.get('frequency'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); - const handle = { node, source }; + const handle = { + node, + nodes: { + source: [source], + wt_lfo: [wtPosModulators], + warp_lfo: [wtWarpModulators], + ...fmHandle?.nodes, + ...vibratoHandle?.nodes, + }, + }; const timeoutNode = webAudioTimeout( ac, () => { releaseAudioNode(source); - releaseAudioNode(vibratoOscillator); - fm?.stop(); + vibratoHandle?.stop(); + fmHandle?.stop(); releaseAudioNode(wtPosModulators); releaseAudioNode(wtWarpModulators); onended(); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 708f34503..f3dc710b1 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -75,7 +75,7 @@ export const getZZFX = (value, t) => { source.start(t); return { node: source, - source, + nodes: { source: [source] }, }; };