Mostly working version

This commit is contained in:
Aria 2025-11-19 19:44:02 -06:00
parent 0dbca05de0
commit 19b710c401
9 changed files with 56 additions and 70 deletions

View file

@ -2320,7 +2320,6 @@ export const { curve } = registerControl('curve');
export const { deltaSlide } = registerControl('deltaSlide'); export const { deltaSlide } = registerControl('deltaSlide');
export const { pitchJump } = registerControl('pitchJump'); export const { pitchJump } = registerControl('pitchJump');
export const { pitchJumpTime } = registerControl('pitchJumpTime'); export const { pitchJumpTime } = registerControl('pitchJumpTime');
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
// noise on the frequency or as bubo calls it "frequency fog" :) // noise on the frequency or as bubo calls it "frequency fog" :)
export const { znoise } = registerControl('znoise'); export const { znoise } = registerControl('znoise');
export const { zmod } = registerControl('zmod'); export const { zmod } = registerControl('zmod');

View file

@ -3710,7 +3710,7 @@ addConfigAlias('lfo', 'target', 't');
addConfigAlias('lfo', 'param', 'p'); addConfigAlias('lfo', 'param', 'p');
addConfigAlias('lfo', 'rate', 'r'); addConfigAlias('lfo', 'rate', 'r');
addConfigAlias('lfo', 'depth', 'dep', 'dp', 'd'); addConfigAlias('lfo', 'depth', 'dep', 'dp', 'd');
addConfigAlias('lfo', 'dc'); addConfigAlias('lfo', 'dcoffset', 'dc');
addConfigAlias('lfo', 'shape', 'sh'); addConfigAlias('lfo', 'shape', 'sh');
addConfigAlias('lfo', 'skew', 'sk'); addConfigAlias('lfo', 'skew', 'sk');
addConfigAlias('lfo', 'curve', 'c'); addConfigAlias('lfo', 'curve', 'c');
@ -3730,7 +3730,7 @@ addConfigAlias('send', 'depth', 'dep', 'dp', 'd');
addConfigAlias('send', 'dc'); addConfigAlias('send', 'dc');
addConfigAlias('send', 'offset', 'off', 'o'); addConfigAlias('send', 'offset', 'off', 'o');
Pattern.prototype.mod = function (type, config, idx) { Pattern.prototype.modulate = function (type, config, idx) {
if (config == null || typeof config !== 'object') { if (config == null || typeof config !== 'object') {
return this; return this;
} }
@ -3766,7 +3766,7 @@ Pattern.prototype.mod = function (type, config, idx) {
* @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r
* @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp, d * @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp, d
* @param {number | Pattern} [config.dc] DC offset / bias for the waveform * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
* @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh
* @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c
@ -3775,7 +3775,7 @@ Pattern.prototype.mod = function (type, config, idx) {
* @returns Pattern * @returns Pattern
*/ */
Pattern.prototype.lfo = function (config, idx) { Pattern.prototype.lfo = function (config, idx) {
return this.mod('lfo', config, idx); return this.modulate('lfo', config, idx);
}; };
export const lfo = (config) => pure({}).lfo(config); export const lfo = (config) => pure({}).lfo(config);
@ -3799,7 +3799,7 @@ export const lfo = (config) => pure({}).lfo(config);
* @returns Pattern * @returns Pattern
*/ */
Pattern.prototype.env = function (config, idx) { Pattern.prototype.env = function (config, idx) {
return this.mod('env', config, idx); return this.modulate('env', config, idx);
}; };
export const env = (config) => pure({}).env(config); export const env = (config) => pure({}).env(config);
@ -3820,6 +3820,6 @@ export const env = (config) => pure({}).env(config);
* @returns Pattern * @returns Pattern
*/ */
Pattern.prototype.send = function (config, idx) { Pattern.prototype.send = function (config, idx) {
return this.mod('send', config, idx); return this.modulate('send', config, idx);
}; };
export const send = (config) => pure({}).send(config); export const send = (config) => pure({}).send(config);

View file

@ -176,7 +176,7 @@ export function registerSoundfonts() {
node.disconnect(); node.disconnect();
onended(); onended();
}; };
return { node, stop }; return { node, stop, source: bufferSource };
}, },
{ type: 'soundfont', prebake: true, fonts }, { type: 'soundfont', prebake: true, fonts },
); );

View file

