Lots of examples; use id instead of index; waveshaper clamping; one mode for synth
This commit is contained in:
parent
e79b4b8ccb
commit
680b7d78ec
6 changed files with 228 additions and 38 deletions
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue