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
+6 -9
View File
@@ -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 {
+3 -3
View File
@@ -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 },
);
+5 -1
View File
@@ -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)),
};
}
+3 -3
View File
@@ -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) {
+17 -16
View File
@@ -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);
+37
View File
@@ -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() {
+26 -26
View File
@@ -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);
},
+14 -5
View File
@@ -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();
+1 -1
View File
@@ -75,7 +75,7 @@ export const getZZFX = (value, t) => {
source.start(t);
return {
node: source,
source,
nodes: { source: [source] },
};
};