Lots of examples; use id instead of index; waveshaper clamping; one mode for synth

This commit is contained in:
Aria 2025-12-29 12:59:48 -06:00
parent e79b4b8ccb
commit 680b7d78ec
6 changed files with 228 additions and 38 deletions

View file

@ -8,6 +8,7 @@ import { getAudioContext } from './audioContext.mjs';
import { gainNode, getEnvelope, getLfo, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { getSuperdoughControlTargets } from './superdoughdata.mjs';
import { clamp } from './util.mjs';
const getNodeParam = (node, name) => {
// Worklet case
@ -27,9 +28,8 @@ const getNodeParam = (node, name) => {
const controlTargets = getSuperdoughControlTargets();
const stripIndex = (control) => control?.replace(/\d+$/, '');
const getControlData = (control) => {
return controlTargets[stripIndex(control)];
return controlTargets[control.split('_')[0]];
};
const getRangeForParam = (paramName, currentValue) => {
@ -41,6 +41,19 @@ const getRangeForParam = (paramName, currentValue) => {
return { min: undefined, max: undefined };
};
const clampWithWaveShaper = (modulator, min, max) => {
const ac = getAudioContext();
const curve = new Float32Array(256);
for (let i = 0; i < curve.length; i++) {
const x = (i / (curve.length - 1)) * 2 - 1;
curve[i] = clamp(x * max, min, max);
}
const shaper = new WaveShaperNode(ac, { curve });
const scaleGain = gainNode(1 / max);
modulator.connect(scaleGain).connect(shaper);
return { modulator, toCleanup: [shaper, scaleGain] };
};
const getTargetParamsForControl = (control, nodes, subControl) => {
const lookupKey = subControl ? `${control}_${subControl}` : control;
const targetInfo = getControlData(lookupKey) ?? getControlData(control);
@ -67,7 +80,7 @@ const getTargetParamsForControl = (control, nodes, subControl) => {
return { targetParams: audioParams, paramName };
};
export const connectLFO = (idx, params, nodeTracker) => {
export const connectLFO = (id, params, nodeTracker) => {
const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl);
if (!targetParams.length) return;
@ -84,12 +97,12 @@ export const connectLFO = (idx, params, nodeTracker) => {
max,
};
const lfoNode = getLfo(getAudioContext(), modParams);
nodeTracker[`lfo${idx}`] = [lfoNode];
nodeTracker[`lfo_${id}`] = [lfoNode];
targetParams.forEach((t) => lfoNode.connect(t));
return lfoNode;
};
export const connectEnvelope = (idx, params, nodeTracker) => {
export const connectEnvelope = (id, params, nodeTracker) => {
const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl);
if (!targetParams.length) return;
@ -106,7 +119,7 @@ export const connectEnvelope = (idx, params, nodeTracker) => {
decayCurve: dcurve,
releaseCurve: rcurve,
});
nodeTracker[`env${idx}`] = [envNode];
nodeTracker[`env_${id}`] = [envNode];
targetParams.forEach((t) => envNode.connect(t));
return envNode;
};
@ -122,13 +135,12 @@ export const connectBusModulator = (params, nodeTracker, controller) => {
const shifted = dc.connect(gainNode(1));
signal.connect(shifted);
let currentValue = targetParams[0].value;
debugger;
currentValue = currentValue === 0 ? 1 : currentValue;
const { min, max } = getRangeForParam(paramName, currentValue);
const depthValue = depthabs != null ? depthabs : depth * currentValue;
const maxAbsDepth = Math.min(Math.abs(min), Math.abs(max));
const boundedDepth = Math.min(Math.abs(depthValue), maxAbsDepth) || Math.abs(depthValue);
const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3));
const depthGain = gainNode((Math.sign(depthValue) * Math.abs(depthValue)) / 0.3);
const unClamped = shifted.connect(depthGain);
let { modulator, toCleanup } = clampWithWaveShaper(unClamped, min, max);
webAudioTimeout(
ac,
() => {
@ -137,5 +149,6 @@ export const connectBusModulator = (params, nodeTracker, controller) => {
0,
params.begin,
);
return { modulator, toCleanup: [dc, shifted, modulator] };
toCleanup.push(dc, shifted, depthGain);
return { modulator, toCleanup };
};

View file

@ -875,9 +875,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// finally, now that `nodes` is populated, set up modulators
if (value.lfo) {
for (const [idx, params] of Object.entries(value.lfo)) {
for (const id of value.lfo.__ids) {
const params = value.lfo[id];
const lfo = connectLFO(
idx,
id,
{
...params,
cps,
@ -891,9 +892,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
}
if (value.env) {
for (const [idx, params] of Object.entries(value.env)) {
for (const id of value.env.__ids) {
const params = value.env[id];
const env = connectEnvelope(
idx,
id,
{
...params,
begin: t,
@ -905,8 +907,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
}
if (value.bmod) {
for (const p of value.bmod) {
const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, controller);
for (const id of value.bmod.__ids) {
const params = value.bmod[id];
const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller);
audioNodes.push(...toCleanup);
}
}

View file

@ -19,7 +19,7 @@ import {
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
const waveformAliases = [
['tri', 'triangle'],
['sqr', 'square'],
@ -508,8 +508,17 @@ export function getOscillator(s, t, value, onended) {
s = 'triangle';
}
s = s === 'user' && !partials ? 'triangle' : s;
// If no partials are given, use stock waveforms
if (!partials || partials?.length === 0 || s === 'sine') {
if (s === 'one') {
// Constant 1 oscillator (used for modulation)
o = new ConstantSourceNode(getAudioContext(), { offset: 1 });
o.start(t);
return {
node: o,
nodes: { source: o },
stop: (time) => o?.stop(time),
};
} else if (!partials || partials?.length === 0 || s === 'sine') {
// If no partials are given, use stock waveforms
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
}