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 { pitchJump } = registerControl('pitchJump');
export const { pitchJumpTime } = registerControl('pitchJumpTime');
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
// noise on the frequency or as bubo calls it "frequency fog" :)
export const { znoise } = registerControl('znoise');
export const { zmod } = registerControl('zmod');

View file

@ -3710,7 +3710,7 @@ addConfigAlias('lfo', 'target', 't');
addConfigAlias('lfo', 'param', 'p');
addConfigAlias('lfo', 'rate', 'r');
addConfigAlias('lfo', 'depth', 'dep', 'dp', 'd');
addConfigAlias('lfo', 'dc');
addConfigAlias('lfo', 'dcoffset', 'dc');
addConfigAlias('lfo', 'shape', 'sh');
addConfigAlias('lfo', 'skew', 'sk');
addConfigAlias('lfo', 'curve', 'c');
@ -3730,7 +3730,7 @@ addConfigAlias('send', 'depth', 'dep', 'dp', 'd');
addConfigAlias('send', 'dc');
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') {
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 {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r
* @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.skew] Waveform skew amount. Aliases: sk
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c
@ -3775,7 +3775,7 @@ Pattern.prototype.mod = function (type, config, idx) {
* @returns Pattern
*/
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);
@ -3799,7 +3799,7 @@ export const lfo = (config) => pure({}).lfo(config);
* @returns Pattern
*/
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);
@ -3820,6 +3820,6 @@ export const env = (config) => pure({}).env(config);
* @returns Pattern
*/
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);

View file

@ -176,7 +176,7 @@ export function registerSoundfonts() {
node.disconnect();
onended();
};
return { node, stop };
return { node, stop, source: bufferSource };
},
{ 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;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
export function getEnvelope(audioContext, properties = {}) {
return getWorklet(audioContext, 'envelope-processor', properties);
}
export function getLfo(audioContext, properties = {}) {
// Extract some params we need for deriving other params
const { begin, shape = 0, ...props } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};

View file

@ -331,7 +331,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const stop = (endTime) => {
bufferSource.stop(endTime);
};
const handle = { node: out, bufferSource, stop };
const handle = { node: out, source: bufferSource, stop };
// cut groups
if (cut !== undefined) {

View file

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

View file

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

View file

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

View file

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