@ -105,22 +105,16 @@ function getModulationShapeInput(val) {
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0; return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
} }
export function getLfo(audioContext, begin, end, properties = {}) { export function getEnvelope(audioContext, properties = {}) {
const { shape = 0, ...props } = properties; return getWorklet(audioContext, 'envelope-processor', properties);
const { dcoffset = -0.5, depth = 1 } = properties; }
export function getLfo(audioContext, properties = {}) {
// Extract some params we need for deriving other params
const { begin, shape = 0, ...props } = properties;
const lfoprops = { const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin, time: begin,
begin,
end,
shape: getModulationShapeInput(shape), shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props, ...props,
}; };

View file

@ -331,7 +331,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, bufferSource, stop }; const handle = { node: out, source: bufferSource, stop };
// cut groups // cut groups
if (cut !== undefined) { if (cut !== undefined) {

View file

@ -16,13 +16,14 @@ import {
getADSRValues, getADSRValues,
getCompressor, getCompressor,
getDistortion, getDistortion,
getEnvelope,
getLfo, getLfo,
getParamADSR, getParamADSR,
getWorklet, getWorklet,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { errorLogger, logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs'; import { loadBuffer } from './sampler.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs';
@ -292,18 +293,6 @@ function getSuperdoughAudioController() {
return controller; return controller;
} }
export function getLfo(audioContext, properties = {}) {
// Extract some params we need for deriving other params
const { begin, shape = 0, ...props } = properties;
const lfoprops = {
time: begin,
shape: getModulationShapeInput(shape),
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function connectToDestination(input, channels) { export function connectToDestination(input, channels) {
const controller = getSuperdoughAudioController(); const controller = getSuperdoughAudioController();
controller.output.connectToDestination(input, channels); controller.output.connectToDestination(input, channels);
@ -445,7 +434,7 @@ function _getTargetParams(nodes, target) {
if (!targetNodes) { if (!targetNodes) {
const keys = Object.keys(nodes); const keys = Object.keys(nodes);
errorLogger( errorLogger(
new Error(`Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`), `Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`,
'superdough', 'superdough',
); );
return []; return [];
@ -456,9 +445,7 @@ function _getTargetParams(nodes, target) {
if (!targetParam) { if (!targetParam) {
const available = _getNodeParams(targetNode); const available = _getNodeParams(targetNode);
errorLogger( errorLogger(
new Error( `Could not connect to parameter '${param}' on '${target}'. Available parameters: ${available.join(', ')}`,
`Could not connect to parameter '${param}' on '${targetName}'. Available parameters: ${available.join(', ')}`,
),
'superdough', 'superdough',
); );
return; return;
@ -472,26 +459,27 @@ function connectLFO(idx, params, nodeTracker) {
const { rate = 1, sync, cps, target, ...filteredParams } = params; const { rate = 1, sync, cps, target, ...filteredParams } = params;
filteredParams['frequency'] = sync !== undefined ? sync / cps : rate; filteredParams['frequency'] = sync !== undefined ? sync / cps : rate;
const ac = getAudioContext(); const ac = getAudioContext();
lfoNode = getLfo(ac, filteredParams); const lfoNode = getLfo(ac, filteredParams);
nodeTracker[`lfo${idx}`] = [lfoNode]; nodeTracker[`lfo${idx}`] = [lfoNode];
_getTargetParams(target).forEach(lfoNode.connect); _getTargetParams(nodeTracker, target).forEach((t) => lfoNode.connect(t));
} }
function connectEnvelope(idx, params, nodeTracker) { function connectEnvelope(idx, params, nodeTracker) {
const { target, ...filteredParams } = params; const { target, ...filteredParams } = params;
const ac = getAudioContext(); const ac = getAudioContext();
envNode = getEnvelope(ac, filteredParams); const envNode = getEnvelope(ac, filteredParams);
nodeTracker[`env${idx}`] = [envNode]; nodeTracker[`env${idx}`] = [envNode];
_getTargetParams(nodeTracker, target).forEach(envNode.connect); _getTargetParams(nodeTracker, target).forEach((t) => envNode.connect(t));
} }
function connectSendModulator(params, signal, nodeTracker, pendingConnections) { function connectSendModulator(params, signal, nodeTracker, chainID) {
const ac = getAudioContext();
const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 });
dc.start(t); dc.start(params.begin);
const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 }); const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 });
offset.start(t); offset.start(params.begin);
const raw = dc.connect(gainNode(1)); const raw = dc.connect(gainNode(1));
const modulator = post const modulator = signal
.connect(raw) .connect(raw)
.connect(gainNode((params.depth ?? 1) / 0.3)) .connect(gainNode((params.depth ?? 1) / 0.3))
.connect(gainNode(1)); .connect(gainNode(1));
@ -499,7 +487,7 @@ function connectSendModulator(params, signal, nodeTracker, pendingConnections) {
webAudioTimeout( webAudioTimeout(
ac, ac,
() => { () => {
_getTargetParams(nodeTracker, params.target).forEach(signal.connect); _getTargetParams(nodeTracker, params.target).forEach((t) => modulator.connect(t));
}, },
0, 0,
params.begin, params.begin,
@ -507,12 +495,13 @@ function connectSendModulator(params, signal, nodeTracker, pendingConnections) {
webAudioTimeout( webAudioTimeout(
ac, ac,
() => { () => {
signal.disconnect(); modulator.disconnect();
delete pendingConnections[params.id][chainID]; delete pendingConnections[params.id][chainID];
}, },
0, 0,
params.end + 0.05, params.end + 0.05,
); );
return modulator;
} }
let activeSoundSources = new Map(); let activeSoundSources = new Map();
@ -679,7 +668,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.oscillator]; nodes['source'] = [soundHandle.source];
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@ -761,7 +750,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
hpParams.type = 'highpass'; hpParams.type = 'highpass';
const hp1 = filt(hpParams); const hp1 = filt(hpParams);
nodes['hpf'] = [hp1]; nodes['hpf'] = [hp1];
chain.push(hp()); chain.push(hp1);
if (ftype === '24db') { if (ftype === '24db') {
const hp2 = filt(hpParams); const hp2 = filt(hpParams);
nodes['hpf'].push(hp1); nodes['hpf'].push(hp1);
@ -791,7 +780,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const bpParams = pickAndRename(value, bpMap); const bpParams = pickAndRename(value, bpMap);
bpParams.type = 'bandpass'; bpParams.type = 'bandpass';
const bp1 = filt(bpParams); const bp1 = filt(bpParams);
chain.push(bp()); chain.push(bp1);
if (ftype === '24db') { if (ftype === '24db') {
const bp2 = filt(bpParams); const bp2 = filt(bpParams);
nodes['bpf'].push(bp2); nodes['bpf'].push(bp2);
@ -946,7 +935,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// finally, now that `nodes` is populated, set up modulators // finally, now that `nodes` is populated, set up modulators
if (value.lfo) { if (value.lfo) {
for (const [params, idx] of Object.entries(value.lfo)) { for (const [idx, params] of Object.entries(value.lfo)) {
connectLFO( connectLFO(
idx, idx,
{ {
@ -955,13 +944,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
begin: t, begin: t,
end: endWithRelease, end: endWithRelease,
}, },
'lfo',
nodes, nodes,
); );
} }
} }
if (value.env) { if (value.env) {
for (const [params, idx] of Object.entries(value.env)) { for (const [idx, params] of Object.entries(value.env)) {
connectEnvelope( connectEnvelope(
idx, idx,
{ {
@ -969,7 +957,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
begin: t, begin: t,
end: endWithRelease, end: endWithRelease,
}, },
'envelope',
nodes, nodes,
); );
} }
@ -978,22 +965,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
idToNodes[value.id] = new WeakRef(nodes); idToNodes[value.id] = new WeakRef(nodes);
} }
if (value.send) { if (value.send) {
for (const p of value.env) { for (const p of value.send) {
const modNodes = idToNodes[p.id]; const modNodes = idToNodes[p.id];
if (!modNodes) { if (!modNodes) {
logger( logger(
`[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`, `[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`,
); );
} else { } else {
connectSendModulator(params, post); const modulator = connectSendModulator({ ...p, begin: t, end: endWithRelease }, post, nodes, chainID);
pendingConnections[p.id] ??= {}; pendingConnections[p.id] ??= {};
pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]); pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]);
} }
} }
} }
if (value.id in pendingConnections) { if (value.id in pendingConnections) {
for (const data of Object.values(pendingConnections[id])) { for (const data of Object.values(pendingConnections[value.id])) {
const [modulator, target, end] = data.deref(); const derefData = data.deref();
if (!derefData) {
delete pendingConnections[value.id];
continue;
}
const [modulator, target, end] = derefData;
const targets = _getTargetParams(nodes, target); const targets = _getTargetParams(nodes, target);
webAudioTimeout( webAudioTimeout(
ac, ac,

View file

@ -71,7 +71,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
oscillator: o, source: o,
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@ -141,7 +141,7 @@ export function registerSynthSounds() {
return { return {
node, node,
oscillator: o, source: o,
stop: (endTime) => { stop: (endTime) => {
o.stop(endTime); o.stop(endTime);
}, },
@ -208,7 +208,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
oscillator: o, source: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -285,7 +285,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
oscillator: o, source: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -363,7 +363,7 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
oscillator: o, source: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@ -409,7 +409,7 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
oscillator: o, source: o,
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@ -503,7 +503,7 @@ export function getOscillator(s, t, value) {
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
oscillator: o, source: o,
stop: (time) => { stop: (time) => {
fmModulator.stop(time); fmModulator.stop(time);
vibratoOscillator?.stop(time); vibratoOscillator?.stop(time);

View file

@ -964,7 +964,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'peak', defaultValue: 1 }, { name: 'depth', defaultValue: 1 },
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 }, { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
]; ];
} }
@ -1026,7 +1026,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
const aCurve = pv(params.attackCurve, i); const aCurve = pv(params.attackCurve, i);
const dCurve = pv(params.decayCurve, i); const dCurve = pv(params.decayCurve, i);
const rCurve = pv(params.releaseCurve, i); const rCurve = pv(params.releaseCurve, i);
const peak = pv(params.peak, i); const depth = pv(params.depth, i);
const states = [ const states = [
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
{ time: attack, start: this.attackStart, target: 1, curve: aCurve }, { time: attack, start: this.attackStart, target: 1, curve: aCurve },
@ -1040,7 +1040,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
this.state = (this.state + 1) % states.length; this.state = (this.state + 1) % states.length;
time = states[this.state].time; time = states[this.state].time;
} }
out[i] = this.val * peak; out[i] = this.val * depth;
} }
return true; return true;
} }

View file

@ -74,6 +74,7 @@ export const getZZFX = (value, t) => {
source.start(t); source.start(t);
return { return {
node: source, node: source,
source,
}; };
}; };