Merge branch 'main' into glossing/random
This commit is contained in:
commit
931d684b19
38 changed files with 2526 additions and 451 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
|
||||
|
|
@ -189,10 +189,9 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
|
|||
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
|
||||
}
|
||||
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
|
||||
return { lfo, disconnect: () => lfo?.disconnect() };
|
||||
return lfo;
|
||||
}
|
||||
|
||||
export function createFilter(context, start, end, params, cps) {
|
||||
export function createFilter(context, start, end, params, cps, cycle) {
|
||||
let {
|
||||
frequency,
|
||||
anchor,
|
||||
|
|
@ -202,12 +201,14 @@ export function createFilter(context, start, end, params, cps) {
|
|||
q = 1,
|
||||
drive = 0.69,
|
||||
depth,
|
||||
depthfrequency,
|
||||
dcoffset = -0.5,
|
||||
skew,
|
||||
shape,
|
||||
rate,
|
||||
sync,
|
||||
} = params;
|
||||
|
||||
let frequencyParam, filter;
|
||||
if (model === 'ladder') {
|
||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||
|
|
@ -238,9 +239,27 @@ export function createFilter(context, start, end, params, cps) {
|
|||
if (sync != null) {
|
||||
rate = cps * sync;
|
||||
}
|
||||
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
|
||||
getParamLfo(context, frequencyParam, start, end, lfoValues);
|
||||
return filter;
|
||||
const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined);
|
||||
let lfo;
|
||||
if (hasLFO) {
|
||||
depth = depth ?? 1;
|
||||
const time = cycle / cps;
|
||||
const modDepth = depthfrequency ?? (depth ?? 1) * frequency;
|
||||
const lfoValues = {
|
||||
depth: modDepth,
|
||||
dcoffset,
|
||||
skew,
|
||||
shape,
|
||||
frequency: rate ?? cps,
|
||||
min: -frequency + 30,
|
||||
max: 20000 - frequency,
|
||||
time,
|
||||
curve: 1,
|
||||
};
|
||||
lfo = getParamLfo(context, frequencyParam, start, end, lfoValues);
|
||||
}
|
||||
|
||||
return { filter, lfo };
|
||||
}
|
||||
|
||||
// stays 1 until .5, then fades out
|
||||
|
|
@ -262,7 +281,18 @@ export function drywet(dry, wet, wetAmount = 0) {
|
|||
let mix = ac.createGain();
|
||||
dry_gain.connect(mix);
|
||||
wet_gain.connect(mix);
|
||||
return mix;
|
||||
return {
|
||||
node: mix,
|
||||
teardown: () => {
|
||||
releaseAudioNode(dry_gain);
|
||||
releaseAudioNode(wet_gain);
|
||||
// it is not the responsability of drywet
|
||||
// to call `releaseAudioNode` on
|
||||
// the 2 external args dry and wet
|
||||
dry.disconnect(dry_gain);
|
||||
wet.disconnect(wet_gain);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let curves = ['linear', 'exponential'];
|
||||
|
|
@ -297,6 +327,10 @@ export function getVibratoOscillator(param, value, t) {
|
|||
gain.gain.value = vibmod * 100;
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(param);
|
||||
onceEnded(vibratoOscillator, () => {
|
||||
releaseAudioNode(gain);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
});
|
||||
vibratoOscillator.start(t);
|
||||
return vibratoOscillator;
|
||||
}
|
||||
|
|
@ -318,25 +352,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||
constantNode.connect(zeroGain);
|
||||
|
||||
// Schedule the `onComplete` callback to occur at `stopTime`
|
||||
constantNode.onended = () => {
|
||||
// Ensure garbage collection
|
||||
try {
|
||||
zeroGain.disconnect();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
try {
|
||||
constantNode.disconnect();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
onceEnded(constantNode, () => {
|
||||
releaseAudioNode(zeroGain);
|
||||
releaseAudioNode(constantNode);
|
||||
onComplete();
|
||||
};
|
||||
});
|
||||
constantNode.start(startTime);
|
||||
constantNode.stop(stopTime);
|
||||
return constantNode;
|
||||
}
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
|
||||
const mod = (freq, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
let osc;
|
||||
if (noises.includes(type)) {
|
||||
|
|
@ -348,64 +374,89 @@ const mod = (freq, range = 1, type = 'sine') => {
|
|||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
}
|
||||
|
||||
osc.start();
|
||||
const g = new GainNode(ctx, { gain: range });
|
||||
osc.connect(g); // -range, range
|
||||
return { node: g, stop: (t) => osc.stop(t) };
|
||||
return osc;
|
||||
};
|
||||
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
||||
|
||||
const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
|
||||
const carrfreq = frequencyparam.value;
|
||||
const modfreq = carrfreq * harmonicityRatio;
|
||||
const modgain = modfreq * modulationIndex;
|
||||
return mod(modfreq, modgain, wave);
|
||||
return { osc: mod(modfreq, wave), freq: modfreq };
|
||||
};
|
||||
|
||||
export function applyFM(param, value, begin) {
|
||||
const {
|
||||
fmh: fmHarmonicity = 1,
|
||||
fmi: fmModulationIndex,
|
||||
fmenv: fmEnvelopeType = 'exp',
|
||||
fmattack: fmAttack,
|
||||
fmdecay: fmDecay,
|
||||
fmsustain: fmSustain,
|
||||
fmrelease: fmRelease,
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
duration,
|
||||
} = value;
|
||||
let modulator;
|
||||
let stop = () => {};
|
||||
|
||||
if (fmModulationIndex) {
|
||||
const ac = getAudioContext();
|
||||
const envGain = ac.createGain();
|
||||
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
|
||||
|
||||
modulator = fmmod.node;
|
||||
stop = fmmod.stop;
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
|
||||
// no envelope by default
|
||||
modulator.connect(param);
|
||||
} else {
|
||||
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
|
||||
const holdEnd = begin + duration;
|
||||
getParamADSR(
|
||||
envGain.gain,
|
||||
attack,
|
||||
decay,
|
||||
sustain,
|
||||
release,
|
||||
0,
|
||||
1,
|
||||
begin,
|
||||
holdEnd,
|
||||
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
||||
);
|
||||
modulator.connect(envGain);
|
||||
envGain.connect(param);
|
||||
const ac = getAudioContext();
|
||||
const toStop = []; // fm oscillators we will expose `stop` for
|
||||
const fms = {};
|
||||
// Matrix
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
for (let j = 0; j <= 8; j++) {
|
||||
let control;
|
||||
if (i === j + 1) {
|
||||
// Standard fm3 -> fm2 -> fm1 -> param usage
|
||||
const iS = i === 1 ? '' : i;
|
||||
control = `fmi${iS}`;
|
||||
} else {
|
||||
control = `fmi${i}${j}`;
|
||||
}
|
||||
const amt = value[control];
|
||||
if (!amt) continue;
|
||||
let io = [];
|
||||
for (let [isMod, idx] of [
|
||||
[true, i], // source
|
||||
[false, j], // target
|
||||
]) {
|
||||
if (idx === 0) {
|
||||
io.push(param);
|
||||
continue;
|
||||
}
|
||||
if (!fms[idx]) {
|
||||
const idxS = idx === 1 ? '' : idx;
|
||||
const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
|
||||
toStop.push(osc);
|
||||
const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop`
|
||||
const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
|
||||
let output = osc;
|
||||
if (adsr.some((v) => v !== undefined)) {
|
||||
const envGain = ac.createGain();
|
||||
const [attack, decay, sustain, release] = getADSRValues(adsr);
|
||||
const holdEnd = begin + value.duration;
|
||||
const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp';
|
||||
getParamADSR(
|
||||
envGain.gain,
|
||||
attack,
|
||||
decay,
|
||||
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]);
|
||||
}
|
||||
}
|
||||
return { stop };
|
||||
return {
|
||||
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||
};
|
||||
}
|
||||
|
||||
// Saturation curves
|
||||
|
|
@ -505,7 +556,7 @@ export const getDistortion = (distort, postgain, algorithm) => {
|
|||
};
|
||||
|
||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
let { note, freq } = value;
|
||||
let { note, freq, octave = 0 } = value;
|
||||
note = note || defaultNote;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
|
|
@ -514,14 +565,64 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|||
if (!freq && typeof note === 'number') {
|
||||
freq = midiToFreq(note); // + 48);
|
||||
}
|
||||
|
||||
freq *= Math.pow(2, octave);
|
||||
return Number(freq);
|
||||
};
|
||||
|
||||
export const destroyAudioWorkletNode = (node) => {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
node.disconnect();
|
||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||
// This helper should be used instead of the `node.onended = callback` pattern
|
||||
// It adds a mechanism to help minimize gc retention
|
||||
export const onceEnded = (node, callback) => {
|
||||
const onended = callback;
|
||||
node.onended = function cleanup() {
|
||||
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) {
|
||||
// while `end` is not native to the web audio API, it is common practice in superdough
|
||||
// to use that param in the worklets to trigger returning false from the processor
|
||||
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)));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { drywet } from './helpers.mjs';
|
||||
import { drywet, onceEnded, releaseAudioNode } from './helpers.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
let noiseCache = {};
|
||||
|
|
@ -65,8 +65,12 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
|||
export function getNoiseMix(inputNode, wet, t) {
|
||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
||||
onceEnded(noiseOscillator.node, () => {
|
||||
releaseAudioNode(noiseOscillator.node);
|
||||
});
|
||||
return {
|
||||
node: noiseMix,
|
||||
node: noiseMix.node,
|
||||
stop: (time) => noiseOscillator?.stop(time),
|
||||
teardown: noiseMix.teardown,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import { registerSound, registerWaveTable } from './index.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';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
|
@ -211,7 +218,7 @@ export async function fetchSampleMap(url) {
|
|||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
const base = getBaseURL(url);
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
|
|
@ -236,12 +243,6 @@ export async function fetchSampleMap(url) {
|
|||
* sd: '808sd/SD0010.WAV'
|
||||
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
||||
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
||||
* @example
|
||||
* samples('shabda:noise,chimp:2')
|
||||
* s("noise <chimp:0*2 chimp:1>")
|
||||
* @example
|
||||
* samples('shabda/speech/fr-FR/f:chocolat')
|
||||
* s("chocolat*4")
|
||||
*/
|
||||
|
||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||
|
|
@ -286,17 +287,19 @@ export async function onTriggerSample(t, value, onended, 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) {
|
||||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||
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
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
|
|
@ -319,13 +322,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||
|
||||
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
||||
node.connect(out);
|
||||
bufferSource.onended = function () {
|
||||
bufferSource.disconnect();
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
out.disconnect();
|
||||
onceEnded(bufferSource, function () {
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
releaseAudioNode(node);
|
||||
releaseAudioNode(out);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (endTime) => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,16 @@ import './reverb.mjs';
|
|||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import {
|
||||
createFilter,
|
||||
gainNode,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getLfo,
|
||||
getWorklet,
|
||||
effectSend,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
|
|
@ -285,8 +294,8 @@ let audioReady;
|
|||
export async function initAudioOnFirstClick(options) {
|
||||
if (!audioReady) {
|
||||
audioReady = new Promise((resolve) => {
|
||||
document.addEventListener('click', async function listener() {
|
||||
document.removeEventListener('click', listener);
|
||||
document.addEventListener('mousedown', async function listener() {
|
||||
document.removeEventListener('mousedown', listener);
|
||||
await initAudio(options);
|
||||
resolve();
|
||||
});
|
||||
|
|
@ -296,7 +305,7 @@ export async function initAudioOnFirstClick(options) {
|
|||
}
|
||||
|
||||
let controller;
|
||||
function getSuperdoughAudioController() {
|
||||
export function getSuperdoughAudioController() {
|
||||
if (controller == null) {
|
||||
controller = new SuperdoughAudioController(getAudioContext());
|
||||
}
|
||||
|
|
@ -309,11 +318,11 @@ export function connectToDestination(input, channels) {
|
|||
|
||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
const ac = getAudioContext();
|
||||
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
||||
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
||||
|
||||
//filters
|
||||
const numStages = 2; //num of filters in series
|
||||
let fOffset = 0;
|
||||
const numStages = 1; //num of filters in series
|
||||
let fOffset = 282; //for backward compat in #1800
|
||||
const filterChain = [];
|
||||
for (let i = 0; i < numStages; i++) {
|
||||
const filter = ac.createBiquadFilter();
|
||||
|
|
@ -322,14 +331,11 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
|
|||
filter.frequency.value = centerFrequency + fOffset;
|
||||
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
|
||||
|
||||
lfoGain.connect(filter.detune);
|
||||
lfo.connect(filter.detune);
|
||||
fOffset += 282;
|
||||
if (i > 0) {
|
||||
filterChain[i - 1].connect(filter);
|
||||
}
|
||||
filterChain.push(filter);
|
||||
}
|
||||
return filterChain[filterChain.length - 1];
|
||||
return { filterChain, lfo };
|
||||
}
|
||||
|
||||
function getFilterType(ftype) {
|
||||
|
|
@ -435,7 +441,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
release = 0,
|
||||
|
||||
//phaser
|
||||
phaserrate: phaser,
|
||||
phaserrate,
|
||||
phaserdepth = getDefaultValue('phaserdepth'),
|
||||
phasersweep,
|
||||
phasercenter,
|
||||
|
|
@ -473,6 +479,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
compressorKnee,
|
||||
compressorAttack,
|
||||
compressorRelease,
|
||||
transient,
|
||||
transsustain,
|
||||
} = value;
|
||||
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
|
|
@ -528,7 +536,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const onEnded = () => {
|
||||
audioNodes.forEach((n) => n?.disconnect());
|
||||
audioNodes.forEach((n) => releaseAudioNode(n));
|
||||
activeSoundSources.delete(chainID);
|
||||
};
|
||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||
|
|
@ -554,6 +562,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
chain.push(sourceNode);
|
||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||
|
||||
transient !== undefined &&
|
||||
chain.push(
|
||||
getWorklet(
|
||||
ac,
|
||||
'transient-processor',
|
||||
{},
|
||||
{
|
||||
processorOptions: {
|
||||
attack: transient,
|
||||
sustain: transsustain,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
|
|
@ -575,16 +600,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
rate: 'lprate',
|
||||
sync: 'lpsync',
|
||||
depth: 'lpdepth',
|
||||
depthfrequency: 'lpdepthfrequency',
|
||||
shape: 'lpshape',
|
||||
dcoffset: 'lpdc',
|
||||
skew: 'lpskew',
|
||||
};
|
||||
const lpParams = pickAndRename(value, lpMap);
|
||||
lpParams.type = 'lowpass';
|
||||
let lp = () => createFilter(ac, t, end, lpParams, cps);
|
||||
chain.push(lp());
|
||||
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
||||
const { filter: lpf1, lfo: lfo1 } = lp();
|
||||
chain.push(lpf1);
|
||||
lfo1 && audioNodes.push(lfo1);
|
||||
if (ftype === '24db') {
|
||||
chain.push(lp());
|
||||
const { filter: lpf2, lfo: lfo2 } = lp();
|
||||
chain.push(lpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -603,16 +633,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
rate: 'hprate',
|
||||
sync: 'hpsync',
|
||||
depth: 'hpdepth',
|
||||
depthfrequency: 'hpdepthfrequency',
|
||||
shape: 'hpshape',
|
||||
dcoffset: 'hpdc',
|
||||
skew: 'hpskew',
|
||||
};
|
||||
const hpParams = pickAndRename(value, hpMap);
|
||||
hpParams.type = 'highpass';
|
||||
let hp = () => createFilter(ac, t, end, hpParams, cps);
|
||||
chain.push(hp());
|
||||
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
|
||||
const { filter: hpf1, lfo: lfo1 } = hp();
|
||||
chain.push(hpf1);
|
||||
lfo1 && audioNodes.push(lfo1);
|
||||
if (ftype === '24db') {
|
||||
chain.push(hp());
|
||||
const { filter: hpf2, lfo: lfo2 } = hp();
|
||||
chain.push(hpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -631,16 +666,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
rate: 'bprate',
|
||||
sync: 'bpsync',
|
||||
depth: 'bpdepth',
|
||||
depthfrequency: 'bpdepthfrequency',
|
||||
shape: 'bpshape',
|
||||
dcoffset: 'bpdc',
|
||||
skew: 'bpskew',
|
||||
};
|
||||
const bpParams = pickAndRename(value, bpMap);
|
||||
bpParams.type = 'bandpass';
|
||||
let bp = () => createFilter(ac, t, end, bpParams, cps);
|
||||
chain.push(bp());
|
||||
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
|
||||
const { filter: bpf1, lfo: lfo1 } = bp();
|
||||
chain.push(bpf1);
|
||||
lfo1 && audioNodes.push(lfo1);
|
||||
if (ftype === '24db') {
|
||||
chain.push(bp());
|
||||
const { filter: bpf2, lfo: lfo2 } = bp();
|
||||
chain.push(bpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -687,6 +727,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
curve: 1.5,
|
||||
});
|
||||
lfo.connect(amGain.gain);
|
||||
audioNodes.push(lfo);
|
||||
chain.push(amGain);
|
||||
}
|
||||
|
||||
|
|
@ -702,9 +743,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
chain.push(panner);
|
||||
}
|
||||
// phaser
|
||||
if (phaser !== undefined && phaserdepth > 0) {
|
||||
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
|
||||
chain.push(phaserFX);
|
||||
if (phaserrate !== undefined && phaserdepth > 0) {
|
||||
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
|
||||
audioNodes.push(lfo);
|
||||
chain.push(...filterChain);
|
||||
}
|
||||
|
||||
// last gain
|
||||
|
|
@ -714,7 +756,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
// delay
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
orbitBus.sendDelay(post, delay);
|
||||
const send = orbitBus.sendDelay(post, delay);
|
||||
audioNodes.push(send);
|
||||
}
|
||||
// reverb
|
||||
if (room > 0) {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export class Orbit {
|
|||
}
|
||||
|
||||
sendDelay(node, amount) {
|
||||
effectSend(node, this.delayNode, amount);
|
||||
return effectSend(node, this.delayNode, amount);
|
||||
}
|
||||
|
||||
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs';
|
|||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
destroyAudioWorkletNode,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
|
|
@ -13,11 +12,14 @@ import {
|
|||
getVibratoOscillator,
|
||||
getWorklet,
|
||||
noises,
|
||||
onceEnded,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
||||
const waveformAliases = [
|
||||
['tri', 'triangle'],
|
||||
['sqr', 'square'],
|
||||
|
|
@ -47,19 +49,17 @@ export function registerSynthSounds() {
|
|||
[0.001, 0.05, 0.6, 0.01],
|
||||
);
|
||||
|
||||
let sound = getOscillator(s, t, value);
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
let sound = getOscillator(s, t, value, () => {
|
||||
releaseAudioNode(g);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
|
|
@ -111,15 +111,15 @@ export function registerSynthSounds() {
|
|||
|
||||
const mix = gainNode(mixGain);
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
sat.disconnect();
|
||||
noise.node.disconnect();
|
||||
noiseGain.disconnect();
|
||||
mix.disconnect();
|
||||
onceEnded(o, () => {
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(g);
|
||||
releaseAudioNode(sat);
|
||||
releaseAudioNode(noise.node);
|
||||
releaseAudioNode(noiseGain);
|
||||
releaseAudioNode(mix);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
|
||||
const node = o.connect(sat).connect(g).connect(mix);
|
||||
noise.node.connect(noiseGain).connect(mix);
|
||||
|
|
@ -193,8 +193,7 @@ export function registerSynthSounds() {
|
|||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
releaseAudioNode(o);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
|
|
@ -271,8 +270,7 @@ export function registerSynthSounds() {
|
|||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
releaseAudioNode(o);
|
||||
onended();
|
||||
},
|
||||
begin,
|
||||
|
|
@ -345,9 +343,8 @@ export function registerSynthSounds() {
|
|||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
destroyAudioWorkletNode(lfo);
|
||||
envGain.disconnect();
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(lfo);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
|
|
@ -388,11 +385,11 @@ export function registerSynthSounds() {
|
|||
|
||||
const { duration } = value;
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onceEnded(o, () => {
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(g);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
|
|
@ -414,9 +411,13 @@ export function registerSynthSounds() {
|
|||
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
||||
}
|
||||
|
||||
export function waveformN(partials, type) {
|
||||
const real = new Float32Array(partials + 1);
|
||||
const imag = new Float32Array(partials + 1);
|
||||
const PI2 = 2 * Math.PI;
|
||||
export function waveformN(partials, phases, type) {
|
||||
const isList = typeof partials === 'object';
|
||||
partials = isList ? partials : new Float32Array(partials).fill(1);
|
||||
const len = partials.length;
|
||||
const real = new Float32Array(len + 1);
|
||||
const imag = new Float32Array(len + 1);
|
||||
const ac = getAudioContext();
|
||||
const osc = ac.createOscillator();
|
||||
|
||||
|
|
@ -424,20 +425,29 @@ export function waveformN(partials, type) {
|
|||
sawtooth: (n) => [0, -1 / n],
|
||||
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
|
||||
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
|
||||
user: (_n) => [0, 1],
|
||||
};
|
||||
|
||||
if (!terms[type]) {
|
||||
throw new Error(`unknown wave type ${type}`);
|
||||
}
|
||||
|
||||
real[0] = 0; // dc offset
|
||||
imag[0] = 0;
|
||||
let n = 1;
|
||||
while (n <= partials) {
|
||||
const [r, i] = terms[type](n);
|
||||
real[n] = r;
|
||||
imag[n] = i;
|
||||
n++;
|
||||
for (let n = 0; n < len; n++) {
|
||||
const mag = partials[n];
|
||||
const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset
|
||||
const phase = phases?.[n] ?? 0;
|
||||
// Scale by `partials`
|
||||
let R = r * mag;
|
||||
let I = i * mag;
|
||||
// Apply rotation by the phase
|
||||
if (phase !== 0) {
|
||||
const c = Math.cos(PI2 * phase);
|
||||
const s = Math.sin(PI2 * phase);
|
||||
R = c * R - s * I;
|
||||
I = s * R + c * I;
|
||||
}
|
||||
real[n + 1] = R;
|
||||
imag[n + 1] = I;
|
||||
}
|
||||
|
||||
const wave = ac.createPeriodicWave(real, imag);
|
||||
|
|
@ -446,21 +456,28 @@ export function waveformN(partials, type) {
|
|||
}
|
||||
|
||||
// expects one of waveforms as s
|
||||
export function getOscillator(s, t, value) {
|
||||
let { n: partials, duration, noise = 0 } = value;
|
||||
export function getOscillator(s, t, value, onended) {
|
||||
const { duration, noise = 0 } = value;
|
||||
const partials = value.partials ?? value.n;
|
||||
let o;
|
||||
if (s === 'user' && !partials) {
|
||||
logger(
|
||||
`[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`,
|
||||
);
|
||||
s = 'triangle';
|
||||
}
|
||||
s = s === 'user' && !partials ? 'triangle' : s;
|
||||
// If no partials are given, use stock waveforms
|
||||
if (!partials || s === 'sine') {
|
||||
if (!partials || partials?.length === 0 || s === 'sine') {
|
||||
o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
}
|
||||
// generate custom waveform if partials are given
|
||||
else {
|
||||
o = waveformN(partials, s);
|
||||
o = waveformN(partials, value.phases, s);
|
||||
}
|
||||
// set frequency
|
||||
o.frequency.value = getFrequencyFromValue(value);
|
||||
o.start(t);
|
||||
|
||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||
|
||||
|
|
@ -473,6 +490,14 @@ export function getOscillator(s, t, value) {
|
|||
noiseMix = getNoiseMix(o, noise, t);
|
||||
}
|
||||
|
||||
onceEnded(o, () => {
|
||||
noiseMix?.teardown();
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(noiseMix?.node);
|
||||
onended();
|
||||
});
|
||||
o.start(t);
|
||||
|
||||
return {
|
||||
node: noiseMix?.node || o,
|
||||
stop: (time) => {
|
||||
|
|
|
|||
|
|
@ -114,3 +114,13 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||
export const pickAndRename = (source, map) => {
|
||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
||||
};
|
||||
|
||||
export const getBaseURL = (url) => {
|
||||
try {
|
||||
// For real URLs
|
||||
return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash
|
||||
} catch {
|
||||
// For pseudo URLS
|
||||
return url.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ if (typeof GainNode !== 'undefined') {
|
|||
throw new Error('vowel: unknown vowel ' + letter);
|
||||
}
|
||||
const { gains, qs, freqs } = vowelFormant[letter];
|
||||
const makeupGain = ac.createGain();
|
||||
this.makeupGain = ac.createGain();
|
||||
this.audioNodes = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const gain = ac.createGain();
|
||||
gain.gain.value = gains[i];
|
||||
|
|
@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') {
|
|||
filter.type = 'bandpass';
|
||||
filter.Q.value = qs[i];
|
||||
filter.frequency.value = freqs[i];
|
||||
this.connect(filter);
|
||||
super.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(makeupGain);
|
||||
this.audioNodes.push(filter);
|
||||
gain.connect(this.makeupGain);
|
||||
this.audioNodes.push(gain);
|
||||
}
|
||||
makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||
this.connect = (target) => makeupGain.connect(target);
|
||||
this.makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||
return this;
|
||||
}
|
||||
connect(target) {
|
||||
this.makeupGain.connect(target);
|
||||
}
|
||||
disconnect() {
|
||||
this.makeupGain.disconnect();
|
||||
this.audioNodes.forEach((n) => n.disconnect());
|
||||
super.disconnect();
|
||||
this.makeupGain = null;
|
||||
this.audioNodes = null;
|
||||
}
|
||||
}
|
||||
|
||||
AudioContext.prototype.createVowelFilter = function (letter) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
applyParameterModulators,
|
||||
destroyAudioWorkletNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
getWorklet,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
|
@ -190,6 +190,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
const base = getBaseURL(url);
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
|
|
@ -200,7 +201,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||
}
|
||||
return fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then((json) => _processTables(json, url, frameLen, options))
|
||||
.then((json) => _processTables(json, base, frameLen, options))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
|
|
@ -318,12 +319,11 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
releaseAudioNode(source);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
fm?.stop();
|
||||
node.disconnect();
|
||||
wtPosModulators?.disconnect();
|
||||
wtWarpModulators?.disconnect();
|
||||
releaseAudioNode(wtPosModulators);
|
||||
releaseAudioNode(wtWarpModulators);
|
||||
onended();
|
||||
},
|
||||
t,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ const PI = Math.PI;
|
|||
const TWO_PI = 2 * PI;
|
||||
const INVSR = 1 / sampleRate;
|
||||
|
||||
const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t);
|
||||
const dbToLin = (db) => Math.pow(10, db / 20);
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const mod = (n, m) => ((n % m) + m) % m;
|
||||
const lerp = (a, b, n) => n * (b - a) + a;
|
||||
|
|
@ -508,13 +511,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||
];
|
||||
}
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= params.end[0]) {
|
||||
// this.port.postMessage({ type: 'onended' });
|
||||
// should terminate
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
// keep alive
|
||||
return true;
|
||||
}
|
||||
const output = outputs[0];
|
||||
const voices = params.voices[0]; // k-rate
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
|
|
@ -532,7 +536,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = ffrac(freqVoice * INVSR);
|
||||
const dt = frac(freqVoice * INVSR);
|
||||
this.phase[n] = this.phase[n] ?? Math.random();
|
||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||
|
||||
|
|
@ -543,8 +547,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||
if (pn >= 1.0) pn -= 1.0;
|
||||
this.phase[n] = pn;
|
||||
// invert right and left gain
|
||||
const tmp = gainL;
|
||||
gainL = gainR;
|
||||
gainR = gainL;
|
||||
gainR = tmp;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1338,7 +1343,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||
const idx = tablePos * (this.numFrames - 1);
|
||||
const fIdx = idx | 0;
|
||||
const frac = idx - fIdx;
|
||||
const interpT = idx - fIdx;
|
||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||
|
|
@ -1368,13 +1373,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
let s = s0 + (s1 - s0) * frac;
|
||||
let s = lerp(s0, s1, interpT);
|
||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||
s = -s;
|
||||
}
|
||||
outL[i] += s * gainL * normalizer;
|
||||
outR[i] += s * gainR * normalizer;
|
||||
this.phase[n] = ffrac(this.phase[n] + dPhase);
|
||||
this.phase[n] = frac(this.phase[n] + dPhase);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1382,3 +1387,82 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||
}
|
||||
|
||||
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
|
||||
|
||||
class TransientProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
this.gainCoeff = timeToCoeff(0.2);
|
||||
this.avgGain = 1;
|
||||
let {
|
||||
attackTime = 0.003,
|
||||
sustainTime = 0.08,
|
||||
attack = 0,
|
||||
sustain = 0,
|
||||
sensitivity = 0.1,
|
||||
mix = 1,
|
||||
begin = 0,
|
||||
end = 0,
|
||||
} = options.processorOptions;
|
||||
attackTime = clamp(attackTime, 0.0005, 0.05);
|
||||
sustainTime = clamp(sustainTime, 0.01, 0.5);
|
||||
this.attackCoeff = timeToCoeff(attackTime);
|
||||
this.sustainCoeff = timeToCoeff(sustainTime);
|
||||
this.attackAmt = clamp(attack, -1, 1);
|
||||
this.sustainAmt = clamp(sustain, -1, 1);
|
||||
this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1);
|
||||
this.mix = clamp(mix, 0, 1);
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
this.attackEnv = new Float32Array(2); // assume stereo
|
||||
this.sustainEnv = new Float32Array(2);
|
||||
}
|
||||
|
||||
process(inputs, outputs, _params) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
if (currentTime >= this.end) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= this.begin) {
|
||||
return true;
|
||||
}
|
||||
const channels = input.length;
|
||||
if (channels > this.attackEnv.length) {
|
||||
this.attackEnv = new Float32Array(channels);
|
||||
this.sustainEnv = new Float32Array(channels);
|
||||
}
|
||||
let avgGain = this.avgGain;
|
||||
for (let ch = 0; ch < channels; ch++) {
|
||||
let attEnv = this.attackEnv[ch];
|
||||
let susEnv = this.sustainEnv[ch];
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
const sample = input[ch][n];
|
||||
const x = Math.abs(sample);
|
||||
attEnv = lerp(attEnv, x, this.attackCoeff);
|
||||
susEnv = lerp(susEnv, x, this.sustainCoeff);
|
||||
const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5);
|
||||
const attScale = peakiness > 0 ? peakiness : 0;
|
||||
const susScale = peakiness < 0 ? -peakiness : 0;
|
||||
const attackGain = dbToLin(this.attackAmt * attScale * 18);
|
||||
const sustainGain = dbToLin(this.sustainAmt * susScale * 36);
|
||||
const gain = clamp(attackGain * sustainGain, 0, 8);
|
||||
avgGain = lerp(avgGain, gain, this.gainCoeff);
|
||||
const makeup = avgGain > 1e-3 ? 1 / avgGain : 1;
|
||||
const wet = sample * gain * makeup;
|
||||
let y = lerp(sample, wet, this.mix);
|
||||
y /= 1 + Math.abs(y); // soft clip
|
||||
output[ch][n] = y;
|
||||
}
|
||||
this.attackEnv[ch] = attEnv;
|
||||
this.sustainEnv[ch] = susEnv;
|
||||
}
|
||||
this.avgGain = avgGain;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('transient-processor', TransientProcessor);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
|
|||
import { registerSound } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { buildSamples } from './zzfx_fork.mjs';
|
||||
import { onceEnded, releaseAudioNode } from './helpers.mjs';
|
||||
|
||||
export const getZZFX = (value, t) => {
|
||||
let {
|
||||
|
|
@ -83,10 +84,10 @@ export function registerZZFXSounds() {
|
|||
wave,
|
||||
(t, value, onended) => {
|
||||
const { node: o } = getZZFX({ s: wave, ...value }, t);
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
onceEnded(o, () => {
|
||||
releaseAudioNode(o);
|
||||
onended();
|
||||
};
|
||||
});
|
||||
return {
|
||||
node: o,
|
||||
stop: () => {},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue