mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 05:57:17 -04:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c407412c6 | |||
| 4c99f4866b | |||
| 099288545e | |||
| 5738020ac2 | |||
| 1478ff51a7 | |||
| 8314023c7a | |||
| d50af71ba1 | |||
| 0664f90178 | |||
| 4321814d36 | |||
| 6610713018 | |||
| 600ab0a83e | |||
| e01621d0cc | |||
| 196f70ab6b | |||
| 1d4f2b81d8 | |||
| d28c8dac8e | |||
| eb18648a2a | |||
| 0ec9826de5 | |||
| af7855402f | |||
| 68b1cb87ca | |||
| 7a3e0fd3fe | |||
| 4061dd4cf8 | |||
| 6a4fd27f5b | |||
| a6008e2700 | |||
| 654c1a86f0 | |||
| ac3f477f30 | |||
| 573cd1ae41 | |||
| 4c49cd9297 | |||
| 70d2da81d8 | |||
| bd73d96847 | |||
| 7cf9db4872 | |||
| c9381a11f9 |
+171
-12
@@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) {
|
|||||||
return bag;
|
return bag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerMultiControl(names, maxControls, ...aliases) {
|
||||||
|
names = Array.isArray(names) ? names : [names];
|
||||||
|
let bag = {};
|
||||||
|
for (let i = 1; i <= maxControls; i++) {
|
||||||
|
let theseAliases = [...aliases];
|
||||||
|
let theseNames = [...names];
|
||||||
|
if (i === 1) {
|
||||||
|
// adds e.g. fm1 as an alias for fm
|
||||||
|
const aliases1 = theseAliases.map((a) => `${a}1`);
|
||||||
|
const names1 = theseNames.map((n) => `${n}1`);
|
||||||
|
theseAliases = theseAliases.concat(aliases1).concat(names1);
|
||||||
|
} else {
|
||||||
|
theseAliases = theseAliases.map((a) => `${a}${i}`);
|
||||||
|
theseNames = theseNames.map((n) => `${n}${i}`);
|
||||||
|
}
|
||||||
|
const subBag = registerControl(theseNames, ...theseAliases);
|
||||||
|
bag = { ...bag, ...subBag };
|
||||||
|
}
|
||||||
|
return bag;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
||||||
* separated by ':'.
|
* separated by ':'.
|
||||||
@@ -435,6 +456,9 @@ export const { attack, att } = registerControl('attack', 'att');
|
|||||||
* Whole numbers and simple ratios sound more natural,
|
* Whole numbers and simple ratios sound more natural,
|
||||||
* while decimal numbers and complex ratios sound metallic.
|
* while decimal numbers and complex ratios sound metallic.
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the harmonicity of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmh2`)
|
||||||
|
*
|
||||||
* @name fmh
|
* @name fmh
|
||||||
* @param {number | Pattern} harmonicity
|
* @param {number | Pattern} harmonicity
|
||||||
* @example
|
* @example
|
||||||
@@ -444,25 +468,40 @@ export const { attack, att } = registerControl('attack', 'att');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
|
export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the Frequency Modulation of the synth.
|
* Sets the Frequency Modulation of the synth.
|
||||||
* Controls the modulation index, which defines the brightness of the sound.
|
* Controls the modulation index, which defines the brightness of the sound.
|
||||||
*
|
*
|
||||||
* @name fm
|
* A number may be added afterwards to control the modulation index of
|
||||||
|
* any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into
|
||||||
|
* each other with matrix commands like `fm13`, which would send `fm1` back into
|
||||||
|
* `fm3`
|
||||||
|
*
|
||||||
|
* @name fmi
|
||||||
* @param {number | Pattern} brightness modulation index
|
* @param {number | Pattern} brightness modulation index
|
||||||
* @synonyms fmi
|
* @synonyms fm
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
* .fm("<0 1 2 8 32>")
|
* .fm("<0 1 2 8 32>")
|
||||||
* ._scope()
|
* ._scope()
|
||||||
|
* @example
|
||||||
|
* s("sine").note("F1").seg(8)
|
||||||
|
* .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8))
|
||||||
|
* .fmh(1.06).fmh2(10).fmh3(0.1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } =
|
||||||
|
registerMultiControl(['fmi', 'fmh'], 8, 'fm');
|
||||||
|
|
||||||
// fm envelope
|
// fm envelope
|
||||||
/**
|
/**
|
||||||
* Ramp type of fm envelope. Exp might be a bit broken..
|
* Ramp type of fm envelope. Exp might be a bit broken..
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmenv4`)
|
||||||
|
*
|
||||||
* @name fmenv
|
* @name fmenv
|
||||||
* @param {number | Pattern} type lin | exp
|
* @param {number | Pattern} type lin | exp
|
||||||
* @example
|
* @example
|
||||||
@@ -474,11 +513,19 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmenv } = registerControl('fmenv');
|
export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
|
||||||
|
'fmenv',
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attack time for the FM envelope: time it takes to reach maximum modulation
|
* Attack time for the FM envelope: time it takes to reach maximum modulation
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the attack of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmatt5`)
|
||||||
|
*
|
||||||
* @name fmattack
|
* @name fmattack
|
||||||
|
* @synonyms fmatt
|
||||||
* @param {number | Pattern} time attack time
|
* @param {number | Pattern} time attack time
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
@@ -487,11 +534,33 @@ export const { fmenv } = registerControl('fmenv');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmattack } = registerControl('fmattack');
|
export const {
|
||||||
|
fmattack,
|
||||||
|
fmattack1,
|
||||||
|
fmattack2,
|
||||||
|
fmattack3,
|
||||||
|
fmattack4,
|
||||||
|
fmattack5,
|
||||||
|
fmattack6,
|
||||||
|
fmattack7,
|
||||||
|
fmattack8,
|
||||||
|
fmatt,
|
||||||
|
fmatt1,
|
||||||
|
fmatt2,
|
||||||
|
fmatt3,
|
||||||
|
fmatt4,
|
||||||
|
fmatt5,
|
||||||
|
fmatt6,
|
||||||
|
fmatt7,
|
||||||
|
fmatt8,
|
||||||
|
} = registerMultiControl('fmattack', 8, 'fmatt');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waveform of the fm modulator
|
* Waveform of the fm modulator
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the waveform
|
||||||
|
* any of the 8 individual FMs (e.g. `fmwave6`)
|
||||||
|
*
|
||||||
* @name fmwave
|
* @name fmwave
|
||||||
* @param {number | Pattern} wave waveform
|
* @param {number | Pattern} wave waveform
|
||||||
* @example
|
* @example
|
||||||
@@ -500,12 +569,19 @@ export const { fmattack } = registerControl('fmattack');
|
|||||||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmwave } = registerControl('fmwave');
|
export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl(
|
||||||
|
'fmwave',
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the decay of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmdec6`)
|
||||||
|
*
|
||||||
* @name fmdecay
|
* @name fmdecay
|
||||||
|
* @synonyms fmdec
|
||||||
* @param {number | Pattern} time decay time
|
* @param {number | Pattern} time decay time
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
@@ -515,11 +591,35 @@ export const { fmwave } = registerControl('fmwave');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmdecay } = registerControl('fmdecay');
|
export const {
|
||||||
|
fmdecay,
|
||||||
|
fmdecay1,
|
||||||
|
fmdecay2,
|
||||||
|
fmdecay3,
|
||||||
|
fmdecay4,
|
||||||
|
fmdecay5,
|
||||||
|
fmdecay6,
|
||||||
|
fmdecay7,
|
||||||
|
fmdecay8,
|
||||||
|
fmdec,
|
||||||
|
fmdec1,
|
||||||
|
fmdec2,
|
||||||
|
fmdec3,
|
||||||
|
fmdec4,
|
||||||
|
fmdec5,
|
||||||
|
fmdec6,
|
||||||
|
fmdec7,
|
||||||
|
fmdec8,
|
||||||
|
} = registerMultiControl('fmdecay', 8, 'fmdec');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
|
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the sustain of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmsus7`)
|
||||||
|
*
|
||||||
* @name fmsustain
|
* @name fmsustain
|
||||||
|
* @synonyms fmsus
|
||||||
* @param {number | Pattern} level sustain level
|
* @param {number | Pattern} level sustain level
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
@@ -529,10 +629,69 @@ export const { fmdecay } = registerControl('fmdecay');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmsustain } = registerControl('fmsustain');
|
export const {
|
||||||
// these are not really useful... skipping for now
|
fmsustain,
|
||||||
export const { fmrelease } = registerControl('fmrelease');
|
fmsustain1,
|
||||||
export const { fmvelocity } = registerControl('fmvelocity');
|
fmsustain2,
|
||||||
|
fmsustain3,
|
||||||
|
fmsustain4,
|
||||||
|
fmsustain5,
|
||||||
|
fmsustain6,
|
||||||
|
fmsustain7,
|
||||||
|
fmsustain8,
|
||||||
|
fmsus,
|
||||||
|
fmsus1,
|
||||||
|
fmsus2,
|
||||||
|
fmsus3,
|
||||||
|
fmsus4,
|
||||||
|
fmsus5,
|
||||||
|
fmsus6,
|
||||||
|
fmsus7,
|
||||||
|
fmsus8,
|
||||||
|
} = registerMultiControl('fmsustain', 8, 'fmsus');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release time for the FM envelope: how much modulation is applied after the note is released
|
||||||
|
*
|
||||||
|
* A number may be added afterwards to control the release of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmrel8`)
|
||||||
|
*
|
||||||
|
* @name fmrelease
|
||||||
|
* @synonyms fmrel
|
||||||
|
* @param {number | Pattern} time release time
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const {
|
||||||
|
fmrelease,
|
||||||
|
fmrelease1,
|
||||||
|
fmrelease2,
|
||||||
|
fmrelease3,
|
||||||
|
fmrelease4,
|
||||||
|
fmrelease5,
|
||||||
|
fmrelease6,
|
||||||
|
fmrelease7,
|
||||||
|
fmrelease8,
|
||||||
|
fmrel,
|
||||||
|
fmrel1,
|
||||||
|
fmrel2,
|
||||||
|
fmrel3,
|
||||||
|
fmrel4,
|
||||||
|
fmrel5,
|
||||||
|
fmrel6,
|
||||||
|
fmrel7,
|
||||||
|
fmrel8,
|
||||||
|
} = registerMultiControl('fmrelease', 8, 'fmrel');
|
||||||
|
|
||||||
|
// FM Matrix
|
||||||
|
// Note: we do not declare top-level exports here since it would add
|
||||||
|
// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least
|
||||||
|
// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it,
|
||||||
|
// TODO to add it explicitly / go with the globalThis approach
|
||||||
|
for (let i = 0; i <= 8; i++) {
|
||||||
|
for (let j = 0; j <= 8; j++) {
|
||||||
|
registerControl(`fmi${i}${j}`, `fm${i}${j}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getADSRValues,
|
getADSRValues,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
|
onceEnded,
|
||||||
} from '@strudel/webaudio';
|
} from '@strudel/webaudio';
|
||||||
import gm from './gm.mjs';
|
import gm from './gm.mjs';
|
||||||
|
|
||||||
@@ -170,12 +171,12 @@ export function registerSoundfonts() {
|
|||||||
|
|
||||||
bufferSource.stop(envEnd);
|
bufferSource.stop(envEnd);
|
||||||
const stop = (releaseTime) => {};
|
const stop = (releaseTime) => {};
|
||||||
bufferSource.onended = () => {
|
onceEnded(bufferSource, () => {
|
||||||
bufferSource.disconnect();
|
bufferSource.disconnect();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
return { node, stop };
|
return { node, stop };
|
||||||
},
|
},
|
||||||
{ type: 'soundfont', prebake: true, fonts },
|
{ type: 'soundfont', prebake: true, fonts },
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
audioContext.mjs - Audio Context manager
|
||||||
|
|
||||||
|
Sets up a common and accessible audio context for all superdough operations
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/audiocontext.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
let audioContext;
|
let audioContext;
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
export const setDefaultAudioContext = () => {
|
||||||
|
|||||||
+165
-85
@@ -1,7 +1,8 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
|
import { getNodeFromPool, markWorkletAsDead } from './nodePools.mjs';
|
||||||
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
@@ -36,6 +37,16 @@ export function getWorklet(ac, processor, params, config) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPooledWorklet(ac, processor, params, constructorOptions, reInitOptions) {
|
||||||
|
const node = getNodeFromPool(processor, () => new AudioWorkletNode(ac, processor, constructorOptions), params);
|
||||||
|
node.port.postMessage({ type: 'initialize', payload: reInitOptions });
|
||||||
|
node.port.onmessage = (e) => {
|
||||||
|
if (e.data.type === 'died') markWorkletAsDead(node);
|
||||||
|
node.port.onmessage = null;
|
||||||
|
};
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
export const getParamADSR = (
|
export const getParamADSR = (
|
||||||
param,
|
param,
|
||||||
attack,
|
attack,
|
||||||
@@ -124,10 +135,11 @@ export function getLfo(audioContext, begin, end, properties = {}) {
|
|||||||
...props,
|
...props,
|
||||||
};
|
};
|
||||||
|
|
||||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
return getPooledWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||||
|
const node = getNodeFromPool('compressor', () => new DynamicsCompressorNode(ac, {}));
|
||||||
const options = {
|
const options = {
|
||||||
threshold: threshold ?? -3,
|
threshold: threshold ?? -3,
|
||||||
ratio: ratio ?? 10,
|
ratio: ratio ?? 10,
|
||||||
@@ -135,7 +147,11 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
|||||||
attack: attack ?? 0.005,
|
attack: attack ?? 0.005,
|
||||||
release: release ?? 0.05,
|
release: release ?? 0.05,
|
||||||
};
|
};
|
||||||
return new DynamicsCompressorNode(ac, options);
|
const now = ac.currentTime;
|
||||||
|
Object.entries(options).forEach(([key, value]) => {
|
||||||
|
node[key].setValueAtTime(value, now);
|
||||||
|
});
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// changes the default values of the envelope based on what parameters the user has defined
|
// changes the default values of the envelope based on what parameters the user has defined
|
||||||
@@ -211,13 +227,16 @@ export function createFilter(context, start, end, params, cps, cycle) {
|
|||||||
|
|
||||||
let frequencyParam, filter;
|
let frequencyParam, filter;
|
||||||
if (model === 'ladder') {
|
if (model === 'ladder') {
|
||||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
filter = getPooledWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||||
frequencyParam = filter.parameters.get('frequency');
|
frequencyParam = filter.parameters.get('frequency');
|
||||||
} else {
|
} else {
|
||||||
filter = context.createBiquadFilter();
|
const factory = () => context.createBiquadFilter();
|
||||||
|
filter = getNodeFromPool('filter', factory);
|
||||||
filter.type = type;
|
filter.type = type;
|
||||||
filter.Q.value = q;
|
const now = context.currentTime;
|
||||||
filter.frequency.value = frequency;
|
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
|
||||||
|
filter[key].setValueAtTime(value, now);
|
||||||
|
});
|
||||||
frequencyParam = filter.frequency;
|
frequencyParam = filter.frequency;
|
||||||
}
|
}
|
||||||
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
||||||
@@ -349,25 +368,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||||||
constantNode.connect(zeroGain);
|
constantNode.connect(zeroGain);
|
||||||
|
|
||||||
// Schedule the `onComplete` callback to occur at `stopTime`
|
// Schedule the `onComplete` callback to occur at `stopTime`
|
||||||
constantNode.onended = () => {
|
onceEnded(constantNode, () => {
|
||||||
// Ensure garbage collection
|
releaseAudioNode(zeroGain);
|
||||||
try {
|
releaseAudioNode(constantNode);
|
||||||
zeroGain.disconnect();
|
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
constantNode.disconnect();
|
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
onComplete();
|
onComplete();
|
||||||
};
|
});
|
||||||
constantNode.start(startTime);
|
constantNode.start(startTime);
|
||||||
constantNode.stop(stopTime);
|
constantNode.stop(stopTime);
|
||||||
return constantNode;
|
return constantNode;
|
||||||
}
|
}
|
||||||
const mod = (freq, range = 1, type = 'sine') => {
|
|
||||||
|
const mod = (freq, type = 'sine') => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
let osc;
|
let osc;
|
||||||
if (noises.includes(type)) {
|
if (noises.includes(type)) {
|
||||||
@@ -379,69 +390,89 @@ const mod = (freq, range = 1, type = 'sine') => {
|
|||||||
osc.type = type;
|
osc.type = type;
|
||||||
osc.frequency.value = freq;
|
osc.frequency.value = freq;
|
||||||
}
|
}
|
||||||
|
|
||||||
osc.start();
|
osc.start();
|
||||||
const g = gainNode(range);
|
return osc;
|
||||||
osc.connect(g); // -range, range
|
|
||||||
return { node: g, stop: (t) => osc.stop(t), osc: osc };
|
|
||||||
};
|
};
|
||||||
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
|
||||||
|
const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
|
||||||
const carrfreq = frequencyparam.value;
|
const carrfreq = frequencyparam.value;
|
||||||
const modfreq = carrfreq * harmonicityRatio;
|
const modfreq = carrfreq * harmonicityRatio;
|
||||||
const modgain = modfreq * modulationIndex;
|
return { osc: mod(modfreq, wave), freq: modfreq };
|
||||||
return mod(modfreq, modgain, wave);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function applyFM(param, value, begin) {
|
export function applyFM(param, value, begin) {
|
||||||
const {
|
const ac = getAudioContext();
|
||||||
fmh: fmHarmonicity = 1,
|
const toStop = []; // fm oscillators we will expose `stop` for
|
||||||
fmi: fmModulationIndex,
|
const fms = {};
|
||||||
fmenv: fmEnvelopeType = 'exp',
|
// Matrix
|
||||||
fmattack: fmAttack,
|
for (let i = 1; i <= 8; i++) {
|
||||||
fmdecay: fmDecay,
|
for (let j = 0; j <= 8; j++) {
|
||||||
fmsustain: fmSustain,
|
let control;
|
||||||
fmrelease: fmRelease,
|
if (i === j + 1) {
|
||||||
fmvelocity: fmVelocity,
|
// Standard fm3 -> fm2 -> fm1 -> param usage
|
||||||
fmwave: fmWaveform = 'sine',
|
const iS = i === 1 ? '' : i;
|
||||||
duration,
|
control = `fmi${iS}`;
|
||||||
} = value;
|
} else {
|
||||||
let modulator;
|
control = `fmi${i}${j}`;
|
||||||
let stop = () => {};
|
}
|
||||||
|
const amt = value[control];
|
||||||
if (fmModulationIndex) {
|
if (!amt) continue;
|
||||||
const ac = getAudioContext();
|
let io = [];
|
||||||
const envGain = ac.createGain();
|
for (let [isMod, idx] of [
|
||||||
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
|
[true, i], // source
|
||||||
|
[false, j], // target
|
||||||
modulator = fmmod.node;
|
]) {
|
||||||
stop = fmmod.stop;
|
if (idx === 0) {
|
||||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
|
io.push(param);
|
||||||
// no envelope by default
|
continue;
|
||||||
modulator.connect(param);
|
}
|
||||||
} else {
|
if (!fms[idx]) {
|
||||||
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
|
const idxS = idx === 1 ? '' : idx;
|
||||||
const holdEnd = begin + duration;
|
const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
|
||||||
getParamADSR(
|
toStop.push(osc);
|
||||||
envGain.gain,
|
const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop`
|
||||||
attack,
|
const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
|
||||||
decay,
|
let output = osc;
|
||||||
sustain,
|
if (adsr.some((v) => v !== undefined)) {
|
||||||
release,
|
const envGain = ac.createGain();
|
||||||
0,
|
const [attack, decay, sustain, release] = getADSRValues(adsr);
|
||||||
1,
|
const holdEnd = begin + value.duration;
|
||||||
begin,
|
const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp';
|
||||||
holdEnd,
|
getParamADSR(
|
||||||
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
envGain.gain,
|
||||||
);
|
attack,
|
||||||
modulator.connect(envGain);
|
decay,
|
||||||
envGain.connect(param);
|
sustain,
|
||||||
|
release,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
begin,
|
||||||
|
holdEnd,
|
||||||
|
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
||||||
|
);
|
||||||
|
toCleanup.push(envGain);
|
||||||
|
output = osc.connect(envGain);
|
||||||
|
}
|
||||||
|
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
||||||
|
}
|
||||||
|
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||||
|
const g = gainNode(amt * freq);
|
||||||
|
io.push(isMod ? output.connect(g) : input);
|
||||||
|
cleanupOnEnd(osc, [...toCleanup, g]);
|
||||||
|
}
|
||||||
|
if (!io[1]) {
|
||||||
|
logger(
|
||||||
|
`[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
io[0].connect(io[1]);
|
||||||
}
|
}
|
||||||
fmmod.osc.onended = () => {
|
|
||||||
envGain.disconnect();
|
|
||||||
modulator.disconnect();
|
|
||||||
fmmod.osc.disconnect();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { stop };
|
return {
|
||||||
|
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saturation curves
|
// Saturation curves
|
||||||
@@ -536,8 +567,9 @@ export const getDistortionAlgorithm = (algo) => {
|
|||||||
return distortionAlgorithms[name];
|
return distortionAlgorithms[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDistortion = (distort, postgain, algorithm) => {
|
export const getDistortion = (fullParams) => {
|
||||||
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
const { algorithm, ...params } = fullParams;
|
||||||
|
return getPooledWorklet(getAudioContext(), 'distort-processor', params, {}, { algorithm });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
@@ -554,10 +586,58 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|||||||
return Number(freq);
|
return Number(freq);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroyAudioWorkletNode = (node) => {
|
// This helper should be used instead of the `node.onended = callback` pattern
|
||||||
if (node == null) {
|
// It adds a mechanism to help minimize gc retention
|
||||||
return;
|
export const onceEnded = (node, callback) => {
|
||||||
}
|
const onended = callback;
|
||||||
node.disconnect();
|
node.onended = function cleanup() {
|
||||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
onended && onended();
|
||||||
|
this.onended = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const releaseAudioNode = (node) => {
|
||||||
|
if (node == null) return;
|
||||||
|
|
||||||
|
// check we received an AudioNode
|
||||||
|
if (!(node instanceof AudioNode)) {
|
||||||
|
throw new Error('releaseAudioNode can only release an AudioNode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect
|
||||||
|
node.disconnect();
|
||||||
|
|
||||||
|
// make sure all AudioScheduledSourceNodes are in a stopped state
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
|
||||||
|
if (node instanceof AudioScheduledSourceNode) {
|
||||||
|
if (node.onended && node.onended.name !== 'cleanup') {
|
||||||
|
logger(
|
||||||
|
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
node.stop();
|
||||||
|
} catch (e) {
|
||||||
|
// At the stage, `start` was not called on the node
|
||||||
|
// but an `onended` callback releasing resources may exist
|
||||||
|
// and we want it to fire :
|
||||||
|
// - we force a start/stop cycle so that `onended` gets called
|
||||||
|
// - we `lock` the node so that no-one can start it
|
||||||
|
node.start(node.context.currentTime + 5); // will never happen
|
||||||
|
node.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing
|
||||||
|
// An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]]
|
||||||
|
// returns true and either its active source flag is true or
|
||||||
|
// any AudioNode connected to one of its inputs is actively processing.
|
||||||
|
if (node instanceof AudioWorkletNode) {
|
||||||
|
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Once the `anchor` node has ended, release all nodes in `toCleanup`
|
||||||
|
export const cleanupOnEnd = (anchor, toCleanup) => {
|
||||||
|
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
nodePools.mjs - Helper functions related to pooling and re-using audio nodes
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/nodePools.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const nodePools = new Map();
|
||||||
|
const POOL_KEY = Symbol('nodePoolKey');
|
||||||
|
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
|
||||||
|
const MAX_POOL_SIZE = 64;
|
||||||
|
|
||||||
|
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||||
|
|
||||||
|
export const releaseNodeToPool = (node) => {
|
||||||
|
node.disconnect();
|
||||||
|
if (node instanceof AudioScheduledSourceNode) {
|
||||||
|
// not reusable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node[IS_WORKLET_DEAD]) {
|
||||||
|
// Worklet already terminated, don't pool it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = node[POOL_KEY];
|
||||||
|
if (key == null) return;
|
||||||
|
const pool = nodePools.get(key) ?? [];
|
||||||
|
if (pool.length < MAX_POOL_SIZE) {
|
||||||
|
pool.push(new WeakRef(node));
|
||||||
|
nodePools.set(key, pool);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
|
||||||
|
|
||||||
|
// Attempt to get node from the pool. If this fails, fall back
|
||||||
|
// to building it with the factory
|
||||||
|
export const getNodeFromPool = (key, factory, params = {}) => {
|
||||||
|
const pool = nodePools.get(key) ?? [];
|
||||||
|
let node;
|
||||||
|
while (pool.length) {
|
||||||
|
const ref = pool.pop();
|
||||||
|
node = ref?.deref();
|
||||||
|
if (node != null && !node[IS_WORKLET_DEAD]) break;
|
||||||
|
}
|
||||||
|
if (node == null || node[IS_WORKLET_DEAD]) {
|
||||||
|
node = factory();
|
||||||
|
}
|
||||||
|
node[POOL_KEY] = key;
|
||||||
|
const paramMap = new Map();
|
||||||
|
if (node instanceof AudioWorkletNode) {
|
||||||
|
for (const [name, param] of node.parameters.entries()) {
|
||||||
|
paramMap.set(name, param);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const name of Object.getOwnPropertyNames(node)) {
|
||||||
|
const value = node[name];
|
||||||
|
if (value instanceof AudioParam) {
|
||||||
|
paramMap.set(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const now = node.context?.currentTime ?? 0;
|
||||||
|
paramMap.forEach((param, name) => {
|
||||||
|
param.cancelScheduledValues(now);
|
||||||
|
// Set values from `params` or restore defaults
|
||||||
|
const target = params[name] !== undefined ? params[name] : param.defaultValue;
|
||||||
|
param.setValueAtTime(target, now);
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
};
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { makeReusable } from './worklets-common.mjs';
|
||||||
|
|
||||||
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||||
const WEBAUDIO_BLOCK_SIZE = 128;
|
const WEBAUDIO_BLOCK_SIZE = 128;
|
||||||
|
|
||||||
/** Overlap-Add Node */
|
/** Overlap-Add Node */
|
||||||
class OLAProcessor extends AudioWorkletProcessor {
|
class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.started = false;
|
|
||||||
this.nbInputs = options.numberOfInputs;
|
this.nbInputs = options.numberOfInputs;
|
||||||
this.nbOutputs = options.numberOfOutputs;
|
this.nbOutputs = options.numberOfOutputs;
|
||||||
|
|
||||||
this.blockSize = options.processorOptions.blockSize;
|
this.blockSize = options.processorOptions.blockSize;
|
||||||
// TODO for now, the only support hop size is the size of a web audio block
|
// TODO for now, the only support hop size is the size of a web audio block
|
||||||
this.hopSize = WEBAUDIO_BLOCK_SIZE;
|
this.hopSize = WEBAUDIO_BLOCK_SIZE;
|
||||||
|
|
||||||
this.nbOverlaps = this.blockSize / this.hopSize;
|
this.nbOverlaps = this.blockSize / this.hopSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
// pre-allocate input buffers (will be reallocated if needed)
|
// pre-allocate input buffers (will be reallocated if needed)
|
||||||
this.inputBuffers = new Array(this.nbInputs);
|
this.inputBuffers = new Array(this.nbInputs);
|
||||||
this.inputBuffersHead = new Array(this.nbInputs);
|
this.inputBuffersHead = new Array(this.nbInputs);
|
||||||
@@ -54,7 +55,6 @@ class OLAProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
allocateInputChannels(inputIndex, nbChannels) {
|
allocateInputChannels(inputIndex, nbChannels) {
|
||||||
// allocate input buffers
|
// allocate input buffers
|
||||||
|
|
||||||
this.inputBuffers[inputIndex] = new Array(nbChannels);
|
this.inputBuffers[inputIndex] = new Array(nbChannels);
|
||||||
for (let i = 0; i < nbChannels; i++) {
|
for (let i = 0; i < nbChannels; i++) {
|
||||||
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
|
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
|
||||||
@@ -157,15 +157,8 @@ class OLAProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, params) {
|
processActive(inputs, outputs, params) {
|
||||||
const input = inputs[0];
|
|
||||||
const hasInput = !(input[0] === undefined);
|
|
||||||
if (this.started && !hasInput) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.started = hasInput;
|
|
||||||
this.reallocateChannelsIfNeeded(inputs, outputs);
|
this.reallocateChannelsIfNeeded(inputs, outputs);
|
||||||
|
|
||||||
this.readInputs(inputs);
|
this.readInputs(inputs);
|
||||||
this.shiftInputBuffers();
|
this.shiftInputBuffers();
|
||||||
this.prepareInputBuffersToSend();
|
this.prepareInputBuffersToSend();
|
||||||
@@ -173,7 +166,6 @@ class OLAProcessor extends AudioWorkletProcessor {
|
|||||||
this.handleOutputBuffersToRetrieve();
|
this.handleOutputBuffersToRetrieve();
|
||||||
this.writeOutputs(outputs);
|
this.writeOutputs(outputs);
|
||||||
this.shiftOutputBuffers();
|
this.shiftOutputBuffers();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import {
|
||||||
|
getADSRValues,
|
||||||
|
getParamADSR,
|
||||||
|
getPitchEnvelope,
|
||||||
|
getVibratoOscillator,
|
||||||
|
onceEnded,
|
||||||
|
releaseAudioNode,
|
||||||
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
@@ -286,17 +293,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
|
|
||||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||||
|
|
||||||
// asny stuff above took too long?
|
|
||||||
if (ac.currentTime > t) {
|
|
||||||
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
|
|
||||||
// console.warn('sample still loading:', s, n);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!bufferSource) {
|
if (!bufferSource) {
|
||||||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// async stuff above took too long?
|
||||||
|
if (ac.currentTime > t) {
|
||||||
|
logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight');
|
||||||
|
// AudioBufferSourceNode will never be used. discard it
|
||||||
|
releaseAudioNode(bufferSource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// vibrato
|
// vibrato
|
||||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||||
|
|
||||||
@@ -319,13 +328,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
|
|
||||||
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
||||||
node.connect(out);
|
node.connect(out);
|
||||||
bufferSource.onended = function () {
|
onceEnded(bufferSource, function () {
|
||||||
bufferSource.disconnect();
|
bufferSource.disconnect();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
out.disconnect();
|
out.disconnect();
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
let envEnd = holdEnd + release + 0.01;
|
let envEnd = holdEnd + release + 0.01;
|
||||||
bufferSource.stop(envEnd);
|
bufferSource.stop(envEnd);
|
||||||
const stop = (endTime) => {
|
const stop = (endTime) => {
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ import './reverb.mjs';
|
|||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
import {
|
||||||
|
createFilter,
|
||||||
|
effectSend,
|
||||||
|
gainNode,
|
||||||
|
getCompressor,
|
||||||
|
getDistortion,
|
||||||
|
getLfo,
|
||||||
|
getPooledWorklet,
|
||||||
|
} from './helpers.mjs';
|
||||||
|
import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
@@ -274,7 +283,7 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let controller;
|
let controller;
|
||||||
function getSuperdoughAudioController() {
|
export function getSuperdoughAudioController() {
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
controller = new SuperdoughAudioController(getAudioContext());
|
controller = new SuperdoughAudioController(getAudioContext());
|
||||||
}
|
}
|
||||||
@@ -294,7 +303,7 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
|
|||||||
let fOffset = 0;
|
let fOffset = 0;
|
||||||
const filterChain = [];
|
const filterChain = [];
|
||||||
for (let i = 0; i < numStages; i++) {
|
for (let i = 0; i < numStages; i++) {
|
||||||
const filter = ac.createBiquadFilter();
|
const filter = getNodeFromPool('filter', () => ac.createBiquadFilter());
|
||||||
filter.type = 'notch';
|
filter.type = 'notch';
|
||||||
filter.gain.value = 1;
|
filter.gain.value = 1;
|
||||||
filter.frequency.value = centerFrequency + fOffset;
|
filter.frequency.value = centerFrequency + fOffset;
|
||||||
@@ -506,7 +515,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
const onEnded = () => {
|
const onEnded = () => {
|
||||||
audioNodes.forEach((n) => n?.disconnect());
|
audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect()));
|
||||||
activeSoundSources.delete(chainID);
|
activeSoundSources.delete(chainID);
|
||||||
};
|
};
|
||||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||||
@@ -530,7 +539,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||||
chain.push(sourceNode);
|
chain.push(sourceNode);
|
||||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
stretch !== undefined &&
|
||||||
|
chain.push(
|
||||||
|
getPooledWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch, begin: t, end: endWithRelease }),
|
||||||
|
);
|
||||||
|
|
||||||
// gain stage
|
// gain stage
|
||||||
chain.push(gainNode(gain));
|
chain.push(gainNode(gain));
|
||||||
@@ -643,10 +655,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// effects
|
// effects
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
coarse !== undefined &&
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
chain.push(getPooledWorklet(ac, 'coarse-processor', { coarse, begin: t, end: endWithRelease }));
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
crush !== undefined && chain.push(getPooledWorklet(ac, 'crush-processor', { crush, begin: t, end: endWithRelease }));
|
||||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
shape !== undefined &&
|
||||||
|
chain.push(getPooledWorklet(ac, 'shape-processor', { shape, postgain: shapevol, begin: t, end: endWithRelease }));
|
||||||
|
distort !== undefined &&
|
||||||
|
chain.push(getDistortion({ distort, postgain: distortvol, algorithm: distorttype, begin: t, end: endWithRelease }));
|
||||||
|
|
||||||
if (tremolosync != null) {
|
if (tremolosync != null) {
|
||||||
tremolo = cps * tremolosync;
|
tremolo = cps * tremolosync;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
superdoughoutput.mjs - Output controller for superdough
|
||||||
|
|
||||||
|
Handles setting up and mixing to the outputs as well as all global (orbit) effects
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughoutput.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||||
import { errorLogger } from './logger.mjs';
|
import { errorLogger } from './logger.mjs';
|
||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
|
|||||||
@@ -3,20 +3,21 @@ import { registerSound, soundMap } from './superdough.mjs';
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
destroyAudioWorkletNode,
|
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
getFrequencyFromValue,
|
||||||
getLfo,
|
getLfo,
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
|
getPooledWorklet,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
|
||||||
noises,
|
noises,
|
||||||
|
releaseAudioNode,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
import { releaseNodeToPool } from './nodePools.mjs';
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
@@ -164,22 +165,14 @@ export function registerSynthSounds() {
|
|||||||
const end = holdend + release + 0.01;
|
const end = holdend + release + 0.01;
|
||||||
const voices = clamp(unison, 1, 100);
|
const voices = clamp(unison, 1, 100);
|
||||||
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
|
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
|
||||||
let o = getWorklet(
|
const params = {
|
||||||
ac,
|
frequency,
|
||||||
'supersaw-oscillator',
|
begin,
|
||||||
{
|
end,
|
||||||
frequency,
|
freqspread: detune,
|
||||||
begin,
|
panspread,
|
||||||
end,
|
};
|
||||||
freqspread: detune,
|
const o = getPooledWorklet(ac, 'supersaw-oscillator', params, { outputChannelCount: [2] }, { voices });
|
||||||
voices,
|
|
||||||
panspread,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
outputChannelCount: [2],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
@@ -192,8 +185,7 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
releaseNodeToPool(o);
|
||||||
envGain.disconnect();
|
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
@@ -247,7 +239,7 @@ export function registerSynthSounds() {
|
|||||||
const holdend = begin + duration;
|
const holdend = begin + duration;
|
||||||
const end = holdend + release + 0.01;
|
const end = holdend + release + 0.01;
|
||||||
|
|
||||||
let o = getWorklet(
|
let o = getPooledWorklet(
|
||||||
ac,
|
ac,
|
||||||
'byte-beat-processor',
|
'byte-beat-processor',
|
||||||
{
|
{
|
||||||
@@ -270,8 +262,7 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
releaseAudioNode(o);
|
||||||
envGain.disconnect();
|
|
||||||
onended();
|
onended();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
@@ -315,7 +306,7 @@ export function registerSynthSounds() {
|
|||||||
);
|
);
|
||||||
const holdend = begin + duration;
|
const holdend = begin + duration;
|
||||||
const end = holdend + release + 0.01;
|
const end = holdend + release + 0.01;
|
||||||
let o = getWorklet(
|
let o = getPooledWorklet(
|
||||||
ac,
|
ac,
|
||||||
'pulse-oscillator',
|
'pulse-oscillator',
|
||||||
{
|
{
|
||||||
@@ -344,9 +335,8 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
releaseAudioNode(o);
|
||||||
destroyAudioWorkletNode(lfo);
|
releaseAudioNode(lfo);
|
||||||
envGain.disconnect();
|
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
|||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
destroyAudioWorkletNode,
|
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
getFrequencyFromValue,
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
|
getPooledWorklet,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
|
import { releaseNodeToPool } from './nodePools.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export const Warpmode = Object.freeze({
|
export const Warpmode = Object.freeze({
|
||||||
@@ -225,24 +225,19 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
}
|
}
|
||||||
const endWithRelease = holdEnd + release;
|
const endWithRelease = holdEnd + release;
|
||||||
const envEnd = endWithRelease + 0.01;
|
const envEnd = endWithRelease + 0.01;
|
||||||
const source = getWorklet(
|
payload.voices = Math.max(value.unison ?? 1, 1);
|
||||||
ac,
|
const params = {
|
||||||
'wavetable-oscillator-processor',
|
begin: t,
|
||||||
{
|
end: envEnd,
|
||||||
begin: t,
|
frequency,
|
||||||
end: envEnd,
|
freqspread: value.detune,
|
||||||
frequency,
|
position: value.wt,
|
||||||
freqspread: value.detune,
|
warp: value.warp,
|
||||||
position: value.wt,
|
warpMode: warpmode,
|
||||||
warp: value.warp,
|
panspread: value.spread,
|
||||||
warpMode: warpmode,
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
};
|
||||||
panspread: value.spread,
|
const source = getPooledWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2] }, payload);
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
|
||||||
},
|
|
||||||
{ outputChannelCount: [2] },
|
|
||||||
);
|
|
||||||
source.port.postMessage({ type: 'table', payload });
|
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
return;
|
return;
|
||||||
@@ -319,10 +314,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
const timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(source);
|
releaseNodeToPool(source);
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
node.disconnect();
|
|
||||||
wtPosModulators?.disconnect();
|
wtPosModulators?.disconnect();
|
||||||
wtWarpModulators?.disconnect();
|
wtWarpModulators?.disconnect();
|
||||||
onended();
|
onended();
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
worklets-common.mjs - Common worklet code
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/worklets-common.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mixin to make class "reusable" (i.e. have a begin and end and properly handle `initialize` messages)
|
||||||
|
export const makeReusable = (Base) =>
|
||||||
|
class extends Base {
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
...(super.parameterDescriptors ?? []),
|
||||||
|
{
|
||||||
|
name: 'begin',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
automationRate: 'k-rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'end',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
automationRate: 'k-rate',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this.isAlive = true;
|
||||||
|
this.graceSeconds = options?.processorOptions?.graceSeconds ?? 0.5;
|
||||||
|
|
||||||
|
this.port.onmessage = (e) => {
|
||||||
|
const { type, payload } = e.data || {};
|
||||||
|
if (type === 'initialize') {
|
||||||
|
this.initialize(payload);
|
||||||
|
} else if (this.handlePortMessage) {
|
||||||
|
this.handlePortMessage(type, payload, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(_options) {
|
||||||
|
// defined on subclasses
|
||||||
|
}
|
||||||
|
|
||||||
|
processActive(_inputs, _outputs, _params) {
|
||||||
|
// processing subclasses should do when we're between begin/end
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, params) {
|
||||||
|
const begin = params.begin[0];
|
||||||
|
const end = params.end[0];
|
||||||
|
if (currentTime >= end + this.graceSeconds) {
|
||||||
|
if (this.isAlive) {
|
||||||
|
this.port.postMessage({ type: 'died' });
|
||||||
|
this.isAlive = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentTime < begin || currentTime >= end) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.processActive(inputs, outputs, params);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
|
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
|
||||||
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
|
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
|
||||||
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
|
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPLOYMENT
|
||||||
|
|
||||||
import OLAProcessor from './ola-processor';
|
import OLAProcessor from './ola-processor';
|
||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||||
|
import { makeReusable } from './worklets-common.mjs';
|
||||||
|
|
||||||
const blockSize = 128;
|
const blockSize = 128;
|
||||||
const PI = Math.PI;
|
const PI = Math.PI;
|
||||||
@@ -108,12 +109,12 @@ const waveshapes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const waveShapeNames = Object.keys(waveshapes);
|
const waveShapeNames = Object.keys(waveshapes);
|
||||||
class LFOProcessor extends AudioWorkletProcessor {
|
|
||||||
|
class LFOProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0 },
|
...super.parameterDescriptors,
|
||||||
{ name: 'time', defaultValue: 0 },
|
{ name: 'time', defaultValue: 0 },
|
||||||
{ name: 'end', defaultValue: 0 },
|
|
||||||
{ name: 'frequency', defaultValue: 0.5 },
|
{ name: 'frequency', defaultValue: 0.5 },
|
||||||
{ name: 'skew', defaultValue: 0.5 },
|
{ name: 'skew', defaultValue: 0.5 },
|
||||||
{ name: 'depth', defaultValue: 1 },
|
{ name: 'depth', defaultValue: 1 },
|
||||||
@@ -126,9 +127,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
initialize() {
|
||||||
super();
|
this.phase = null;
|
||||||
this.phase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementPhase(dt) {
|
incrementPhase(dt) {
|
||||||
@@ -138,15 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
processActive(_inputs, outputs, parameters) {
|
||||||
const begin = parameters['begin'][0];
|
|
||||||
if (currentTime >= parameters.end[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentTime <= begin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const frequency = parameters['frequency'][0];
|
const frequency = parameters['frequency'][0];
|
||||||
|
|
||||||
@@ -182,26 +174,14 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('lfo-processor', LFOProcessor);
|
registerProcessor('lfo-processor', LFOProcessor);
|
||||||
|
|
||||||
class CoarseProcessor extends AudioWorkletProcessor {
|
class CoarseProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [{ name: 'coarse', defaultValue: 1 }];
|
return [...super.parameterDescriptors, { name: 'coarse', defaultValue: 1 }];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
processActive(inputs, outputs, parameters) {
|
||||||
super();
|
|
||||||
this.started = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
|
||||||
const input = inputs[0];
|
const input = inputs[0];
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
|
|
||||||
const hasInput = !(input[0] === undefined);
|
|
||||||
if (this.started && !hasInput) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.started = hasInput;
|
|
||||||
|
|
||||||
let coarse = parameters.coarse[0] ?? 0;
|
let coarse = parameters.coarse[0] ?? 0;
|
||||||
coarse = Math.max(1, coarse);
|
coarse = Math.max(1, coarse);
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
@@ -214,26 +194,14 @@ class CoarseProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('coarse-processor', CoarseProcessor);
|
registerProcessor('coarse-processor', CoarseProcessor);
|
||||||
|
|
||||||
class CrushProcessor extends AudioWorkletProcessor {
|
class CrushProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [{ name: 'crush', defaultValue: 0 }];
|
return [...super.parameterDescriptors, { name: 'crush', defaultValue: 0 }];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
processActive(inputs, outputs, parameters) {
|
||||||
super();
|
|
||||||
this.started = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
|
||||||
const input = inputs[0];
|
const input = inputs[0];
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
|
|
||||||
const hasInput = !(input[0] === undefined);
|
|
||||||
if (this.started && !hasInput) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.started = hasInput;
|
|
||||||
|
|
||||||
let crush = parameters.crush[0] ?? 8;
|
let crush = parameters.crush[0] ?? 8;
|
||||||
crush = Math.max(1, crush);
|
crush = Math.max(1, crush);
|
||||||
|
|
||||||
@@ -248,29 +216,14 @@ class CrushProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('crush-processor', CrushProcessor);
|
registerProcessor('crush-processor', CrushProcessor);
|
||||||
|
|
||||||
class ShapeProcessor extends AudioWorkletProcessor {
|
class ShapeProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [...super.parameterDescriptors, { name: 'shape', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }];
|
||||||
{ name: 'shape', defaultValue: 0 },
|
|
||||||
{ name: 'postgain', defaultValue: 1 },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
processActive(inputs, outputs, parameters) {
|
||||||
super();
|
|
||||||
this.started = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
|
||||||
const input = inputs[0];
|
const input = inputs[0];
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
|
|
||||||
const hasInput = !(input[0] === undefined);
|
|
||||||
if (this.started && !hasInput) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.started = hasInput;
|
|
||||||
|
|
||||||
let shape = parameters.shape[0];
|
let shape = parameters.shape[0];
|
||||||
shape = shape < 1 ? shape : 1.0 - 4e-10;
|
shape = shape < 1 ? shape : 1.0 - 4e-10;
|
||||||
shape = (2.0 * shape) / (1.0 - shape);
|
shape = (2.0 * shape) / (1.0 - shape);
|
||||||
@@ -354,17 +307,17 @@ class DJFProcessor extends AudioWorkletProcessor {
|
|||||||
registerProcessor('djf-processor', DJFProcessor);
|
registerProcessor('djf-processor', DJFProcessor);
|
||||||
|
|
||||||
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
|
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
|
||||||
class LadderProcessor extends AudioWorkletProcessor {
|
class LadderProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
|
...super.parameterDescriptors,
|
||||||
{ name: 'frequency', defaultValue: 500 },
|
{ name: 'frequency', defaultValue: 500 },
|
||||||
{ name: 'q', defaultValue: 1 },
|
{ name: 'q', defaultValue: 1 },
|
||||||
{ name: 'drive', defaultValue: 0.69 },
|
{ name: 'drive', defaultValue: 0.69 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
initialize(_options) {
|
||||||
super();
|
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.p0 = [0, 0];
|
this.p0 = [0, 0];
|
||||||
this.p1 = [0, 0];
|
this.p1 = [0, 0];
|
||||||
@@ -375,17 +328,9 @@ class LadderProcessor extends AudioWorkletProcessor {
|
|||||||
this.p34 = [0, 0];
|
this.p34 = [0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
processActive(inputs, outputs, parameters) {
|
||||||
const input = inputs[0];
|
const input = inputs[0];
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
|
|
||||||
const hasInput = !(input[0] === undefined);
|
|
||||||
if (this.started && !hasInput) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.started = hasInput;
|
|
||||||
|
|
||||||
const resonance = parameters.q[0];
|
const resonance = parameters.q[0];
|
||||||
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
||||||
|
|
||||||
@@ -418,29 +363,18 @@ class LadderProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('ladder-processor', LadderProcessor);
|
registerProcessor('ladder-processor', LadderProcessor);
|
||||||
|
|
||||||
class DistortProcessor extends AudioWorkletProcessor {
|
class DistortProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [...super.parameterDescriptors, { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }];
|
||||||
{ name: 'distort', defaultValue: 0 },
|
|
||||||
{ name: 'postgain', defaultValue: 1 },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ processorOptions }) {
|
initialize(options) {
|
||||||
super();
|
this.algorithm = getDistortionAlgorithm(options?.algorithm);
|
||||||
this.started = false;
|
|
||||||
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
processActive(inputs, outputs, parameters) {
|
||||||
const input = inputs[0];
|
const input = inputs[0];
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
|
|
||||||
const hasInput = !(input[0] === undefined);
|
|
||||||
if (this.started && !hasInput) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.started = hasInput;
|
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||||
const shape = Math.expm1(pv(parameters.distort, n));
|
const shape = Math.expm1(pv(parameters.distort, n));
|
||||||
@@ -455,33 +389,15 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
registerProcessor('distort-processor', DistortProcessor);
|
registerProcessor('distort-processor', DistortProcessor);
|
||||||
|
|
||||||
// SUPERSAW
|
// SUPERSAW
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.phase = [];
|
|
||||||
}
|
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{
|
...super.parameterDescriptors,
|
||||||
name: 'begin',
|
|
||||||
defaultValue: 0,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'end',
|
|
||||||
defaultValue: 0,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'frequency',
|
name: 'frequency',
|
||||||
defaultValue: 440,
|
defaultValue: 440,
|
||||||
min: Number.EPSILON,
|
min: Number.EPSILON,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'panspread',
|
name: 'panspread',
|
||||||
defaultValue: 0.4,
|
defaultValue: 0.4,
|
||||||
@@ -498,25 +414,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name: 'voices',
|
|
||||||
defaultValue: 5,
|
|
||||||
min: 1,
|
|
||||||
automationRate: 'k-rate',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(_input, outputs, params) {
|
initialize(options) {
|
||||||
if (currentTime <= params.begin[0]) {
|
this.phase = [];
|
||||||
return true;
|
this.voices = options?.voices ?? 5;
|
||||||
}
|
}
|
||||||
if (currentTime >= params.end[0]) {
|
processActive(_input, outputs, params) {
|
||||||
// this.port.postMessage({ type: 'onended' });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0]; // k-rate
|
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
const detune = pv(params.detune, i);
|
const detune = pv(params.detune, i);
|
||||||
const freqspread = pv(params.freqspread, i);
|
const freqspread = pv(params.freqspread, i);
|
||||||
@@ -526,8 +431,8 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
let freq = pv(params.frequency, i);
|
let freq = pv(params.frequency, i);
|
||||||
// Main detuning
|
// Main detuning
|
||||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||||
const detuner = getDetuner(voices, freqspread);
|
const detuner = getDetuner(this.voices, freqspread);
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let n = 0; n < this.voices; n++) {
|
||||||
// Individual voice detuning
|
// Individual voice detuning
|
||||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
||||||
// We must wrap this here because it is passed into sawblep below which
|
// We must wrap this here because it is passed into sawblep below which
|
||||||
@@ -571,12 +476,7 @@ function genHannWindow(length) {
|
|||||||
|
|
||||||
class PhaseVocoderProcessor extends OLAProcessor {
|
class PhaseVocoderProcessor extends OLAProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [...super.parameterDescriptors, { name: 'pitchFactor', defaultValue: 1.0 }];
|
||||||
{
|
|
||||||
name: 'pitchFactor',
|
|
||||||
defaultValue: 1.0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
@@ -584,9 +484,13 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
blockSize: BUFFERED_BLOCK_SIZE,
|
blockSize: BUFFERED_BLOCK_SIZE,
|
||||||
};
|
};
|
||||||
super(options);
|
super(options);
|
||||||
this.timeCursor = 0;
|
|
||||||
this.fftSize = this.blockSize;
|
this.fftSize = this.blockSize;
|
||||||
this.invfftSize = 1 / this.fftSize;
|
this.invfftSize = 1 / this.fftSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(options) {
|
||||||
|
super.initialize(options);
|
||||||
|
this.timeCursor = 0;
|
||||||
this.hannWindow = genHannWindow(this.fftSize);
|
this.hannWindow = genHannWindow(this.fftSize);
|
||||||
// prepare FFT and pre-allocate buffers
|
// prepare FFT and pre-allocate buffers
|
||||||
this.fft = new FFT(this.fftSize);
|
this.fft = new FFT(this.fftSize);
|
||||||
@@ -717,9 +621,8 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
||||||
|
|
||||||
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
|
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
|
||||||
class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
class PulseOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
constructor() {
|
initialize() {
|
||||||
super();
|
|
||||||
this.phi = -PI; // phase
|
this.phi = -PI; // phase
|
||||||
this.Y0 = 0; // feedback memories
|
this.Y0 = 0; // feedback memories
|
||||||
this.Y1 = 0;
|
this.Y1 = 0;
|
||||||
@@ -731,20 +634,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{
|
...super.parameterDescriptors,
|
||||||
name: 'begin',
|
|
||||||
defaultValue: 0,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'end',
|
|
||||||
defaultValue: 0,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'frequency',
|
name: 'frequency',
|
||||||
defaultValue: 440,
|
defaultValue: 440,
|
||||||
@@ -765,20 +655,10 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, params) {
|
processActive(inputs, outputs, params) {
|
||||||
if (this.disconnected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentTime <= params.begin[0]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (currentTime >= params.end[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
let env = 1,
|
let env = 1,
|
||||||
dphi;
|
dphi;
|
||||||
|
|
||||||
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
||||||
const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
|
const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
|
||||||
const detune = pv(params.detune, i);
|
const detune = pv(params.detune, i);
|
||||||
@@ -861,28 +741,23 @@ function getByteBeatFunc(codetext) {
|
|||||||
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
|
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ByteBeatProcessor extends AudioWorkletProcessor {
|
class ByteBeatProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
constructor() {
|
initialize(options) {
|
||||||
super();
|
let codeText = options?.codeText;
|
||||||
this.port.onmessage = (event) => {
|
const byteBeatStartTime = options?.byteBeatStartTime;
|
||||||
let { codeText } = event.data;
|
if (byteBeatStartTime != null) {
|
||||||
const { byteBeatStartTime } = event.data;
|
this.t = 0;
|
||||||
if (byteBeatStartTime != null) {
|
this.initialOffset = Math.floor(byteBeatStartTime);
|
||||||
this.t = 0;
|
}
|
||||||
this.initialOffset = Math.floor(byteBeatStartTime);
|
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
|
||||||
}
|
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
|
||||||
|
codeText = codeText
|
||||||
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
|
.trim()
|
||||||
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
|
.replace(
|
||||||
codeText = codeText
|
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
|
||||||
.trim()
|
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
|
||||||
.replace(
|
);
|
||||||
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
|
this.func = getByteBeatFunc(codeText);
|
||||||
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.func = getByteBeatFunc(codeText);
|
|
||||||
};
|
|
||||||
this.initialOffset = 0;
|
this.initialOffset = 0;
|
||||||
this.t = null;
|
this.t = null;
|
||||||
this.func = null;
|
this.func = null;
|
||||||
@@ -890,12 +765,7 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{
|
...super.parameterDescriptors,
|
||||||
name: 'begin',
|
|
||||||
defaultValue: 0,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'frequency',
|
name: 'frequency',
|
||||||
defaultValue: 440,
|
defaultValue: 440,
|
||||||
@@ -907,25 +777,10 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
min: Number.NEGATIVE_INFINITY,
|
min: Number.NEGATIVE_INFINITY,
|
||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'end',
|
|
||||||
defaultValue: 0,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, params) {
|
processActive(inputs, outputs, params) {
|
||||||
if (this.disconnected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentTime <= params.begin[0]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (currentTime >= params.end[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.t == null) {
|
if (this.t == null) {
|
||||||
this.t = params.begin[0] * sampleRate;
|
this.t = params.begin[0] * sampleRate;
|
||||||
}
|
}
|
||||||
@@ -951,11 +806,9 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|
||||||
class EnvelopeProcessor extends AudioWorkletProcessor {
|
class EnvelopeProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0 },
|
|
||||||
{ name: 'end', defaultValue: 0 },
|
|
||||||
{ name: 'attack', defaultValue: 0.005, minValue: 0 },
|
{ name: 'attack', defaultValue: 0.005, minValue: 0 },
|
||||||
{ name: 'decay', defaultValue: 0.14, minValue: 0 },
|
{ name: 'decay', defaultValue: 0.14, minValue: 0 },
|
||||||
{ name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
@@ -1115,56 +968,36 @@ function brownian(x, oct = 4) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tablesCache = {};
|
const tablesCache = {};
|
||||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
class WavetableOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
...super.parameterDescriptors,
|
||||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
|
||||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||||
{ name: 'detune', defaultValue: 0 },
|
{ name: 'detune', defaultValue: 0 },
|
||||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
|
|
||||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options) {
|
initialize(options) {
|
||||||
super(options);
|
this.table = null;
|
||||||
this.frameLen = 0;
|
this.frameLen = null;
|
||||||
this.numFrames = 0;
|
this.numFrames = null;
|
||||||
this.phase = [];
|
this.phase = [];
|
||||||
|
if (options?.key) {
|
||||||
this.port.onmessage = (e) => {
|
const key = options.key;
|
||||||
const { type, payload } = e.data || {};
|
this.frameLen = options.frameLen;
|
||||||
if (type === 'table') {
|
if (!tablesCache[key]) {
|
||||||
const key = payload.key;
|
tablesCache[key] = options.frames;
|
||||||
this.frameLen = payload.frameLen;
|
|
||||||
if (!tablesCache[key]) {
|
|
||||||
const tables = [payload.frames];
|
|
||||||
let table = tables[0];
|
|
||||||
for (let level = 1; level < 1; level++) {
|
|
||||||
const nextLen = table.length >> 1;
|
|
||||||
const nextTable = table.map((frame) => {
|
|
||||||
const avg = new Float32Array(nextLen);
|
|
||||||
for (let i = 0; i < nextLen; i++) {
|
|
||||||
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
|
||||||
}
|
|
||||||
return avg;
|
|
||||||
});
|
|
||||||
tables.push(nextTable);
|
|
||||||
table = nextTable;
|
|
||||||
if (nextLen <= 32) break;
|
|
||||||
}
|
|
||||||
tablesCache[key] = tables;
|
|
||||||
}
|
|
||||||
this.tables = tablesCache[key];
|
|
||||||
this.numFrames = this.tables[0].length;
|
|
||||||
}
|
}
|
||||||
};
|
this.table = tablesCache[key];
|
||||||
|
this.numFrames = this.table.length;
|
||||||
|
}
|
||||||
|
this.voices = options?.voices ?? 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
_mirror(x) {
|
_mirror(x) {
|
||||||
@@ -1309,30 +1142,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
|
|
||||||
_chooseMip(dphi) {
|
processActive(_inputs, outputs, parameters) {
|
||||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
|
||||||
let level = 0;
|
|
||||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
|
||||||
level++;
|
|
||||||
}
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
|
||||||
if (currentTime >= parameters.end[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentTime <= parameters.begin[0]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const outL = outputs[0][0];
|
const outL = outputs[0][0];
|
||||||
const outR = outputs[0][1] || outputs[0][0];
|
const outR = outputs[0][1] || outputs[0][0];
|
||||||
if (!this.tables) {
|
if (!this.table) {
|
||||||
outL.fill(0);
|
outL.fill(0);
|
||||||
if (outR !== outL) outR.set(outL);
|
if (outR !== outL) outR.set(outL);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const voices = parameters.voices[0]; // k-rate
|
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const detune = pv(parameters.detune, i);
|
||||||
const freqspread = pv(parameters.freqspread, i);
|
const freqspread = pv(parameters.freqspread, i);
|
||||||
@@ -1343,14 +1160,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
const panspread = this.voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
let f = pv(parameters.frequency, i);
|
let f = pv(parameters.frequency, i);
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
const normalizer = 1 / Math.sqrt(voices);
|
const normalizer = 1 / Math.sqrt(this.voices);
|
||||||
const detuner = getDetuner(voices, freqspread);
|
const detuner = getDetuner(this.voices, freqspread);
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let n = 0; n < this.voices; n++) {
|
||||||
const isOdd = (n & 1) == 1;
|
const isOdd = (n & 1) == 1;
|
||||||
let gainL = gain1;
|
let gainL = gain1;
|
||||||
let gainR = gain2;
|
let gainR = gain2;
|
||||||
@@ -1361,14 +1178,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
||||||
const dPhase = fVoice * INVSR;
|
const dPhase = fVoice * INVSR;
|
||||||
const level = this._chooseMip(dPhase);
|
|
||||||
const table = this.tables[level];
|
|
||||||
|
|
||||||
// warp phase then sample
|
// warp phase then sample
|
||||||
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
const s0 = this._sampleFrame(this.table[fIdx], ph);
|
||||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||||
let s = lerp(s0, s1, interpT);
|
let s = lerp(s0, s1, interpT);
|
||||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||||
s = -s;
|
s = -s;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
|
|||||||
import { registerSound } from './superdough.mjs';
|
import { registerSound } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { buildSamples } from './zzfx_fork.mjs';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
|
import { onceEnded, releaseAudioNode } from './helpers.mjs';
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
let {
|
let {
|
||||||
@@ -83,10 +84,10 @@ export function registerZZFXSounds() {
|
|||||||
wave,
|
wave,
|
||||||
(t, value, onended) => {
|
(t, value, onended) => {
|
||||||
const { node: o } = getZZFX({ s: wave, ...value }, t);
|
const { node: o } = getZZFX({ s: wave, ...value }, t);
|
||||||
o.onended = () => {
|
onceEnded(o, () => {
|
||||||
o.disconnect();
|
releaseAudioNode(o);
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
return {
|
return {
|
||||||
node: o,
|
node: o,
|
||||||
stop: () => {},
|
stop: () => {},
|
||||||
|
|||||||
@@ -4092,35 +4092,6 @@ exports[`runs examples > example "floor" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "fm" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/6 | note:c fmi:0 ]",
|
|
||||||
"[ 1/6 → 1/3 | note:e fmi:0 ]",
|
|
||||||
"[ 1/3 → 1/2 | note:g fmi:0 ]",
|
|
||||||
"[ 1/2 → 2/3 | note:b fmi:0 ]",
|
|
||||||
"[ 2/3 → 5/6 | note:g fmi:0 ]",
|
|
||||||
"[ 5/6 → 1/1 | note:e fmi:0 ]",
|
|
||||||
"[ 1/1 → 7/6 | note:c fmi:1 ]",
|
|
||||||
"[ 7/6 → 4/3 | note:e fmi:1 ]",
|
|
||||||
"[ 4/3 → 3/2 | note:g fmi:1 ]",
|
|
||||||
"[ 3/2 → 5/3 | note:b fmi:1 ]",
|
|
||||||
"[ 5/3 → 11/6 | note:g fmi:1 ]",
|
|
||||||
"[ 11/6 → 2/1 | note:e fmi:1 ]",
|
|
||||||
"[ 2/1 → 13/6 | note:c fmi:2 ]",
|
|
||||||
"[ 13/6 → 7/3 | note:e fmi:2 ]",
|
|
||||||
"[ 7/3 → 5/2 | note:g fmi:2 ]",
|
|
||||||
"[ 5/2 → 8/3 | note:b fmi:2 ]",
|
|
||||||
"[ 8/3 → 17/6 | note:g fmi:2 ]",
|
|
||||||
"[ 17/6 → 3/1 | note:e fmi:2 ]",
|
|
||||||
"[ 3/1 → 19/6 | note:c fmi:8 ]",
|
|
||||||
"[ 19/6 → 10/3 | note:e fmi:8 ]",
|
|
||||||
"[ 10/3 → 7/2 | note:g fmi:8 ]",
|
|
||||||
"[ 7/2 → 11/3 | note:b fmi:8 ]",
|
|
||||||
"[ 11/3 → 23/6 | note:g fmi:8 ]",
|
|
||||||
"[ 23/6 → 4/1 | note:e fmi:8 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "fmattack" example index 0 1`] = `
|
exports[`runs examples > example "fmattack" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]",
|
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]",
|
||||||
@@ -4237,6 +4208,72 @@ exports[`runs examples > example "fmh" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "fmi" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/6 | note:c fmi:0 ]",
|
||||||
|
"[ 1/6 → 1/3 | note:e fmi:0 ]",
|
||||||
|
"[ 1/3 → 1/2 | note:g fmi:0 ]",
|
||||||
|
"[ 1/2 → 2/3 | note:b fmi:0 ]",
|
||||||
|
"[ 2/3 → 5/6 | note:g fmi:0 ]",
|
||||||
|
"[ 5/6 → 1/1 | note:e fmi:0 ]",
|
||||||
|
"[ 1/1 → 7/6 | note:c fmi:1 ]",
|
||||||
|
"[ 7/6 → 4/3 | note:e fmi:1 ]",
|
||||||
|
"[ 4/3 → 3/2 | note:g fmi:1 ]",
|
||||||
|
"[ 3/2 → 5/3 | note:b fmi:1 ]",
|
||||||
|
"[ 5/3 → 11/6 | note:g fmi:1 ]",
|
||||||
|
"[ 11/6 → 2/1 | note:e fmi:1 ]",
|
||||||
|
"[ 2/1 → 13/6 | note:c fmi:2 ]",
|
||||||
|
"[ 13/6 → 7/3 | note:e fmi:2 ]",
|
||||||
|
"[ 7/3 → 5/2 | note:g fmi:2 ]",
|
||||||
|
"[ 5/2 → 8/3 | note:b fmi:2 ]",
|
||||||
|
"[ 8/3 → 17/6 | note:g fmi:2 ]",
|
||||||
|
"[ 17/6 → 3/1 | note:e fmi:2 ]",
|
||||||
|
"[ 3/1 → 19/6 | note:c fmi:8 ]",
|
||||||
|
"[ 19/6 → 10/3 | note:e fmi:8 ]",
|
||||||
|
"[ 10/3 → 7/2 | note:g fmi:8 ]",
|
||||||
|
"[ 7/2 → 11/3 | note:b fmi:8 ]",
|
||||||
|
"[ 11/3 → 23/6 | note:g fmi:8 ]",
|
||||||
|
"[ 23/6 → 4/1 | note:e fmi:8 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "fmi" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
"[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "fmsustain" example index 0 1`] = `
|
exports[`runs examples > example "fmsustain" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]",
|
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]",
|
||||||
|
|||||||
Reference in New Issue
Block a user