mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c407412c6 | |||
| 099288545e | |||
| 5738020ac2 | |||
| 1478ff51a7 | |||
| 8314023c7a |
@@ -1,7 +1,7 @@
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { getNodeFromPool } from './nodePools.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead } from './nodePools.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
@@ -37,6 +37,16 @@ export function getWorklet(ac, processor, params, config) {
|
||||
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 = (
|
||||
param,
|
||||
attack,
|
||||
@@ -125,7 +135,7 @@ export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
...props,
|
||||
};
|
||||
|
||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||
return getPooledWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||
}
|
||||
|
||||
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||
@@ -217,7 +227,7 @@ export function createFilter(context, start, end, params, cps, cycle) {
|
||||
|
||||
let frequencyParam, filter;
|
||||
if (model === 'ladder') {
|
||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||
filter = getPooledWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||
frequencyParam = filter.parameters.get('frequency');
|
||||
} else {
|
||||
const factory = () => context.createBiquadFilter();
|
||||
@@ -557,8 +567,9 @@ export const getDistortionAlgorithm = (algo) => {
|
||||
return distortionAlgorithms[name];
|
||||
};
|
||||
|
||||
export const getDistortion = (distort, postgain, algorithm) => {
|
||||
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
||||
export const getDistortion = (fullParams) => {
|
||||
const { algorithm, ...params } = fullParams;
|
||||
return getPooledWorklet(getAudioContext(), 'distort-processor', params, {}, { algorithm });
|
||||
};
|
||||
|
||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
|
||||
@@ -12,26 +12,6 @@ const MAX_POOL_SIZE = 64;
|
||||
|
||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||
|
||||
const getParams = (node) => {
|
||||
const params = new Set();
|
||||
node.parameters?.forEach((param) => params.add(param));
|
||||
const visited = new Set(); // prioritize deepest definition
|
||||
let proto = node;
|
||||
// Move up the prototype chain
|
||||
while (proto !== Object.prototype) {
|
||||
for (const key of Object.getOwnPropertyNames(proto)) {
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
const value = node[key];
|
||||
if (value instanceof AudioParam) {
|
||||
params.add(value);
|
||||
}
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
export const releaseNodeToPool = (node) => {
|
||||
node.disconnect();
|
||||
if (node instanceof AudioScheduledSourceNode) {
|
||||
@@ -44,8 +24,6 @@ export const releaseNodeToPool = (node) => {
|
||||
}
|
||||
const key = node[POOL_KEY];
|
||||
if (key == null) return;
|
||||
const now = node.context?.currentTime ?? 0;
|
||||
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
if (pool.length < MAX_POOL_SIZE) {
|
||||
pool.push(new WeakRef(node));
|
||||
@@ -57,7 +35,7 @@ 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) => {
|
||||
export const getNodeFromPool = (key, factory, params = {}) => {
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
let node;
|
||||
while (pool.length) {
|
||||
@@ -69,5 +47,25 @@ export const getNodeFromPool = (key, factory) => {
|
||||
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';
|
||||
|
||||
import { makeReusable } from './worklets-common.mjs';
|
||||
|
||||
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
const WEBAUDIO_BLOCK_SIZE = 128;
|
||||
|
||||
/** Overlap-Add Node */
|
||||
class OLAProcessor extends AudioWorkletProcessor {
|
||||
class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.started = false;
|
||||
this.nbInputs = options.numberOfInputs;
|
||||
this.nbOutputs = options.numberOfOutputs;
|
||||
|
||||
this.blockSize = options.processorOptions.blockSize;
|
||||
// TODO for now, the only support hop size is the size of a web audio block
|
||||
this.hopSize = WEBAUDIO_BLOCK_SIZE;
|
||||
|
||||
this.nbOverlaps = this.blockSize / this.hopSize;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.inputBuffers = new Array(this.nbInputs);
|
||||
this.inputBuffersHead = new Array(this.nbInputs);
|
||||
@@ -54,7 +55,6 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
|
||||
allocateInputChannels(inputIndex, nbChannels) {
|
||||
// allocate input buffers
|
||||
|
||||
this.inputBuffers[inputIndex] = new Array(nbChannels);
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
|
||||
@@ -157,15 +157,8 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
const input = inputs[0];
|
||||
const hasInput = !(input[0] === undefined);
|
||||
if (this.started && !hasInput) {
|
||||
return false;
|
||||
}
|
||||
this.started = hasInput;
|
||||
processActive(inputs, outputs, params) {
|
||||
this.reallocateChannelsIfNeeded(inputs, outputs);
|
||||
|
||||
this.readInputs(inputs);
|
||||
this.shiftInputBuffers();
|
||||
this.prepareInputBuffersToSend();
|
||||
@@ -173,7 +166,6 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
this.handleOutputBuffersToRetrieve();
|
||||
this.writeOutputs(outputs);
|
||||
this.shiftOutputBuffers();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,15 @@ import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, effectSend, gainNode, getCompressor, getDistortion, getLfo, getWorklet } 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 { logger } from './logger.mjs';
|
||||
@@ -531,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
|
||||
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
|
||||
chain.push(gainNode(gain));
|
||||
@@ -644,10 +655,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||
coarse !== undefined &&
|
||||
chain.push(getPooledWorklet(ac, 'coarse-processor', { coarse, begin: t, end: endWithRelease }));
|
||||
crush !== undefined && chain.push(getPooledWorklet(ac, 'crush-processor', { crush, begin: t, end: endWithRelease }));
|
||||
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) {
|
||||
tremolo = cps * tremolosync;
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getPooledWorklet,
|
||||
getVibratoOscillator,
|
||||
getWorklet,
|
||||
noises,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { releaseNodeToPool } from './nodePools.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
||||
const waveformAliases = [
|
||||
@@ -170,22 +170,9 @@ export function registerSynthSounds() {
|
||||
begin,
|
||||
end,
|
||||
freqspread: detune,
|
||||
voices,
|
||||
panspread,
|
||||
};
|
||||
const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] });
|
||||
const o = getNodeFromPool('supersaw', factory);
|
||||
const now = ac.currentTime;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const param = o.parameters.get(key);
|
||||
const target = value !== undefined ? value : param.defaultValue;
|
||||
param.setValueAtTime(target, now);
|
||||
});
|
||||
o.port.postMessage({ type: 'initialize' });
|
||||
o.port.onmessage = (e) => {
|
||||
if (e.data.type === 'died') markWorkletAsDead(o);
|
||||
o.port.onmessage = null;
|
||||
};
|
||||
const o = getPooledWorklet(ac, 'supersaw-oscillator', params, { outputChannelCount: [2] }, { voices });
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
@@ -252,7 +239,7 @@ export function registerSynthSounds() {
|
||||
const holdend = begin + duration;
|
||||
const end = holdend + release + 0.01;
|
||||
|
||||
let o = getWorklet(
|
||||
let o = getPooledWorklet(
|
||||
ac,
|
||||
'byte-beat-processor',
|
||||
{
|
||||
@@ -319,7 +306,7 @@ export function registerSynthSounds() {
|
||||
);
|
||||
const holdend = begin + duration;
|
||||
const end = holdend + release + 0.01;
|
||||
let o = getWorklet(
|
||||
let o = getPooledWorklet(
|
||||
ac,
|
||||
'pulse-oscillator',
|
||||
{
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
getFrequencyFromValue,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getPooledWorklet,
|
||||
getVibratoOscillator,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { releaseNodeToPool } from './nodePools.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const Warpmode = Object.freeze({
|
||||
@@ -224,6 +225,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
}
|
||||
const endWithRelease = holdEnd + release;
|
||||
const envEnd = endWithRelease + 0.01;
|
||||
payload.voices = Math.max(value.unison ?? 1, 1);
|
||||
const params = {
|
||||
begin: t,
|
||||
end: envEnd,
|
||||
@@ -232,23 +234,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
panspread: value.spread,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
};
|
||||
const factory = () => new AudioWorkletNode(ac, 'wavetable-oscillator-processor', { outputChannelCount: [2] });
|
||||
const source = getNodeFromPool('wavetable', factory);
|
||||
const now = ac.currentTime;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const param = source.parameters.get(key);
|
||||
const target = value !== undefined ? value : param.defaultValue;
|
||||
param.setValueAtTime(target, now);
|
||||
});
|
||||
source.port.postMessage({ type: 'initialize', payload });
|
||||
source.port.onmessage = (e) => {
|
||||
if (e.data.type === 'died') markWorkletAsDead(source);
|
||||
source.port.onmessage = null;
|
||||
};
|
||||
const source = getPooledWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2] }, payload);
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
return;
|
||||
|
||||
@@ -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
|
||||
// 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 FFT from './fft.js';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
import { makeReusable } from './worklets-common.mjs';
|
||||
|
||||
const blockSize = 128;
|
||||
const PI = Math.PI;
|
||||
@@ -108,12 +109,12 @@ const waveshapes = {
|
||||
};
|
||||
|
||||
const waveShapeNames = Object.keys(waveshapes);
|
||||
class LFOProcessor extends AudioWorkletProcessor {
|
||||
|
||||
class LFOProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0 },
|
||||
...super.parameterDescriptors,
|
||||
{ name: 'time', defaultValue: 0 },
|
||||
{ name: 'end', defaultValue: 0 },
|
||||
{ name: 'frequency', defaultValue: 0.5 },
|
||||
{ name: 'skew', defaultValue: 0.5 },
|
||||
{ name: 'depth', defaultValue: 1 },
|
||||
@@ -126,9 +127,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.phase;
|
||||
initialize() {
|
||||
this.phase = null;
|
||||
}
|
||||
|
||||
incrementPhase(dt) {
|
||||
@@ -138,15 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
processActive(_inputs, outputs, parameters) {
|
||||
const output = outputs[0];
|
||||
const frequency = parameters['frequency'][0];
|
||||
|
||||
@@ -182,26 +174,14 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
registerProcessor('lfo-processor', LFOProcessor);
|
||||
|
||||
class CoarseProcessor extends AudioWorkletProcessor {
|
||||
class CoarseProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [{ name: 'coarse', defaultValue: 1 }];
|
||||
return [...super.parameterDescriptors, { name: 'coarse', defaultValue: 1 }];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
processActive(inputs, outputs, parameters) {
|
||||
const input = inputs[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;
|
||||
coarse = Math.max(1, coarse);
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
@@ -214,26 +194,14 @@ class CoarseProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
registerProcessor('coarse-processor', CoarseProcessor);
|
||||
|
||||
class CrushProcessor extends AudioWorkletProcessor {
|
||||
class CrushProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [{ name: 'crush', defaultValue: 0 }];
|
||||
return [...super.parameterDescriptors, { name: 'crush', defaultValue: 0 }];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
processActive(inputs, outputs, parameters) {
|
||||
const input = inputs[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;
|
||||
crush = Math.max(1, crush);
|
||||
|
||||
@@ -248,29 +216,14 @@ class CrushProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
registerProcessor('crush-processor', CrushProcessor);
|
||||
|
||||
class ShapeProcessor extends AudioWorkletProcessor {
|
||||
class ShapeProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'shape', defaultValue: 0 },
|
||||
{ name: 'postgain', defaultValue: 1 },
|
||||
];
|
||||
return [...super.parameterDescriptors, { name: 'shape', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
processActive(inputs, outputs, parameters) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
|
||||
const hasInput = !(input[0] === undefined);
|
||||
if (this.started && !hasInput) {
|
||||
return false;
|
||||
}
|
||||
this.started = hasInput;
|
||||
|
||||
let shape = parameters.shape[0];
|
||||
shape = shape < 1 ? shape : 1.0 - 4e-10;
|
||||
shape = (2.0 * shape) / (1.0 - shape);
|
||||
@@ -354,17 +307,17 @@ class DJFProcessor extends AudioWorkletProcessor {
|
||||
registerProcessor('djf-processor', DJFProcessor);
|
||||
|
||||
//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() {
|
||||
return [
|
||||
...super.parameterDescriptors,
|
||||
{ name: 'frequency', defaultValue: 500 },
|
||||
{ name: 'q', defaultValue: 1 },
|
||||
{ name: 'drive', defaultValue: 0.69 },
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
initialize(_options) {
|
||||
this.started = false;
|
||||
this.p0 = [0, 0];
|
||||
this.p1 = [0, 0];
|
||||
@@ -375,17 +328,9 @@ class LadderProcessor extends AudioWorkletProcessor {
|
||||
this.p34 = [0, 0];
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
processActive(inputs, outputs, parameters) {
|
||||
const input = inputs[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 drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
||||
|
||||
@@ -418,29 +363,18 @@ class LadderProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
registerProcessor('ladder-processor', LadderProcessor);
|
||||
|
||||
class DistortProcessor extends AudioWorkletProcessor {
|
||||
class DistortProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'distort', defaultValue: 0 },
|
||||
{ name: 'postgain', defaultValue: 1 },
|
||||
];
|
||||
return [...super.parameterDescriptors, { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }];
|
||||
}
|
||||
|
||||
constructor({ processorOptions }) {
|
||||
super();
|
||||
this.started = false;
|
||||
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
||||
initialize(options) {
|
||||
this.algorithm = getDistortionAlgorithm(options?.algorithm);
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
processActive(inputs, outputs, parameters) {
|
||||
const input = inputs[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++) {
|
||||
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||
const shape = Math.expm1(pv(parameters.distort, n));
|
||||
@@ -455,43 +389,15 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
registerProcessor('distort-processor', DistortProcessor);
|
||||
|
||||
// SUPERSAW
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.isAlive = true; // used internally to prevent multiple death messages
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'initialize') {
|
||||
this.initialize(payload);
|
||||
}
|
||||
};
|
||||
this.initialize();
|
||||
}
|
||||
initialize(_options) {
|
||||
this.phase = [];
|
||||
}
|
||||
class SuperSawOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
...super.parameterDescriptors,
|
||||
{
|
||||
name: 'frequency',
|
||||
defaultValue: 440,
|
||||
min: Number.EPSILON,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'panspread',
|
||||
defaultValue: 0.4,
|
||||
@@ -508,30 +414,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'voices',
|
||||
defaultValue: 5,
|
||||
min: 1,
|
||||
automationRate: 'k-rate',
|
||||
},
|
||||
];
|
||||
}
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime >= params.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
return true;
|
||||
}
|
||||
initialize(options) {
|
||||
this.phase = [];
|
||||
this.voices = options?.voices ?? 5;
|
||||
}
|
||||
processActive(_input, outputs, params) {
|
||||
const output = outputs[0];
|
||||
const voices = params.voices[0]; // k-rate
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
const detune = pv(params.detune, i);
|
||||
const freqspread = pv(params.freqspread, i);
|
||||
@@ -541,8 +431,8 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
let freq = pv(params.frequency, i);
|
||||
// Main detuning
|
||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||
const detuner = getDetuner(voices, freqspread);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const detuner = getDetuner(this.voices, freqspread);
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
// Individual voice detuning
|
||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
@@ -586,12 +476,7 @@ function genHannWindow(length) {
|
||||
|
||||
class PhaseVocoderProcessor extends OLAProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'pitchFactor',
|
||||
defaultValue: 1.0,
|
||||
},
|
||||
];
|
||||
return [...super.parameterDescriptors, { name: 'pitchFactor', defaultValue: 1.0 }];
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
@@ -599,9 +484,13 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
blockSize: BUFFERED_BLOCK_SIZE,
|
||||
};
|
||||
super(options);
|
||||
this.timeCursor = 0;
|
||||
this.fftSize = this.blockSize;
|
||||
this.invfftSize = 1 / this.fftSize;
|
||||
}
|
||||
|
||||
initialize(options) {
|
||||
super.initialize(options);
|
||||
this.timeCursor = 0;
|
||||
this.hannWindow = genHannWindow(this.fftSize);
|
||||
// prepare FFT and pre-allocate buffers
|
||||
this.fft = new FFT(this.fftSize);
|
||||
@@ -732,9 +621,8 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
||||
|
||||
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
|
||||
class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
class PulseOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
initialize() {
|
||||
this.phi = -PI; // phase
|
||||
this.Y0 = 0; // feedback memories
|
||||
this.Y1 = 0;
|
||||
@@ -746,20 +634,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
...super.parameterDescriptors,
|
||||
{
|
||||
name: 'frequency',
|
||||
defaultValue: 440,
|
||||
@@ -780,20 +655,10 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= params.end[0]) {
|
||||
return false;
|
||||
}
|
||||
processActive(inputs, outputs, params) {
|
||||
const output = outputs[0];
|
||||
let env = 1,
|
||||
dphi;
|
||||
|
||||
for (let i = 0; i < (output[0].length ?? 0); i++) {
|
||||
const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
|
||||
const detune = pv(params.detune, i);
|
||||
@@ -876,28 +741,23 @@ function getByteBeatFunc(codetext) {
|
||||
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
|
||||
}
|
||||
|
||||
class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.port.onmessage = (event) => {
|
||||
let { codeText } = event.data;
|
||||
const { byteBeatStartTime } = event.data;
|
||||
if (byteBeatStartTime != null) {
|
||||
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
|
||||
.trim()
|
||||
.replace(
|
||||
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
|
||||
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
|
||||
);
|
||||
|
||||
this.func = getByteBeatFunc(codeText);
|
||||
};
|
||||
class ByteBeatProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
initialize(options) {
|
||||
let codeText = options?.codeText;
|
||||
const byteBeatStartTime = options?.byteBeatStartTime;
|
||||
if (byteBeatStartTime != null) {
|
||||
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
|
||||
.trim()
|
||||
.replace(
|
||||
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
|
||||
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
|
||||
);
|
||||
this.func = getByteBeatFunc(codeText);
|
||||
this.initialOffset = 0;
|
||||
this.t = null;
|
||||
this.func = null;
|
||||
@@ -905,12 +765,7 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
...super.parameterDescriptors,
|
||||
{
|
||||
name: 'frequency',
|
||||
defaultValue: 440,
|
||||
@@ -922,25 +777,10 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= params.end[0]) {
|
||||
return false;
|
||||
}
|
||||
processActive(inputs, outputs, params) {
|
||||
if (this.t == null) {
|
||||
this.t = params.begin[0] * sampleRate;
|
||||
}
|
||||
@@ -966,11 +806,9 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||
|
||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||
|
||||
class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
class EnvelopeProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0 },
|
||||
{ name: 'end', defaultValue: 0 },
|
||||
{ name: 'attack', defaultValue: 0.005, minValue: 0 },
|
||||
{ name: 'decay', defaultValue: 0.14, minValue: 0 },
|
||||
{ name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
@@ -1130,34 +968,21 @@ function brownian(x, oct = 4) {
|
||||
}
|
||||
|
||||
const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
class WavetableOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
...super.parameterDescriptors,
|
||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||
{ name: 'detune', defaultValue: 0 },
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warpMode', defaultValue: 0 },
|
||||
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
|
||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.isAlive = true; // used internally to prevent multiple death messages
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'initialize') {
|
||||
this.initialize(payload);
|
||||
}
|
||||
};
|
||||
this.initialize();
|
||||
}
|
||||
initialize(options) {
|
||||
this.table = null;
|
||||
this.frameLen = null;
|
||||
@@ -1172,6 +997,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
this.table = tablesCache[key];
|
||||
this.numFrames = this.table.length;
|
||||
}
|
||||
this.voices = options?.voices ?? 1;
|
||||
}
|
||||
|
||||
_mirror(x) {
|
||||
@@ -1316,19 +1142,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return a + (b - a) * frac;
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
if (currentTime >= parameters.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
return true;
|
||||
}
|
||||
processActive(_inputs, outputs, parameters) {
|
||||
const outL = outputs[0][0];
|
||||
const outR = outputs[0][1] || outputs[0][0];
|
||||
if (!this.table) {
|
||||
@@ -1336,7 +1150,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
if (outR !== outL) outR.set(outL);
|
||||
return true;
|
||||
}
|
||||
const voices = parameters.voices[0]; // k-rate
|
||||
for (let i = 0; i < outL.length; i++) {
|
||||
const detune = pv(parameters.detune, i);
|
||||
const freqspread = pv(parameters.freqspread, i);
|
||||
@@ -1347,14 +1160,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
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 gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||
let f = pv(parameters.frequency, i);
|
||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||
const normalizer = 1 / Math.sqrt(voices);
|
||||
const detuner = getDetuner(voices, freqspread);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const normalizer = 1 / Math.sqrt(this.voices);
|
||||
const detuner = getDetuner(this.voices, freqspread);
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
|
||||
Reference in New Issue
Block a user