Extend to FM, vibrato, etc

This commit is contained in:
Aria 2025-12-28 20:13:15 -05:00
parent 99bec835f8
commit 48f6a41683
9 changed files with 112 additions and 64 deletions

View file

@ -2870,36 +2870,33 @@ registerSubControls('bmod', [
]); ]);
Pattern.prototype.modulate = function (type, config, idx) { Pattern.prototype.modulate = function (type, config, idx) {
if (config == null || typeof config !== 'object') { config ??= { control: undefined };
return this;
}
const modulatorKeys = ['lfo', 'env', 'bmod']; const modulatorKeys = ['lfo', 'env', 'bmod'];
if (!modulatorKeys.includes(type)) { if (!modulatorKeys.includes(type)) {
logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`);
return this; return this;
} }
let output = this; let output = this;
let defaultValue = {}; let defaultValue = undefined;
let defaultSet = 'control' in config;
for (const [rawKey, value] of Object.entries(config)) { for (const [rawKey, value] of Object.entries(config)) {
const key = getMainSubcontrolName(type, rawKey); const key = getMainSubcontrolName(type, rawKey);
const valuePat = reify(value); const valuePat = reify(value);
output = output output = output
.fmap((v) => (c) => { .fmap((v) => (c) => {
if (!defaultSet) { if (defaultValue === undefined) {
// default control to the control set just before this in the chain // default control to the control set just before this in the chain
// e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO
let control = getControlName(Object.keys(v).at(-1)); let control = getControlName(Object.keys(v).at(-1));
if (modulatorKeys.includes(control)) { if (modulatorKeys.includes(control)) {
control = `${control}${v[control].length - 1}`; control = `${control}${v[control].length - 1}`;
} }
defaultValue = { control }; defaultValue = control;
defaultSet = true;
} }
v[type] ??= []; v[type] ??= [];
const t = v[type]; const t = v[type];
idx ??= t.length; idx ??= t.length;
t[idx] ??= defaultValue; t[idx] ??= { control: defaultValue };
if (c === undefined) return v;
if (key === 'control' || key === 'subControl') { if (key === 'control' || key === 'subControl') {
t[idx][key] = getControlName(c); t[idx][key] = getControlName(c);
} else { } else {

View file

@ -166,7 +166,7 @@ export function registerSoundfonts() {
let envEnd = holdEnd + release + 0.01; let envEnd = holdEnd + release + 0.01;
// vibrato // vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time); const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time);
// pitch envelope // pitch envelope
getPitchEnvelope(bufferSource.detune, value, time, holdEnd); getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
@ -174,10 +174,10 @@ export function registerSoundfonts() {
const stop = (releaseTime) => {}; const stop = (releaseTime) => {};
onceEnded(bufferSource, () => { onceEnded(bufferSource, () => {
releaseAudioNode(bufferSource); releaseAudioNode(bufferSource);
releaseAudioNode(vibratoOscillator); vibratoHandle?.stop();
onended(); onended();
}); });
return { node, stop, source: bufferSource }; return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } };
}, },
{ type: 'soundfont', prebake: true, fonts }, { type: 'soundfont', prebake: true, fonts },
); );

View file

@ -351,7 +351,7 @@ export function getVibratoOscillator(param, value, t) {
releaseAudioNode(vibratoOscillator); releaseAudioNode(vibratoOscillator);
}); });
vibratoOscillator.start(t); 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 ac = getAudioContext();
const toStop = []; // fm oscillators we will expose `stop` for const toStop = []; // fm oscillators we will expose `stop` for
const fms = {}; const fms = {};
const nodes = {};
// Matrix // Matrix
for (let i = 1; i <= 8; i++) { for (let i = 1; i <= 8; i++) {
for (let j = 0; j <= 8; j++) { for (let j = 0; j <= 8; j++) {
@ -457,11 +458,13 @@ export function applyFM(param, value, begin) {
output = osc.connect(envGain); output = osc.connect(envGain);
} }
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup }; fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
nodes[`fm_${idx}`] = [osc];
} }
const { input, output, freq, osc, toCleanup } = fms[idx]; const { input, output, freq, osc, toCleanup } = fms[idx];
const g = gainNode(amt * freq); const g = gainNode(amt * freq);
io.push(isMod ? output.connect(g) : input); io.push(isMod ? output.connect(g) : input);
cleanupOnEnd(osc, [...toCleanup, g]); cleanupOnEnd(osc, [...toCleanup, g]);
nodes[`fm_${idx}_gain`] = [g];
} }
if (!io[1]) { if (!io[1]) {
logger( logger(
@ -474,6 +477,7 @@ export function applyFM(param, value, begin) {
} }
} }
return { return {
nodes,
stop: (t) => toStop.forEach((m) => m?.stop(t)), stop: (t) => toStop.forEach((m) => m?.stop(t)),
}; };
} }

View file

@ -301,7 +301,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
} }
// vibrato // vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t);
const time = t + nudge; const time = t + nudge;
bufferSource.start(time, offset); bufferSource.start(time, offset);
@ -324,7 +324,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
node.connect(out); node.connect(out);
onceEnded(bufferSource, function () { onceEnded(bufferSource, function () {
releaseAudioNode(bufferSource); releaseAudioNode(bufferSource);
releaseAudioNode(vibratoOscillator); vibratoHandle?.stop();
releaseAudioNode(node); releaseAudioNode(node);
releaseAudioNode(out); releaseAudioNode(out);
onended(); onended();
@ -334,7 +334,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const stop = (endTime) => { const stop = (endTime) => {
bufferSource.stop(endTime); bufferSource.stop(endTime);
}; };
const handle = { node: out, source: bufferSource, stop }; const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop };
// cut groups // cut groups
if (cut !== undefined) { if (cut !== undefined) {

View file

@ -532,7 +532,7 @@ function mapChannelNumbers(channels) {
} }
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { 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 // new: t is always expected to be the absolute target onset time
const ac = getAudioContext(); const ac = getAudioContext();
const audioController = getSuperdoughAudioController(); const audioController = getSuperdoughAudioController();
@ -690,7 +690,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
nodes['source'] = [soundHandle.source]; nodes = { ...nodes, ...soundHandle.nodes };
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@ -709,9 +709,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(sourceNode); chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
transient !== undefined && if (transient !== undefined) {
chain.push( const transProcessor = getWorklet(
getWorklet(
ac, ac,
'transient-processor', 'transient-processor',
{}, {},
@ -723,8 +722,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
end: endWithRelease, end: endWithRelease,
}, },
}, },
),
); );
chain.push(transProcessor);
nodes['transient'] = transProcessor;
}
// gain stage // gain stage
const initialGain = gainNode(gain); const initialGain = gainNode(gain);

View file

@ -23,6 +23,9 @@ const CONTROL_TARGETS = {
lfo_sync: { node: 'lfo', param: 'frequency' }, lfo_sync: { node: 'lfo', param: 'frequency' },
lfo_depth: { node: 'lfo', param: 'depth' }, lfo_depth: { node: 'lfo', param: 'depth' },
lfo_depthabs: { 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: { node: 'env', param: 'depth' },
env_attack: { node: 'env', param: 'attack' }, env_attack: { node: 'env', param: 'attack' },
env_decay: { node: 'env', param: 'decay' }, env_decay: { node: 'env', param: 'decay' },
@ -107,6 +110,40 @@ const CONTROL_TARGETS = {
warp: { node: 'source', param: 'warp' }, warp: { node: 'source', param: 'warp' },
freq: { node: 'source', param: 'frequency' }, freq: { node: 'source', param: 'frequency' },
note: { 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() { export function getSuperdoughControlTargets() {

View file

@ -52,17 +52,17 @@ export function registerSynthSounds() {
// turn down // turn down
const g = gainNode(0.3); const g = gainNode(0.3);
let sound = getOscillator(s, t, value, () => { const sound = getOscillator(s, t, value, () => {
releaseAudioNode(g); releaseAudioNode(g);
onended(); onended();
}); });
let { node: o, stop, triggerRelease } = sound; const { node: o, nodes, stop, triggerRelease } = sound;
const { duration } = value; const { duration } = value;
const envGain = gainNode(1); const envGain = gainNode(1);
let node = o.connect(g).connect(envGain); const node = o.connect(g).connect(envGain);
const holdEnd = t + duration; const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01; const envEnd = holdEnd + release + 0.01;
@ -70,7 +70,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
source: o, nodes,
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@ -140,7 +140,7 @@ export function registerSynthSounds() {
return { return {
node, node,
source: o, nodes: { source: [o] },
stop: (endTime) => { stop: (endTime) => {
o.stop(endTime); o.stop(endTime);
}, },
@ -185,8 +185,8 @@ export function registerSynthSounds() {
const gainAdjustment = 1 / Math.sqrt(voices); const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin); const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1); let envGain = gainNode(1);
envGain = o.connect(envGain); envGain = o.connect(envGain);
@ -197,8 +197,8 @@ export function registerSynthSounds() {
() => { () => {
releaseAudioNode(o); releaseAudioNode(o);
onended(); onended();
fm?.stop(); fmHandle?.stop();
vibratoOscillator?.stop(); vibratoHandle?.stop();
}, },
begin, begin,
end, end,
@ -206,7 +206,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
source: o, nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -333,25 +333,25 @@ export function registerSynthSounds() {
); );
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin); const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1); let envGain = gainNode(1);
envGain = o.connect(envGain); envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let lfo; let pw_lfo;
if (pwsweep != 0) { if (pwsweep != 0) {
lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end });
lfo.connect(o.parameters.get('pulsewidth')); pw_lfo.connect(o.parameters.get('pulsewidth'));
} }
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
releaseAudioNode(o); releaseAudioNode(o);
releaseAudioNode(lfo); releaseAudioNode(pw_lfo);
onended(); onended();
fm?.stop(); fmHandle?.stop();
vibratoOscillator?.stop(); vibratoHandle?.stop();
}, },
begin, begin,
end, end,
@ -359,7 +359,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
source: o, nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -394,7 +394,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
source: bus, nodes: { source: [bus] },
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -440,7 +440,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
source: o, nodes: { source: [o] },
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@ -520,11 +520,11 @@ export function getOscillator(s, t, value, onended) {
// set frequency // set frequency
o.frequency.value = getFrequencyFromValue(value); o.frequency.value = getFrequencyFromValue(value);
let vibratoOscillator = getVibratoOscillator(o.detune, value, t); const vibratoHandle = getVibratoOscillator(o.detune, value, t);
// pitch envelope // pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration); getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t); const fmHandle = applyFM(o.frequency, value, t);
let noiseMix; let noiseMix;
if (noise) { if (noise) {
@ -541,10 +541,10 @@ export function getOscillator(s, t, value, onended) {
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
source: o, nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes },
stop: (time) => { stop: (time) => {
fmModulator.stop(time); fmHandle.stop(time);
vibratoOscillator?.stop(time); vibratoHandle?.stop(time);
noiseMix?.stop(time); noiseMix?.stop(time);
o.stop(time); o.stop(time);
}, },

View file

@ -314,19 +314,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
dcoffset: value.warpdc ?? 0, dcoffset: value.warpdc ?? 0,
}, },
); );
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t);
const fm = applyFM(source.parameters.get('frequency'), value, t); const fmHandle = applyFM(source.parameters.get('frequency'), value, t);
const envGain = ac.createGain(); const envGain = ac.createGain();
const node = source.connect(envGain); const node = source.connect(envGain);
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); 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( const timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
releaseAudioNode(source); releaseAudioNode(source);
releaseAudioNode(vibratoOscillator); vibratoHandle?.stop();
fm?.stop(); fmHandle?.stop();
releaseAudioNode(wtPosModulators); releaseAudioNode(wtPosModulators);
releaseAudioNode(wtWarpModulators); releaseAudioNode(wtWarpModulators);
onended(); onended();

View file

@ -75,7 +75,7 @@ export const getZZFX = (value, t) => {
source.start(t); source.start(t);
return { return {
node: source, node: source,
source, nodes: { source: [source] },
}; };
}; };