mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-18 16:46:10 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ce09cfd32 | |||
| 96d92b4728 | |||
| 8b66002dd0 | |||
| 34620ffed2 | |||
| 835659213e | |||
| 584f158c29 | |||
| 56c6a1fd56 | |||
| af2ad9505d | |||
| 352344aa91 | |||
| ffb618dc97 |
@@ -162,6 +162,7 @@ export class StrudelMirror {
|
|||||||
this.onDraw = onDraw || this.draw;
|
this.onDraw = onDraw || this.draw;
|
||||||
this.id = id || s4();
|
this.id = id || s4();
|
||||||
this.solo = solo;
|
this.solo = solo;
|
||||||
|
this.hasPainters = false;
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||||
@@ -177,7 +178,7 @@ export class StrudelMirror {
|
|||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
replOptions?.onToggle?.(started);
|
replOptions?.onToggle?.(started);
|
||||||
if (started) {
|
if (started) {
|
||||||
this.drawer.start(this.repl.scheduler);
|
this.shouldAnimate() && this.drawer.start(this.repl.scheduler);
|
||||||
if (this.solo) {
|
if (this.solo) {
|
||||||
// stop other repls when this one is started
|
// stop other repls when this one is started
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
@@ -207,9 +208,10 @@ export class StrudelMirror {
|
|||||||
updateWidgets(this.editor, widgets);
|
updateWidgets(this.editor, widgets);
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
|
this.hasPainters = options.pattern.getPainters().length > 0;
|
||||||
// if no painters are set (.onPaint was not called), then we only need
|
// if no painters are set (.onPaint was not called), then we only need
|
||||||
// the present moment (for highlighting)
|
// the present moment (for highlighting)
|
||||||
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
|
const drawTime = this.hasPainters ? this.drawTime : [0, 0];
|
||||||
this.drawer.setDrawTime(drawTime);
|
this.drawer.setDrawTime(drawTime);
|
||||||
// invalidate drawer after we've set the appropriate drawTime
|
// invalidate drawer after we've set the appropriate drawTime
|
||||||
this.drawer.invalidate(this.repl.scheduler);
|
this.drawer.invalidate(this.repl.scheduler);
|
||||||
@@ -237,6 +239,7 @@ export class StrudelMirror {
|
|||||||
cmEditor.style.backgroundColor = 'transparent';
|
cmEditor.style.backgroundColor = 'transparent';
|
||||||
}
|
}
|
||||||
const settings = codemirrorSettings.get();
|
const settings = codemirrorSettings.get();
|
||||||
|
this.isPatternHighlightingEnabled = parseBooleans(settings.isPatternHighlightingEnabled);
|
||||||
this.setFontSize(settings.fontSize);
|
this.setFontSize(settings.fontSize);
|
||||||
this.setFontFamily(settings.fontFamily);
|
this.setFontFamily(settings.fontFamily);
|
||||||
|
|
||||||
@@ -321,8 +324,17 @@ export class StrudelMirror {
|
|||||||
flash(this.editor, ms);
|
flash(this.editor, ms);
|
||||||
}
|
}
|
||||||
highlight(haps, time) {
|
highlight(haps, time) {
|
||||||
|
if (!this.isPatternHighlightingEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
highlightMiniLocations(this.editor, time, haps);
|
highlightMiniLocations(this.editor, time, haps);
|
||||||
}
|
}
|
||||||
|
shouldAnimate() {
|
||||||
|
return (
|
||||||
|
this.repl.scheduler.started &&
|
||||||
|
(this.isPatternHighlightingEnabled || this.hasPainters || this.onDraw !== this.draw)
|
||||||
|
);
|
||||||
|
}
|
||||||
setFontSize(size) {
|
setFontSize(size) {
|
||||||
this.root.style.fontSize = size + 'px';
|
this.root.style.fontSize = size + 'px';
|
||||||
}
|
}
|
||||||
@@ -343,6 +355,11 @@ export class StrudelMirror {
|
|||||||
this.editor.dispatch({
|
this.editor.dispatch({
|
||||||
effects: compartments[key].reconfigure(newValue),
|
effects: compartments[key].reconfigure(newValue),
|
||||||
});
|
});
|
||||||
|
if (key === 'isPatternHighlightingEnabled') {
|
||||||
|
this.isPatternHighlightingEnabled = value;
|
||||||
|
this.drawer.stop();
|
||||||
|
this.shouldAnimate() && this.drawer.start(this.repl.scheduler);
|
||||||
|
}
|
||||||
if (key === 'theme') {
|
if (key === 'theme') {
|
||||||
activateTheme(value);
|
activateTheme(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ Fraction.prototype.or = function (other) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fraction = (n) => {
|
const fraction = (n) => {
|
||||||
|
if (n instanceof Fraction) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
if (typeof n === 'number') {
|
if (typeof n === 'number') {
|
||||||
/*
|
/*
|
||||||
https://github.com/infusion/Fraction.js/#doubles
|
https://github.com/infusion/Fraction.js/#doubles
|
||||||
|
|||||||
@@ -253,7 +253,12 @@ export const pairs = function (xs) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
// Optimized clamp (~1.7x faster than Math.max/min approach)
|
||||||
|
export const clamp = (num, min, max) => {
|
||||||
|
if (num < min) return min;
|
||||||
|
if (num > max) return max;
|
||||||
|
return num;
|
||||||
|
};
|
||||||
|
|
||||||
/* solmization, not used yet */
|
/* solmization, not used yet */
|
||||||
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
||||||
|
|||||||
@@ -36,6 +36,27 @@ export function getWorklet(ac, processor, params, config) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pool of inactive (weakly referenced) voices to be reused if possible
|
||||||
|
const voicePools = {};
|
||||||
|
|
||||||
|
// Cycles through the pool attempting to find an inactive voice that hasn't been GC'd
|
||||||
|
export const claimVoice = (key) => {
|
||||||
|
voicePools[key] ??= [];
|
||||||
|
const pool = voicePools[key];
|
||||||
|
let node;
|
||||||
|
while (pool.length && !node) {
|
||||||
|
const ref = pool.pop();
|
||||||
|
node = ref?.deref?.();
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Releases a voice from use and adds to the inactive pool
|
||||||
|
export const releaseVoice = (key, node) => {
|
||||||
|
voicePools[key] ??= [];
|
||||||
|
voicePools[key].push(new WeakRef(node));
|
||||||
|
};
|
||||||
|
|
||||||
export const getParamADSR = (
|
export const getParamADSR = (
|
||||||
param,
|
param,
|
||||||
attack,
|
attack,
|
||||||
@@ -318,6 +339,17 @@ export function scheduleAtTime(callback, targetTime, audioContext = getAudioCont
|
|||||||
const currentTime = audioContext.currentTime;
|
const currentTime = audioContext.currentTime;
|
||||||
webAudioTimeout(audioContext, callback, currentTime, targetTime);
|
webAudioTimeout(audioContext, callback, currentTime, targetTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let silencer;
|
||||||
|
function getSilencer() {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
if (!silencer) {
|
||||||
|
silencer = gainNode(0);
|
||||||
|
silencer.connect(ac.destination);
|
||||||
|
}
|
||||||
|
return silencer;
|
||||||
|
}
|
||||||
|
|
||||||
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
||||||
// a bit of a hack, but it works very well :)
|
// a bit of a hack, but it works very well :)
|
||||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||||
@@ -325,18 +357,11 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||||||
|
|
||||||
// Certain browsers requires audio nodes to be connected in order for their onended events
|
// Certain browsers requires audio nodes to be connected in order for their onended events
|
||||||
// to fire, so we _mute it_ and then connect it to the destination
|
// to fire, so we _mute it_ and then connect it to the destination
|
||||||
const zeroGain = gainNode(0);
|
const zeroGain = getSilencer();
|
||||||
zeroGain.connect(audioContext.destination);
|
|
||||||
constantNode.connect(zeroGain);
|
constantNode.connect(zeroGain);
|
||||||
|
|
||||||
// Schedule the `onComplete` callback to occur at `stopTime`
|
// Schedule the `onComplete` callback to occur at `stopTime`
|
||||||
constantNode.onended = () => {
|
constantNode.onended = () => {
|
||||||
// Ensure garbage collection
|
|
||||||
try {
|
|
||||||
zeroGain.disconnect();
|
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
constantNode.disconnect();
|
constantNode.disconnect();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -422,11 +447,16 @@ export function applyFM(param, value, begin) {
|
|||||||
|
|
||||||
// Saturation curves
|
// Saturation curves
|
||||||
|
|
||||||
|
const fastTanh = (x) => {
|
||||||
|
const x2 = x * x;
|
||||||
|
const y = (x * (27 + x2)) / (27 + 9 * x2);
|
||||||
|
return y < -1 ? -1 : y > 1 ? 1 : y;
|
||||||
|
};
|
||||||
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
|
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
|
||||||
const _mod = (n, m) => ((n % m) + m) % m;
|
const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
|
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||||
const _soft = (x, k) => Math.tanh(x * (1 + k));
|
const _soft = (x, k) => fastTanh(x * (1 + k));
|
||||||
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
|
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
|
||||||
|
|
||||||
const _fold = (x, k) => {
|
const _fold = (x, k) => {
|
||||||
@@ -466,13 +496,8 @@ const _chebyshev = (x, k) => {
|
|||||||
let tnm1 = 1;
|
let tnm1 = 1;
|
||||||
let tnm2 = x;
|
let tnm2 = x;
|
||||||
let tn;
|
let tn;
|
||||||
let y = 0;
|
let y = x;
|
||||||
for (let i = 1; i < 64; i++) {
|
for (let i = 2; i < 64; i++) {
|
||||||
if (i < 2) {
|
|
||||||
// Already set inital conditions
|
|
||||||
y += i == 0 ? tnm1 : tnm2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
|
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
|
||||||
tnm2 = tnm1;
|
tnm2 = tnm1;
|
||||||
tnm1 = tn;
|
tnm1 = tn;
|
||||||
@@ -537,3 +562,25 @@ export const destroyAudioWorkletNode = (node) => {
|
|||||||
node.disconnect();
|
node.disconnect();
|
||||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setParams = (node, params, t) => {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
t ??= ac.currentTime;
|
||||||
|
const workletParams = node && node.parameters && typeof node.parameters.get === 'function';
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value == null) continue;
|
||||||
|
let param;
|
||||||
|
if (workletParams) {
|
||||||
|
param = node.parameters.get(key);
|
||||||
|
}
|
||||||
|
if (param == null && node[key] instanceof AudioParam) {
|
||||||
|
param = node[key];
|
||||||
|
}
|
||||||
|
if (param instanceof AudioParam) {
|
||||||
|
param.cancelScheduledValues(t);
|
||||||
|
param.setValueAtTime(value, t);
|
||||||
|
} else {
|
||||||
|
node[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -478,15 +478,16 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
const endWithRelease = end + release;
|
const endWithRelease = end + release;
|
||||||
const chainID = Math.round(Math.random() * 1000000);
|
const chainID = Math.round(Math.random() * 1000000);
|
||||||
|
|
||||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
// oldest audio nodes will be stopped if maximum polyphony is exceeded
|
||||||
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
|
while (activeSoundSources.size >= maxPolyphony) {
|
||||||
const ch = activeSoundSources.entries().next();
|
const { value: entry } = activeSoundSources.entries().next();
|
||||||
const source = ch.value[1].deref();
|
if (!entry) break;
|
||||||
const chainID = ch.value[0];
|
const [oldChainID, ref] = entry;
|
||||||
|
const handle = ref?.deref();
|
||||||
const endTime = t + 0.25;
|
const endTime = t + 0.25;
|
||||||
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
|
handle?.node?.gain?.linearRampToValueAtTime?.(0, endTime);
|
||||||
source?.stop?.(endTime);
|
handle?.stop?.(endTime);
|
||||||
activeSoundSources.delete(chainID);
|
activeSoundSources.delete(oldChainID);
|
||||||
}
|
}
|
||||||
|
|
||||||
let audioNodes = [];
|
let audioNodes = [];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { registerSound, soundMap } from './superdough.mjs';
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
|
claimVoice,
|
||||||
destroyAudioWorkletNode,
|
destroyAudioWorkletNode,
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
@@ -12,6 +13,8 @@ import {
|
|||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
|
setParams,
|
||||||
|
releaseVoice,
|
||||||
noises,
|
noises,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
@@ -164,36 +167,38 @@ 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(
|
let o = claimVoice('supersaw');
|
||||||
ac,
|
const params = {
|
||||||
'supersaw-oscillator',
|
frequency,
|
||||||
{
|
begin,
|
||||||
frequency,
|
end,
|
||||||
begin,
|
freqspread: detune,
|
||||||
end,
|
panspread,
|
||||||
freqspread: detune,
|
};
|
||||||
voices,
|
const processorOptions = {
|
||||||
panspread,
|
voices,
|
||||||
},
|
};
|
||||||
{
|
if (o == null) {
|
||||||
|
o = getWorklet(ac, 'supersaw-oscillator', params, {
|
||||||
outputChannelCount: [2],
|
outputChannelCount: [2],
|
||||||
},
|
processorOptions,
|
||||||
);
|
});
|
||||||
|
} else {
|
||||||
|
setParams(o, params);
|
||||||
|
o.port.postMessage({ type: 'reset', payload: { processorOptions } });
|
||||||
|
}
|
||||||
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);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
let envGain = gainNode(1);
|
const envGain = o.connect(gainNode(1));
|
||||||
envGain = o.connect(envGain);
|
|
||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||||
|
const timeoutNode = webAudioTimeout(
|
||||||
let timeoutNode = webAudioTimeout(
|
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
destroyAudioWorkletNode(o);
|
||||||
envGain.disconnect();
|
envGain.disconnect();
|
||||||
|
releaseVoice('supersaw', o);
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
@@ -267,7 +272,7 @@ export function registerSynthSounds() {
|
|||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
|
|
||||||
let timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
destroyAudioWorkletNode(o);
|
||||||
@@ -341,7 +346,7 @@ export function registerSynthSounds() {
|
|||||||
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
||||||
lfo.connect(o.parameters.get('pulsewidth'));
|
lfo.connect(o.parameters.get('pulsewidth'));
|
||||||
}
|
}
|
||||||
let timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
destroyAudioWorkletNode(o);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { logger } from './logger.mjs';
|
|||||||
// currently duplicate with core util.mjs to skip dependency
|
// currently duplicate with core util.mjs to skip dependency
|
||||||
// TODO: add separate util module?
|
// TODO: add separate util module?
|
||||||
|
|
||||||
|
export const LN2 = Math.LN2;
|
||||||
|
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
@@ -32,10 +34,9 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
||||||
|
|
||||||
export const freqToMidi = (freq) => {
|
export const freqToMidi = (freq) => {
|
||||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
return (12 * Math.log(freq / 440)) / LN2 + 69;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const valueToMidi = (value, fallbackValue) => {
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
@@ -114,3 +115,38 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
export const pickAndRename = (source, map) => {
|
export const pickAndRename = (source, map) => {
|
||||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Optimized / numerical functions (ideal for hot dsp loops)
|
||||||
|
|
||||||
|
// Optimized clamp (~1.7x faster than Math.max/min approach)
|
||||||
|
export const clamp = (num, min, max) => {
|
||||||
|
if (num < min) return min;
|
||||||
|
if (num > max) return max;
|
||||||
|
return num;
|
||||||
|
};
|
||||||
|
export const mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
export const lerp = (a, b, n) => n * (b - a) + a;
|
||||||
|
export const frac = (x) => x - Math.floor(x);
|
||||||
|
|
||||||
|
// Fast integer ops for non-negative values
|
||||||
|
export const ffloor = (x) => x | 0;
|
||||||
|
export const fround = (x) => ffloor(x + 0.5);
|
||||||
|
export const fceil = (x) => ffloor(x + 1);
|
||||||
|
export const ffrac = (x) => x - ffloor(x);
|
||||||
|
|
||||||
|
export const fastexpm1 = (x) => x * (1 + x + 0.5 * x * x); // Taylor approximation
|
||||||
|
|
||||||
|
export const fastpow2 = (x) => {
|
||||||
|
// Taylor approximation of 2 ^ x
|
||||||
|
const a = x * LN2;
|
||||||
|
return 1 + a * (1 + a * (0.5 + a / 6));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||||
|
return frequency * Math.pow(2, detune / 12);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyFastDetune = (frequency, detune) => {
|
||||||
|
if (detune < 4) return frequency * fastpow2(detune / 12);
|
||||||
|
return applySemitoneDetuneToFrequency(frequency, detune);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ import { getCommonSampleInfo } from './util.mjs';
|
|||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
destroyAudioWorkletNode,
|
claimVoice,
|
||||||
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
getFrequencyFromValue,
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
|
setParams,
|
||||||
|
releaseVoice,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
|
destroyAudioWorkletNode,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
@@ -218,32 +222,39 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
const frequency = getFrequencyFromValue(value);
|
const frequency = getFrequencyFromValue(value);
|
||||||
const { url, label } = getCommonSampleInfo(value, tables);
|
const { url, label } = getCommonSampleInfo(value, tables);
|
||||||
const payload = await getPayload(url, label, frameLen);
|
const payload = await getPayload(url, label, frameLen);
|
||||||
|
const voiceKey = `wavetable:${payload.key}`;
|
||||||
let holdEnd = t + duration;
|
let holdEnd = t + duration;
|
||||||
if (clip !== undefined) {
|
if (clip !== undefined) {
|
||||||
holdEnd = Math.min(t + clip * duration, holdEnd);
|
holdEnd = Math.min(t + clip * duration, holdEnd);
|
||||||
}
|
}
|
||||||
const endWithRelease = holdEnd + release;
|
const endWithRelease = holdEnd + release;
|
||||||
const envEnd = endWithRelease + 0.01;
|
const envEnd = endWithRelease + 0.01;
|
||||||
const source = getWorklet(
|
let source = claimVoice(voiceKey);
|
||||||
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,
|
};
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
const processorOptions = {
|
||||||
panspread: value.spread,
|
phaseRand: value.wtphaserand ?? (value.unison > 1 ? 1 : 0),
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
voices: Math.max(value.unison ?? 1, 1),
|
||||||
},
|
};
|
||||||
{ outputChannelCount: [2] },
|
if (source == null) {
|
||||||
);
|
source = getWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2], processorOptions });
|
||||||
|
} else {
|
||||||
|
setParams(source, params);
|
||||||
|
source.port.postMessage({ type: 'reset', payload: { processorOptions } });
|
||||||
|
}
|
||||||
source.port.postMessage({ type: 'table', payload });
|
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');
|
||||||
|
destroyAudioWorkletNode(source);
|
||||||
|
releaseVoice(voiceKey, source);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
|
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
|
||||||
@@ -310,20 +321,20 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
);
|
);
|
||||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||||
const envGain = ac.createGain();
|
const envGain = source.connect(gainNode(1));
|
||||||
const node = source.connect(envGain);
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
|
||||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||||
const handle = { node, source };
|
const handle = { node: envGain };
|
||||||
const timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(source);
|
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
node.disconnect();
|
|
||||||
wtPosModulators?.disconnect();
|
wtPosModulators?.disconnect();
|
||||||
wtWarpModulators?.disconnect();
|
wtWarpModulators?.disconnect();
|
||||||
|
envGain.disconnect();
|
||||||
|
destroyAudioWorkletNode(source);
|
||||||
|
releaseVoice(voiceKey, source);
|
||||||
onended();
|
onended();
|
||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
|
|||||||
+127
-134
@@ -5,27 +5,29 @@
|
|||||||
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 {
|
||||||
|
applyFastDetune,
|
||||||
|
applySemitoneDetuneToFrequency,
|
||||||
|
clamp,
|
||||||
|
fastexpm1,
|
||||||
|
fceil,
|
||||||
|
ffloor,
|
||||||
|
ffrac,
|
||||||
|
frac,
|
||||||
|
fround,
|
||||||
|
lerp,
|
||||||
|
} from './util.mjs';
|
||||||
|
|
||||||
const blockSize = 128;
|
const blockSize = 128;
|
||||||
const PI = Math.PI;
|
const PI = Math.PI;
|
||||||
const TWO_PI = 2 * PI;
|
const TWO_PI = 2 * PI;
|
||||||
const INVSR = 1 / sampleRate;
|
const INVSR = 1 / sampleRate;
|
||||||
|
const MAX_VOICES = 64;
|
||||||
|
|
||||||
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;
|
|
||||||
const pv = (arr, n) => arr[n] ?? arr[0];
|
const pv = (arr, n) => arr[n] ?? arr[0];
|
||||||
const frac = (x) => x - Math.floor(x);
|
|
||||||
|
|
||||||
// Fast integer ops for non-negative values
|
|
||||||
const ffloor = (x) => x | 0;
|
|
||||||
const fround = (x) => ffloor(x + 0.5);
|
|
||||||
const fceil = (x) => ffloor(x + 1);
|
|
||||||
const ffrac = (x) => x - ffloor(x);
|
|
||||||
|
|
||||||
const fast_tanh = (x) => {
|
const fast_tanh = (x) => {
|
||||||
const x2 = x ** 2;
|
const x2 = x * x;
|
||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
return (x * (27 + x2)) / (27 + 9 * x2);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optimized per-voice detuner which precomputes constants
|
// Optimized per-voice detuner which precomputes constants
|
||||||
@@ -38,10 +40,6 @@ const getDetuner = (unison, detune) => {
|
|||||||
return (voiceIdx) => voiceIdx * scale - center;
|
return (voiceIdx) => voiceIdx * scale - center;
|
||||||
};
|
};
|
||||||
|
|
||||||
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
|
||||||
return frequency * Math.pow(2, detune / 12);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
||||||
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||||
function polyBlep(phase, dt) {
|
function polyBlep(phase, dt) {
|
||||||
@@ -169,10 +167,11 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
const dt = frequency * INVSR;
|
const dt = frequency * INVSR;
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
|
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||||
|
modval = curve !== 1 ? Math.pow(modval, curve) : modval;
|
||||||
|
const out = clamp(modval, min, max);
|
||||||
for (let i = 0; i < output.length; i++) {
|
for (let i = 0; i < output.length; i++) {
|
||||||
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
output[i][n] = out;
|
||||||
modval = Math.pow(modval, curve);
|
|
||||||
output[i][n] = clamp(modval, min, max);
|
|
||||||
}
|
}
|
||||||
this.incrementPhase(dt);
|
this.incrementPhase(dt);
|
||||||
}
|
}
|
||||||
@@ -443,7 +442,7 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
this.started = hasInput;
|
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 = fastexpm1(pv(parameters.distort, n));
|
||||||
for (let ch = 0; ch < input.length; ch++) {
|
for (let ch = 0; ch < input.length; ch++) {
|
||||||
const x = input[ch][n];
|
const x = input[ch][n];
|
||||||
output[ch][n] = postgain * this.algorithm(x, shape);
|
output[ch][n] = postgain * this.algorithm(x, shape);
|
||||||
@@ -456,9 +455,17 @@ registerProcessor('distort-processor', DistortProcessor);
|
|||||||
|
|
||||||
// SUPERSAW
|
// SUPERSAW
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor(options) {
|
||||||
super();
|
super();
|
||||||
this.phase = [];
|
this.phases = new Float32Array(MAX_VOICES);
|
||||||
|
this.reset(options.processorOptions);
|
||||||
|
}
|
||||||
|
reset(options) {
|
||||||
|
const phases = this.phases;
|
||||||
|
for (let i = 0; i < phases.length; i++) {
|
||||||
|
phases[i] = Math.random();
|
||||||
|
}
|
||||||
|
this.voices = options.voices;
|
||||||
}
|
}
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
@@ -468,20 +475,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'end',
|
name: 'end',
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
min: 0,
|
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,53 +502,57 @@ 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) {
|
process(_input, outputs, params) {
|
||||||
if (currentTime <= params.begin[0]) {
|
const started = currentTime >= params.begin[0];
|
||||||
|
const ended = currentTime >= params.end[0];
|
||||||
|
if (!started || ended) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (currentTime >= params.end[0]) {
|
|
||||||
// this.port.postMessage({ type: 'onended' });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0]; // k-rate
|
// fast k-rate paths
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
let freq, detune, freqspread, detuner;
|
||||||
const detune = pv(params.detune, i);
|
if (params.freqspread.length === 1) {
|
||||||
const freqspread = pv(params.freqspread, i);
|
freqspread = params.freqspread[0];
|
||||||
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
detuner = getDetuner(this.voices, freqspread);
|
||||||
let gainL = Math.sqrt(1 - panspread);
|
}
|
||||||
let gainR = Math.sqrt(panspread);
|
if (params.frequency.length === 1 && params.detune.length === 1) {
|
||||||
let freq = pv(params.frequency, i);
|
freq = params.frequency[0];
|
||||||
// Main detuning
|
detune = params.detune[0];
|
||||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||||
const detuner = getDetuner(voices, freqspread);
|
}
|
||||||
for (let n = 0; n < voices; n++) {
|
let panspread, gainL, gainR;
|
||||||
|
if (params.panspread.length === 1) {
|
||||||
|
panspread = this.voices > 1 ? clamp(params.panspread[0], 0, 1) : 0;
|
||||||
|
gainL = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
|
gainR = Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
|
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||||
|
gainL = Math.sqrt(1 - panspread);
|
||||||
|
gainR = Math.sqrt(panspread);
|
||||||
|
// Main detuning (a-rate path)
|
||||||
|
detune ??= pv(params.detune, i);
|
||||||
|
freqspread ??= pv(params.freqspread, i);
|
||||||
|
freq ??= applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
|
||||||
|
detuner ??= getDetuner(this.voices, freqspread);
|
||||||
|
for (let n = 0; n < this.voices; n++) {
|
||||||
// Individual voice detuning
|
// Individual voice detuning
|
||||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
const freqVoice = applyFastDetune(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
|
||||||
// has domain [0, 1]
|
// has domain [0, 1]
|
||||||
const dt = frac(freqVoice * INVSR);
|
const dt = frac(freqVoice * INVSR);
|
||||||
this.phase[n] = this.phase[n] ?? Math.random();
|
const v = waveshapes.sawblep(this.phases[n], dt);
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
|
||||||
|
|
||||||
output[0][i] += v * gainL;
|
output[0][i] += v * gainL;
|
||||||
output[1][i] += v * gainR;
|
output[1][i] += v * gainR;
|
||||||
|
let pn = this.phases[n] + dt;
|
||||||
let pn = this.phase[n] + dt;
|
|
||||||
if (pn >= 1.0) pn -= 1.0;
|
if (pn >= 1.0) pn -= 1.0;
|
||||||
this.phase[n] = pn;
|
this.phases[n] = pn;
|
||||||
// invert right and left gain
|
// invert right and left gain
|
||||||
|
const tmp = gainL;
|
||||||
gainL = gainR;
|
gainL = gainR;
|
||||||
gainR = gainL;
|
gainR = tmp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1124,10 +1132,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
{ 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, automationRate: 'k-rate' },
|
||||||
{ 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 },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1135,35 +1141,32 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
super(options);
|
super(options);
|
||||||
this.frameLen = 0;
|
this.frameLen = 0;
|
||||||
this.numFrames = 0;
|
this.numFrames = 0;
|
||||||
this.phase = [];
|
this.phases = new Float32Array(MAX_VOICES);
|
||||||
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
if (type === 'table') {
|
if (type === 'table') {
|
||||||
const key = payload.key;
|
const key = payload.key;
|
||||||
this.frameLen = payload.frameLen;
|
this.frameLen = payload.frameLen;
|
||||||
if (!tablesCache[key]) {
|
if (!tablesCache[key]) {
|
||||||
const tables = [payload.frames];
|
tablesCache[key] = 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.table = tablesCache[key];
|
||||||
this.numFrames = this.tables[0].length;
|
this.numFrames = this.table.length;
|
||||||
|
} else if (type === 'reset') {
|
||||||
|
this.reset(payload.processorOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.reset(options.processorOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(options) {
|
||||||
|
const phaseRand = options.phaseRand ?? 1;
|
||||||
|
const phases = this.phases;
|
||||||
|
for (let i = 0; i < phases.length; i++) {
|
||||||
|
phases[i] = Math.random() * phaseRand;
|
||||||
|
}
|
||||||
|
this.voices = options.voices;
|
||||||
|
this.normalizer = 1 / Math.sqrt(this.voices);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mirror(x) {
|
_mirror(x) {
|
||||||
@@ -1308,73 +1311,63 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
|
|
||||||
_chooseMip(dphi) {
|
process(_inputs, outputs, params) {
|
||||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
const started = currentTime >= params.begin[0];
|
||||||
let level = 0;
|
const ended = currentTime >= params.end[0];
|
||||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
if (!started || ended) {
|
||||||
level++;
|
|
||||||
}
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
|
||||||
if (currentTime >= parameters.end[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentTime <= parameters.begin[0]) {
|
|
||||||
return true;
|
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) {
|
// fast k-rate paths
|
||||||
outL.fill(0);
|
let freq, detune, freqspread, detuner;
|
||||||
if (outR !== outL) outR.set(outL);
|
if (params.freqspread.length === 1) {
|
||||||
return true;
|
freqspread = params.freqspread[0];
|
||||||
|
detuner = getDetuner(this.voices, freqspread);
|
||||||
}
|
}
|
||||||
const voices = parameters.voices[0]; // k-rate
|
if (params.frequency.length === 1 && params.detune.length === 1) {
|
||||||
|
freq = params.frequency[0];
|
||||||
|
detune = params.detune[0];
|
||||||
|
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||||
|
}
|
||||||
|
let panspread, gainL, gainR;
|
||||||
|
if (params.panspread.length === 1) {
|
||||||
|
panspread = this.voices > 1 ? clamp(params.panspread[0], 0, 1) : 0;
|
||||||
|
gainL = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
|
gainR = Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
|
}
|
||||||
|
const warpMode = params.warpMode[0];
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const tablePos = clamp(pv(params.position, i), 0, 1);
|
||||||
const freqspread = pv(parameters.freqspread, i);
|
|
||||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
const interpT = idx - fIdx;
|
const interpT = idx - fIdx;
|
||||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
const warpAmount = clamp(pv(params.warp, i), 0, 1);
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
panspread ??= this.voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0;
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
gainL ??= Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
gainR ??= Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
detune ??= pv(params.detune, i);
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
freqspread ??= pv(params.freqspread, i);
|
||||||
let f = pv(parameters.frequency, i);
|
freq ??= applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); // overall detune
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
detuner ??= getDetuner(this.voices, freqspread);
|
||||||
const normalizer = 1 / Math.sqrt(voices);
|
for (let n = 0; n < this.voices; n++) {
|
||||||
const detuner = getDetuner(voices, freqspread);
|
const freqVoice = applyFastDetune(freq, detuner(n)); // voice detune
|
||||||
for (let n = 0; n < voices; n++) {
|
const dPhase = freqVoice * INVSR;
|
||||||
const isOdd = (n & 1) == 1;
|
|
||||||
let gainL = gain1;
|
|
||||||
let gainR = gain2;
|
|
||||||
// invert right and left gain
|
|
||||||
if (isOdd) {
|
|
||||||
gainL = gain2;
|
|
||||||
gainR = gain1;
|
|
||||||
}
|
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
|
||||||
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;
|
const ph = this._warpPhase(this.phases[n], warpAmount, warpMode);
|
||||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
const s0 = this._sampleFrame(this.table[fIdx], ph);
|
||||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||||
const s1 = this._sampleFrame(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.phases[n] < warpAmount) {
|
||||||
s = -s;
|
s = -s;
|
||||||
}
|
}
|
||||||
outL[i] += s * gainL * normalizer;
|
outL[i] += s * gainL * this.normalizer;
|
||||||
outR[i] += s * gainR * normalizer;
|
outR[i] += s * gainR * this.normalizer;
|
||||||
this.phase[n] = frac(this.phase[n] + dPhase);
|
this.phases[n] = frac(this.phases[n] + dPhase);
|
||||||
|
// invert right and left gain
|
||||||
|
const tmp = gainL;
|
||||||
|
gainR = gainL;
|
||||||
|
gainL = tmp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Dough } from './dough.mjs';
|
import { Dough } from './dough.mjs';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => {
|
||||||
|
if (num < min) return min;
|
||||||
|
if (num > max) return max;
|
||||||
|
return num;
|
||||||
|
};
|
||||||
|
|
||||||
class DoughProcessor extends AudioWorkletProcessor {
|
class DoughProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
|||||||
const ISR = 1 / SAMPLE_RATE;
|
const ISR = 1 / SAMPLE_RATE;
|
||||||
|
|
||||||
let gainCurveFunc = (val) => Math.pow(val, 2);
|
let gainCurveFunc = (val) => Math.pow(val, 2);
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => {
|
||||||
|
if (num < min) return min;
|
||||||
|
if (num > max) return max;
|
||||||
|
return num;
|
||||||
|
};
|
||||||
|
|
||||||
function applyGainCurve(val) {
|
function applyGainCurve(val) {
|
||||||
return gainCurveFunc(val);
|
return gainCurveFunc(val);
|
||||||
|
|||||||
Reference in New Issue
Block a user