Compare commits

..

7 Commits

Author SHA1 Message Date
Felix Roos 301fb5e59a Merge remote-tracking branch 'origin/main' into example-fixes 2025-11-12 21:35:03 +01:00
Felix Roos 9ac691c19e fix: naming in draw package readme 2025-11-12 21:26:00 +01:00
Felix Roos 6e7512016c chore: package-lock 2025-11-12 21:25:47 +01:00
Felix Roos 3bbd808783 fix: use published web package for headless-repl 2025-11-12 21:25:40 +01:00
Felix Roos 7b245abd1a fix: headless-repl stop button 2025-11-12 21:25:18 +01:00
Felix Roos 9e5c9c7539 fix: update web example 2025-11-12 21:25:08 +01:00
Felix Roos d14194db6a fix: codemirror-repl example 2025-11-12 21:24:56 +01:00
29 changed files with 605 additions and 1156 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/web@1.0.3"></script> <script src="https://unpkg.com/@strudel/web@1.2.6"></script>
<button id="play">PLAY</button> <button id="play">PLAY</button>
<script> <script>
initStrudel({ initStrudel({
+8 -8
View File
@@ -12,14 +12,14 @@
"vite": "^6.0.11" "vite": "^6.0.11"
}, },
"dependencies": { "dependencies": {
"@strudel/codemirror": "workspace:*", "@strudel/codemirror": "1.2.6",
"@strudel/core": "workspace:*", "@strudel/core": "1.2.5",
"@strudel/draw": "workspace:*", "@strudel/draw": "1.2.5",
"@strudel/mini": "workspace:*", "@strudel/mini": "1.2.5",
"@strudel/soundfonts": "workspace:*", "@strudel/soundfonts": "1.2.6",
"@strudel/tonal": "workspace:*", "@strudel/tonal": "1.2.5",
"@strudel/transpiler": "workspace:*", "@strudel/transpiler": "1.2.5",
"@strudel/webaudio": "workspace:*" "@strudel/webaudio": "1.2.6"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
+5 -8
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0 export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples') samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/') samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2" "<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo // make it 22 edo
@@ -28,13 +28,12 @@ samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-dru
.mask("<0@4 1@12>/4") .mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>") .reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it... // wait for it...
).fast(2/3) ).fast(4/3)
//.crush(6) // remove "//" if you dare`; //.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0 export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples') samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', { addVoicings('hip', {
m11: ['2M 3m 4P 7m'], m11: ['2M 3m 4P 7m'],
@@ -61,16 +60,14 @@ stack(
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2) s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
) )
) )
.late("[0 .05]*2").late(12) .late("[0 .05]*2").late(12).fast(2)
`; `;
export const funk42 = `// froos - how to funk in 42 lines of code export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg // adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4 // thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
samples('github:felixroos/samples') samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5) setcps(.5)
+1 -1
View File
@@ -13,7 +13,7 @@
<button id="c">C</button> <button id="c">C</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script type="module"> <script type="module">
import { initStrudel } from '@strudel/web'; import { initStrudel, hush } from '@strudel/web';
initStrudel({ initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'), prebake: () => samples('github:tidalcycles/dirt-samples'),
}); });
+1 -1
View File
@@ -13,7 +13,7 @@
"vite": "^6.0.11" "vite": "^6.0.11"
}, },
"dependencies": { "dependencies": {
"@strudel/web": "workspace:*" "@strudel/web": "1.2.6"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
+2 -19
View File
@@ -162,7 +162,6 @@ 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));
@@ -178,7 +177,7 @@ export class StrudelMirror {
onToggle: (started) => { onToggle: (started) => {
replOptions?.onToggle?.(started); replOptions?.onToggle?.(started);
if (started) { if (started) {
this.shouldAnimate() && this.drawer.start(this.repl.scheduler); 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(
@@ -208,10 +207,9 @@ 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 = this.hasPainters ? this.drawTime : [0, 0]; const drawTime = options.pattern.getPainters().length ? 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);
@@ -239,7 +237,6 @@ 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);
@@ -324,17 +321,8 @@ 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';
} }
@@ -355,11 +343,6 @@ 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);
} }
+1
View File
@@ -2264,6 +2264,7 @@ export const { tsdelay } = registerControl('tsdelay');
export const { real } = registerControl('real'); export const { real } = registerControl('real');
export const { imag } = registerControl('imag'); export const { imag } = registerControl('imag');
export const { enhance } = registerControl('enhance'); export const { enhance } = registerControl('enhance');
export const { partials } = registerControl('partials');
export const { comb } = registerControl('comb'); export const { comb } = registerControl('comb');
export const { smear } = registerControl('smear'); export const { smear } = registerControl('smear');
export const { scram } = registerControl('scram'); export const { scram } = registerControl('scram');
-3
View File
@@ -91,9 +91,6 @@ 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
-63
View File
@@ -3624,66 +3624,3 @@ for (const name of distAlgoNames) {
return this.distort(argsPat); return this.distort(argsPat);
}; };
} }
/**
* Turns a list of patterns into a single pattern which outputs list-values
*
* @name parray
* @returns Pattern
*/
export const parray = (pats) => {
const pack = (...xs) => xs;
let acc = pure(curry(pack, null, pats.length));
for (const p of pats) acc = acc.appBoth(reify(p));
return acc;
};
const _ensureListPattern = (list) => {
if (Array.isArray(list)) {
return parray(list);
}
return reify(list);
};
/**
* Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
*
* Can also be used to create a new synth via `s('user').partials(...)`
*
* @name partials
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
* @example
* s("user").seg(16).n(irand(8)).scale("A:major")
* .partials([1, 0, 1, 0, 0, 1])
* @example
* s("saw").seg(8).n(irand(12)).scale("G#:minor")
* .partials(binaryL(irand(256).add("1")))
*/
Pattern.prototype.partials = function (list) {
return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
};
// Also create a top-level function
export const partials = (list) => {
return _ensureListPattern(list).as('partials');
};
/**
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
*
* @name phases
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
* @example
* // Phase cancellation
* s("saw").seg(8).n(irand(12)).scale("G#1:minor")
* .partials(partials([1, 1, 1]))
* .superimpose(x => x.phases([0.5, 0.5, 0.5]))
*/
Pattern.prototype.phases = function (list) {
return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
};
// Also create a top-level function
export const phases = (list) => {
return _ensureListPattern(list).as('phases');
};
+3 -14
View File
@@ -152,9 +152,9 @@ export function repl({
// allows muting a pattern x with x_ or _x // allows muting a pattern x with x_ or _x
return silence; return silence;
} }
if (id.includes('$')) { if (id === '$') {
// allows adding anonymous patterns with $: // allows adding anonymous patterns with $:
id = `${id}${anonymousIndex}`; id = `$${anonymousIndex}`;
anonymousIndex++; anonymousIndex++;
} }
pPatterns[id] = this; pPatterns[id] = this;
@@ -215,19 +215,8 @@ export function repl({
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) { if (Object.keys(pPatterns).length) {
let patterns = []; let patterns = [];
let soloActive = false;
for (const [key, value] of Object.entries(pPatterns)) { for (const [key, value] of Object.entries(pPatterns)) {
// handle soloed patterns ex: S$: s("bd!4") patterns.push(value.withState((state) => state.setControls({ id: key })));
const isSolod = key.length > 1 && key.startsWith('S');
if (isSolod && soloActive === false) {
// first time we see a soloed pattern, clear existing patterns
patterns = [];
soloActive = true;
}
if (!soloActive || (soloActive && isSolod)) {
const valWithState = value.withState((state) => state.setControls({ id: key }));
patterns.push(valWithState);
}
} }
if (eachTransform) { if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed // Explicit lambda so only element (not index and array) are passed
+3 -48
View File
@@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
export const run = (n) => saw.range(0, n).round().segment(n); export const run = (n) => saw.range(0, n).round().segment(n);
/** /**
* Creates a binary pattern from a number. * Creates a pattern from a binary number.
* *
* @name binary * @name binary
* @param {number} n - input number to convert to binary * @param {number} n - input number to convert to binary
@@ -242,7 +242,7 @@ export const binary = (n) => {
}; };
/** /**
* Creates a binary pattern from a number, padded to n bits long. * Creates a pattern from a binary number, padded to n bits long.
* *
* @name binaryN * @name binaryN
* @param {number} n - input number to convert to binary * @param {number} n - input number to convert to binary
@@ -258,51 +258,6 @@ export const binaryN = (n, nBits = 16) => {
return reify(n).segment(nBits).brshift(bitPos).band(pure(1)); return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
}; };
/**
* Creates a binary list pattern from a number.
*
* @name binaryL
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
*/
export const binaryL = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
return binaryNL(n, nBits);
};
/**
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
export const binaryNL = (n, nBits = 16) => {
return reify(n)
.withValue((v) => (bits) => {
const bList = [];
for (let i = bits - 1; i >= 0; i--) {
bList.push((v >> i) & 1);
}
return bList;
})
.appLeft(reify(nBits));
};
/**
* Creates a list of random numbers of the given length
*
* @name randL
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => { export const randrun = (n) => {
return signal((t) => { return signal((t) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,... // Without adding 0.5, the first cycle is always 0,1,2,3,...
@@ -524,7 +479,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* @example * @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example * @example
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note() * wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
* @example * @example
* // The probability can itself be a pattern * // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s() * wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
+1 -6
View File
@@ -253,12 +253,7 @@ export const pairs = function (xs) {
return result; return result;
}; };
// Optimized clamp (~1.7x faster than Math.max/min approach) export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
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*/
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/canvas # @strudel/draw
Helpers for drawing with the Canvas API and Strudel Helpers for drawing with the Canvas API and Strudel
## Install ## Install
```sh ```sh
npm i @strudel/canvas --save npm i @strudel/draw --save
``` ```
+1 -1
View File
@@ -89,7 +89,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `decay`: seconds of decay phase - `decay`: seconds of decay phase
- `sustain`: gain of sustain phase - `sustain`: gain of sustain phase
- `release`: seconds of release phase - `release`: seconds of release phase
- `deadline`: seconds from audio context initialization before playing the sound (getAudioContextCurrentTime() = immediate) - `deadline`: seconds until the sound should play (0 = immediate)
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds - `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### registerSynthSounds() ### registerSynthSounds()
+18 -77
View File
@@ -36,27 +36,6 @@ 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,
@@ -259,7 +238,7 @@ export function createFilter(context, start, end, params, cps) {
if (sync != null) { if (sync != null) {
rate = cps * sync; rate = cps * sync;
} }
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate, min: 10, max: 20000 }; const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
getParamLfo(context, frequencyParam, start, end, lfoValues); getParamLfo(context, frequencyParam, start, end, lfoValues);
return filter; return filter;
} }
@@ -283,15 +262,7 @@ export function drywet(dry, wet, wetAmount = 0) {
let mix = ac.createGain(); let mix = ac.createGain();
dry_gain.connect(mix); dry_gain.connect(mix);
wet_gain.connect(mix); wet_gain.connect(mix);
return { return mix;
node: mix,
onended: () => {
dry_gain.disconnect(mix);
wet_gain.disconnect(mix);
dry.disconnect(dry_gain);
wet.disconnect(wet_gain);
},
};
} }
let curves = ['linear', 'exponential']; let curves = ['linear', 'exponential'];
@@ -326,10 +297,6 @@ export function getVibratoOscillator(param, value, t) {
gain.gain.value = vibmod * 100; gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain); vibratoOscillator.connect(gain);
gain.connect(param); gain.connect(param);
vibratoOscillator.onended = () => {
gain.disconnect(param);
vibratoOscillator.disconnect(gain);
};
vibratoOscillator.start(t); vibratoOscillator.start(t);
return vibratoOscillator; return vibratoOscillator;
} }
@@ -339,17 +306,6 @@ 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) {
@@ -357,11 +313,18 @@ 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 = getSilencer(); const zeroGain = gainNode(0);
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 {
@@ -447,16 +410,11 @@ 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) => fastTanh(x * (1 + k)); const _soft = (x, k) => Math.tanh(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) => {
@@ -496,8 +454,13 @@ const _chebyshev = (x, k) => {
let tnm1 = 1; let tnm1 = 1;
let tnm2 = x; let tnm2 = x;
let tn; let tn;
let y = x; let y = 0;
for (let i = 2; i < 64; i++) { for (let i = 1; 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;
@@ -562,25 +525,3 @@ 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;
}
}
};
+1 -2
View File
@@ -65,9 +65,8 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
export function getNoiseMix(inputNode, wet, t) { export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t); const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet); const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
noiseOscillator.node.onended = () => noiseMix.onended();
return { return {
node: noiseMix.node, node: noiseMix,
stop: (time) => noiseOscillator?.stop(time), stop: (time) => noiseOscillator?.stop(time),
}; };
} }
-2
View File
@@ -104,8 +104,6 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
player.start(); player.start();
context.oncomplete = function (event) { context.oncomplete = function (event) {
callback(event.renderedBuffer); callback(event.renderedBuffer);
filter.disconnect();
player.disconnect();
}; };
context.startRendering(); context.startRendering();
+11 -14
View File
@@ -478,16 +478,15 @@ 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 stopped if maximum polyphony is exceeded // oldest audio nodes will be destroyed if maximum polyphony is exceeded
while (activeSoundSources.size >= maxPolyphony) { for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const { value: entry } = activeSoundSources.entries().next(); const ch = activeSoundSources.entries().next();
if (!entry) break; const source = ch.value[1];
const [oldChainID, ref] = entry; const chainID = ch.value[0];
const handle = ref?.deref();
const endTime = t + 0.25; const endTime = t + 0.25;
handle?.node?.gain?.linearRampToValueAtTime?.(0, endTime); source?.node?.gain?.linearRampToValueAtTime(0, endTime);
handle?.stop?.(endTime); source?.stop?.(endTime);
activeSoundSources.delete(oldChainID); activeSoundSources.delete(chainID);
} }
let audioNodes = []; let audioNodes = [];
@@ -514,7 +513,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC activeSoundSources.set(chainID, soundHandle);
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -693,8 +692,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// delay // delay
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
orbitBus.getDelay(delaytime, delayfeedback, t); orbitBus.getDelay(delaytime, delayfeedback, t);
const send = orbitBus.sendDelay(post, delay); orbitBus.sendDelay(post, delay);
audioNodes.push(send);
} }
// reverb // reverb
if (room > 0) { if (room > 0) {
@@ -710,8 +708,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomIR = await loadBuffer(url, ac, ir, 0); roomIR = await loadBuffer(url, ac, ir, 0);
} }
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
const send = orbitBus.sendReverb(post, room); orbitBus.sendReverb(post, room);
audioNodes.push(send);
} }
if (djf != null) { if (djf != null) {
+2 -2
View File
@@ -78,11 +78,11 @@ export class Orbit {
return this.reverbNode; return this.reverbNode;
} }
sendReverb(node, amount) { sendReverb(node, amount) {
return effectSend(node, this.reverbNode, amount); effectSend(node, this.reverbNode, amount);
} }
sendDelay(node, amount) { sendDelay(node, amount) {
return effectSend(node, this.delayNode, amount); effectSend(node, this.delayNode, amount);
} }
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
+42 -73
View File
@@ -3,7 +3,6 @@ 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,
@@ -13,15 +12,12 @@ import {
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
getWorklet, getWorklet,
setParams,
releaseVoice,
noises, noises,
webAudioTimeout, webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [ const waveformAliases = [
['tri', 'triangle'], ['tri', 'triangle'],
['sqr', 'square'], ['sqr', 'square'],
@@ -51,17 +47,19 @@ export function registerSynthSounds() {
[0.001, 0.05, 0.6, 0.01], [0.001, 0.05, 0.6, 0.01],
); );
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down // turn down
const g = gainNode(0.3); const g = gainNode(0.3);
let sound = getOscillator(s, t, value, () => { const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect(); g.disconnect();
onended(); onended();
}); };
let { node: o, stop, triggerRelease } = sound;
const { duration } = value;
const envGain = gainNode(1); const envGain = gainNode(1);
let node = o.connect(g).connect(envGain); let node = o.connect(g).connect(envGain);
@@ -167,38 +165,36 @@ 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 = claimVoice('supersaw'); let o = getWorklet(
const params = { ac,
'supersaw-oscillator',
{
frequency, frequency,
begin, begin,
end, end,
freqspread: detune, freqspread: detune,
panspread,
};
const processorOptions = {
voices, voices,
}; panspread,
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);
const envGain = o.connect(gainNode(1)); let envGain = 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();
@@ -272,7 +268,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');
const timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); destroyAudioWorkletNode(o);
@@ -346,7 +342,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'));
} }
const timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); destroyAudioWorkletNode(o);
@@ -418,13 +414,9 @@ export function registerSynthSounds() {
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
} }
const PI2 = 2 * Math.PI; export function waveformN(partials, type) {
export function waveformN(partials, phases, type) { const real = new Float32Array(partials + 1);
const isList = typeof partials === 'object'; const imag = new Float32Array(partials + 1);
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 ac = getAudioContext();
const osc = ac.createOscillator(); const osc = ac.createOscillator();
@@ -432,29 +424,20 @@ export function waveformN(partials, phases, type) {
sawtooth: (n) => [0, -1 / n], sawtooth: (n) => [0, -1 / n],
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n], square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0], triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
user: (_n) => [0, 1],
}; };
if (!terms[type]) { if (!terms[type]) {
throw new Error(`unknown wave type ${type}`); throw new Error(`unknown wave type ${type}`);
} }
for (let n = 0; n < len; n++) { real[0] = 0; // dc offset
const mag = partials[n]; imag[0] = 0;
const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset let n = 1;
const phase = phases?.[n] ?? 0; while (n <= partials) {
// Scale by `partials` const [r, i] = terms[type](n);
let R = r * mag; real[n] = r;
let I = i * mag; imag[n] = i;
// Apply rotation by the phase n++;
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); const wave = ac.createPeriodicWave(real, imag);
@@ -463,28 +446,21 @@ export function waveformN(partials, phases, type) {
} }
// expects one of waveforms as s // expects one of waveforms as s
export function getOscillator(s, t, value, onended) { export function getOscillator(s, t, value) {
const { duration, noise = 0 } = value; let { n: partials, duration, noise = 0 } = value;
const partials = value.partials ?? value.n;
let o; 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 no partials are given, use stock waveforms
if (!partials || partials?.length === 0 || s === 'sine') { if (!partials || s === 'sine') {
o = getAudioContext().createOscillator(); o = getAudioContext().createOscillator();
o.type = s || 'triangle'; o.type = s || 'triangle';
} }
// generate custom waveform if partials are given // generate custom waveform if partials are given
else { else {
o = waveformN(partials, value.phases, s); o = waveformN(partials, s);
} }
// set frequency // set frequency
o.frequency.value = getFrequencyFromValue(value); o.frequency.value = getFrequencyFromValue(value);
o.start(t);
let vibratoOscillator = getVibratoOscillator(o.detune, value, t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
@@ -497,13 +473,6 @@ export function getOscillator(s, t, value, onended) {
noiseMix = getNoiseMix(o, noise, t); noiseMix = getNoiseMix(o, noise, t);
} }
o.onended = () => {
o.disconnect();
noiseMix?.node.disconnect();
onended();
};
o.start(t);
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
stop: (time) => { stop: (time) => {
+2 -38
View File
@@ -3,8 +3,6 @@ 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 [];
@@ -34,9 +32,10 @@ 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)) / LN2 + 69; return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
}; };
export const valueToMidi = (value, fallbackValue) => { export const valueToMidi = (value, fallbackValue) => {
@@ -115,38 +114,3 @@ 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);
};
+16 -27
View File
@@ -3,18 +3,14 @@ import { getCommonSampleInfo } from './util.mjs';
import { import {
applyFM, applyFM,
applyParameterModulators, applyParameterModulators,
claimVoice, destroyAudioWorkletNode,
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';
@@ -222,15 +218,16 @@ 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;
let source = claimVoice(voiceKey); const source = getWorklet(
const params = { ac,
'wavetable-oscillator-processor',
{
begin: t, begin: t,
end: envEnd, end: envEnd,
frequency, frequency,
@@ -238,23 +235,15 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
position: value.wt, position: value.wt,
warp: value.warp, warp: value.warp,
warpMode: warpmode, warpMode: warpmode,
panspread: value.spread,
};
const processorOptions = {
phaseRand: value.wtphaserand ?? (value.unison > 1 ? 1 : 0),
voices: Math.max(value.unison ?? 1, 1), voices: Math.max(value.unison ?? 1, 1),
}; panspread: value.spread,
if (source == null) { phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
source = getWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2], processorOptions }); },
} else { { outputChannelCount: [2] },
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];
@@ -321,20 +310,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 = source.connect(gainNode(1)); const envGain = ac.createGain();
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); const node = source.connect(envGain);
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: envGain }; const handle = { node, source };
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,
+279 -335
View File
@@ -5,56 +5,52 @@
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 PI = Math.PI;
const TWO_PI = 2 * PI;
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 fast_tanh = (x) => { const frac = (x) => x - Math.floor(x);
const x2 = x * x; const ffloor = (x) => x | 0; // fast floor for non-negative
return (x * (27 + x2)) / (27 + 9 * x2);
};
// Optimized per-voice detuner which precomputes constants const getUnisonDetune = (unison, detune, voiceIndex) => {
const getDetuner = (unison, detune) => {
if (unison < 2) { if (unison < 2) {
return (_voiceIdx) => 0; return 0;
} }
const scale = detune / (unison - 1); return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
const center = detune * 0.5; };
return (voiceIdx) => voiceIdx * scale - center; const applySemitoneDetuneToFrequency = (frequency, detune) => {
return frequency * Math.pow(2, detune / 12);
}; };
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
const blockSize = 128;
// 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) {
dt = Math.min(dt, 1 - dt); dt = Math.min(dt, 1 - dt);
const invdt = 1 / dt;
// Start of cycle // Start of cycle
if (phase < dt) { if (phase < dt) {
phase *= invdt; phase /= dt;
return 2 * phase - phase ** 2 - 1; // 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
} }
// End of cycle // End of cycle
else if (phase > 1 - dt) { else if (phase > 1 - dt) {
phase = (phase - 1) * invdt; phase = (phase - 1) / dt;
return phase ** 2 + 2 * phase + 1; // 2 * (phase^2/2 + phase + 0.5)
return phase * phase + phase + phase + 1;
} }
// 0 otherwise // 0 otherwise
else { else {
return 0; return 0;
@@ -70,7 +66,7 @@ const waveshapes = {
return phase / skew; return phase / skew;
}, },
sine(phase) { sine(phase) {
return Math.sin(TWO_PI * phase) * 0.5 + 0.5; return Math.sin(Math.PI * 2 * phase) * 0.5 + 0.5;
}, },
ramp(phase) { ramp(phase) {
return phase; return phase;
@@ -104,6 +100,12 @@ const waveshapes = {
return v - polyBlep(phase, dt); return v - polyBlep(phase, dt);
}, },
}; };
function getParamValue(block, param) {
if (param.length > 1) {
return param[block];
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes); const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor { class LFOProcessor extends AudioWorkletProcessor {
@@ -163,15 +165,14 @@ class LFOProcessor extends AudioWorkletProcessor {
const blockSize = output[0].length ?? 0; const blockSize = output[0].length ?? 0;
if (this.phase == null) { if (this.phase == null) {
this.phase = ffrac(time * frequency + phaseoffset); this.phase = mod(time * frequency + phaseoffset, 1);
} }
const dt = frequency * INVSR; const dt = frequency / sampleRate;
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++) {
output[i][n] = out; let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = Math.pow(modval, curve);
output[i][n] = clamp(modval, min, max);
} }
this.incrementPhase(dt); this.incrementPhase(dt);
} }
@@ -292,8 +293,8 @@ class TwoPoleFilter {
// Out of bound values can produce NaNs // Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1); resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1); cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * PI * INVSR), 0, 1.14); const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
const r = Math.pow(0.5, 8 * resonance + 1); const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c; const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf this.s1 = mrc * this.s1 + c * this.s0; // lpf
@@ -352,6 +353,11 @@ class DJFProcessor extends AudioWorkletProcessor {
} }
registerProcessor('djf-processor', DJFProcessor); registerProcessor('djf-processor', DJFProcessor);
function fast_tanh(x) {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
}
const _PI = 3.14159265359;
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file //adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
class LadderProcessor extends AudioWorkletProcessor { class LadderProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
@@ -389,7 +395,7 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0]; let cutoff = parameters.frequency[0];
cutoff = cutoff * TWO_PI * INVSR; cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff; cutoff = cutoff > 1 ? 1 : cutoff;
const k = Math.min(8, resonance * 0.13); const k = Math.min(8, resonance * 0.13);
@@ -442,7 +448,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 = fastexpm1(pv(parameters.distort, n)); const shape = Math.expm1(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);
@@ -455,17 +461,9 @@ registerProcessor('distort-processor', DistortProcessor);
// SUPERSAW // SUPERSAW
class SuperSawOscillatorProcessor extends AudioWorkletProcessor { class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor(options) { constructor() {
super(); super();
this.phases = new Float32Array(MAX_VOICES); this.phase = [];
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 [
@@ -475,17 +473,20 @@ 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,
@@ -502,57 +503,56 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
defaultValue: 0, defaultValue: 0,
min: 0, min: 0,
}, },
{
name: 'voices',
defaultValue: 5,
min: 1,
},
]; ];
} }
process(_input, outputs, params) { process(_input, outputs, params) {
const started = currentTime >= params.begin[0]; if (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];
// fast k-rate paths
let freq, detune, freqspread, detuner;
if (params.freqspread.length === 1) {
freqspread = params.freqspread[0];
detuner = getDetuner(this.voices, freqspread);
}
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);
}
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i);
const voices = pv(params.voices, i);
const freqspread = pv(params.freqspread, i);
const panspread = pv(params.panspread, i) * 0.5 + 0.5; const panspread = pv(params.panspread, i) * 0.5 + 0.5;
gainL = Math.sqrt(1 - panspread); const gain1 = Math.sqrt(1 - panspread);
gainR = Math.sqrt(panspread); const gain2 = Math.sqrt(panspread);
// Main detuning (a-rate path) let freq = pv(params.frequency, i);
detune ??= pv(params.detune, i); // Main detuning
freqspread ??= pv(params.freqspread, i); freq = applySemitoneDetuneToFrequency(freq, detune / 100);
freq ??= applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); for (let n = 0; n < voices; n++) {
detuner ??= getDetuner(this.voices, freqspread); const isOdd = (n & 1) == 1;
for (let n = 0; n < this.voices; n++) { let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// Individual voice detuning // Individual voice detuning
const freqVoice = applyFastDetune(freq, detuner(n)); const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, 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 = mod(freqVoice / sampleRate, 1);
const v = waveshapes.sawblep(this.phases[n], dt); this.phase[n] = this.phase[n] ?? Math.random();
output[0][i] += v * gainL; const v = waveshapes.sawblep(this.phase[n], dt);
output[1][i] += v * gainR;
let pn = this.phases[n] + dt; output[0][i] = output[0][i] + v * gainL;
if (pn >= 1.0) pn -= 1.0; output[1][i] = output[1][i] + v * gainR;
this.phases[n] = pn;
// invert right and left gain this.phase[n] = wrapPhase(this.phase[n] + dt);
const tmp = gainL;
gainL = gainR;
gainR = tmp;
} }
} }
return true; return true;
@@ -564,16 +564,12 @@ registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file // Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048; const BUFFERED_BLOCK_SIZE = 2048;
const hannCache = new Map();
function genHannWindow(length) { function genHannWindow(length) {
if (!hannCache.has(length)) { let win = new Float32Array(length);
const win = new Float32Array(length); for (var i = 0; i < length; i++) {
for (let i = 0; i < length; i++) { win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length));
win[i] = 0.5 * (1 - Math.cos((TWO_PI * i) / length));
} }
hannCache.set(length, win); return win;
}
return hannCache.get(length);
} }
class PhaseVocoderProcessor extends OLAProcessor { class PhaseVocoderProcessor extends OLAProcessor {
@@ -591,10 +587,11 @@ class PhaseVocoderProcessor extends OLAProcessor {
blockSize: BUFFERED_BLOCK_SIZE, blockSize: BUFFERED_BLOCK_SIZE,
}; };
super(options); super(options);
this.timeCursor = 0;
this.fftSize = this.blockSize; this.fftSize = this.blockSize;
this.invfftSize = 1 / this.fftSize; this.timeCursor = 0;
this.hannWindow = genHannWindow(this.fftSize);
this.hannWindow = genHannWindow(this.blockSize);
// prepare FFT and pre-allocate buffers // prepare FFT and pre-allocate buffers
this.fft = new FFT(this.fftSize); this.fft = new FFT(this.fftSize);
this.freqComplexBuffer = this.fft.createComplexArray(); this.freqComplexBuffer = this.fft.createComplexArray();
@@ -607,43 +604,52 @@ class PhaseVocoderProcessor extends OLAProcessor {
processOLA(inputs, outputs, parameters) { processOLA(inputs, outputs, parameters) {
// no automation, take last value // no automation, take last value
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
if (pitchFactor < 0) { if (pitchFactor < 0) {
pitchFactor = pitchFactor * 0.25; pitchFactor = pitchFactor * 0.25;
} }
pitchFactor = Math.max(0, pitchFactor + 1); pitchFactor = Math.max(0, pitchFactor + 1);
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < inputs[i].length; j++) { for (var i = 0; i < this.nbInputs; i++) {
const input = inputs[i][j]; for (var j = 0; j < inputs[i].length; j++) {
const output = outputs[i][j]; // big assumption here: output is symetric to input
var input = inputs[i][j];
var output = outputs[i][j];
this.applyHannWindow(input); this.applyHannWindow(input);
this.fft.realTransform(this.freqComplexBuffer, input); this.fft.realTransform(this.freqComplexBuffer, input);
this.computeMagnitudes(); this.computeMagnitudes();
this.findPeaks(); this.findPeaks();
this.shiftPeaks(pitchFactor); this.shiftPeaks(pitchFactor);
this.fft.completeSpectrum(this.freqComplexBufferShifted); this.fft.completeSpectrum(this.freqComplexBufferShifted);
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
this.fft.fromComplexArray(this.timeComplexBuffer, output); this.fft.fromComplexArray(this.timeComplexBuffer, output);
this.applyHannWindow(output); this.applyHannWindow(output);
} }
} }
this.timeCursor += this.hopSize; this.timeCursor += this.hopSize;
} }
/** Apply Hann window in-place */ /** Apply Hann window in-place */
applyHannWindow(input) { applyHannWindow(input) {
for (let i = 0; i < this.blockSize; i++) { for (var i = 0; i < this.blockSize; i++) {
input[i] *= this.hannWindow[i] * 1.62; input[i] = input[i] * this.hannWindow[i] * 1.62;
} }
} }
/** Compute squared magnitudes for peak finding **/ /** Compute squared magnitudes for peak finding **/
computeMagnitudes() { computeMagnitudes() {
let i = 0, var i = 0,
j = 0; j = 0;
while (i < this.magnitudes.length) { while (i < this.magnitudes.length) {
const real = this.freqComplexBuffer[j]; let real = this.freqComplexBuffer[j];
const imag = this.freqComplexBuffer[j + 1]; let imag = this.freqComplexBuffer[j + 1];
// no need to sqrt for peak finding // no need to sqrt for peak finding
this.magnitudes[i] = real ** 2 + imag ** 2; this.magnitudes[i] = real ** 2 + imag ** 2;
i += 1; i += 1;
@@ -654,10 +660,12 @@ class PhaseVocoderProcessor extends OLAProcessor {
/** Find peaks in spectrum magnitudes **/ /** Find peaks in spectrum magnitudes **/
findPeaks() { findPeaks() {
this.nbPeaks = 0; this.nbPeaks = 0;
let i = 2; var i = 2;
const end = this.magnitudes.length - 2; let end = this.magnitudes.length - 2;
while (i < end) { while (i < end) {
const mag = this.magnitudes[i]; let mag = this.magnitudes[i];
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
i++; i++;
continue; continue;
@@ -666,6 +674,7 @@ class PhaseVocoderProcessor extends OLAProcessor {
i++; i++;
continue; continue;
} }
this.peakIndexes[this.nbPeaks] = i; this.peakIndexes[this.nbPeaks] = i;
this.nbPeaks++; this.nbPeaks++;
i += 2; i += 2;
@@ -676,44 +685,53 @@ class PhaseVocoderProcessor extends OLAProcessor {
shiftPeaks(pitchFactor) { shiftPeaks(pitchFactor) {
// zero-fill new spectrum // zero-fill new spectrum
this.freqComplexBufferShifted.fill(0); this.freqComplexBufferShifted.fill(0);
for (let i = 0; i < this.nbPeaks; i++) {
const peakIndex = this.peakIndexes[i]; for (var i = 0; i < this.nbPeaks; i++) {
const peakIndexShifted = fround(peakIndex * pitchFactor); let peakIndex = this.peakIndexes[i];
let peakIndexShifted = Math.round(peakIndex * pitchFactor);
if (peakIndexShifted > this.magnitudes.length) { if (peakIndexShifted > this.magnitudes.length) {
break; break;
} }
// find region of influence // find region of influence
let startIndex = 0; var startIndex = 0;
let endIndex = this.fftSize; var endIndex = this.fftSize;
if (i > 0) { if (i > 0) {
startIndex = peakIndex - fround((peakIndex - this.peakIndexes[i - 1]) / 2); let peakIndexBefore = this.peakIndexes[i - 1];
startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2);
} }
if (i < this.nbPeaks - 1) { if (i < this.nbPeaks - 1) {
endIndex = peakIndex + fceil((this.peakIndexes[i + 1] - peakIndex) / 2); let peakIndexAfter = this.peakIndexes[i + 1];
endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2);
} }
// shift whole region of influence around peak to shifted peak // shift whole region of influence around peak to shifted peak
const startOffset = startIndex - peakIndex; let startOffset = startIndex - peakIndex;
const endOffset = endIndex - peakIndex; let endOffset = endIndex - peakIndex;
const omegaDelta = TWO_PI * this.invfftSize * (peakIndexShifted - peakIndex); for (var j = startOffset; j < endOffset; j++) {
const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); let binIndex = peakIndex + j;
const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); let binIndexShifted = peakIndexShifted + j;
for (let j = startOffset; j < endOffset; j++) {
const binIndex = peakIndex + j;
const binIndexShifted = peakIndexShifted + j;
if (binIndexShifted >= this.magnitudes.length) { if (binIndexShifted >= this.magnitudes.length) {
break; break;
} }
// apply phase correction // apply phase correction
const indexReal = 2 * binIndex; let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize;
const indexImag = indexReal + 1; let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
const valueReal = this.freqComplexBuffer[indexReal]; let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
const valueImag = this.freqComplexBuffer[indexImag];
const valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; let indexReal = binIndex * 2;
const valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; let indexImag = indexReal + 1;
let valueReal = this.freqComplexBuffer[indexReal];
let valueImag = this.freqComplexBuffer[indexImag];
const indexShiftedReal = 2 * binIndexShifted; let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
const indexShiftedImag = indexShiftedReal + 1; let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
let indexShiftedReal = binIndexShifted * 2;
let indexShiftedImag = indexShiftedReal + 1;
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
} }
@@ -727,10 +745,11 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
class PulseOscillatorProcessor extends AudioWorkletProcessor { class PulseOscillatorProcessor extends AudioWorkletProcessor {
constructor() { constructor() {
super(); super();
this.phi = -PI; // phase this.pi = _PI;
this.phi = -this.pi; // phase
this.Y0 = 0; // feedback memories this.Y0 = 0; // feedback memories
this.Y1 = 0; this.Y1 = 0;
this.PW = PI; // pulse width this.PW = this.pi; // pulse width
this.B = 2.3; // feedback coefficient this.B = 2.3; // feedback coefficient
this.dphif = 0; // filtered phase increment this.dphif = 0; // filtered phase increment
this.envf = 0; // filtered envelope this.envf = 0; // filtered envelope
@@ -787,11 +806,11 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
dphi; dphi;
for (let i = 0; i < (output[0].length ?? 0); i++) { for (let i = 0; i < (output[0].length ?? 0); i++) {
const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI; const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi;
const detune = pv(params.detune, i); const detune = getParamValue(i, params.detune);
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
dphi = freq * TWO_PI * INVSR; // phase increment dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment
this.dphif += 0.1 * (dphi - this.dphif); this.dphif += 0.1 * (dphi - this.dphif);
env *= 0.9998; // exponential decay envelope env *= 0.9998; // exponential decay envelope
@@ -803,7 +822,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
// Waveform generation (half-Tomisawa oscillators) // Waveform generation (half-Tomisawa oscillators)
this.phi += this.dphif; // phase increment this.phi += this.dphif; // phase increment
if (this.phi >= PI) this.phi -= TWO_PI; // phase wrapping if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping
// First half-Tomisawa generator // First half-Tomisawa generator
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
@@ -833,23 +852,24 @@ const chyx = {
/*bit reverse*/ br: function (x, size = 8) { /*bit reverse*/ br: function (x, size = 8) {
if (size > 32) { if (size > 32) {
throw new Error('br() Size cannot be greater than 32'); throw new Error('br() Size cannot be greater than 32');
} } else {
let result = 0; let result = 0;
for (let idx = 0; idx < size; idx++) { for (let idx = 0; idx < size - 0; idx++) {
result |= chyx.bitC(x, 1 << idx, 1 << (size - (idx + 1))); result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1)));
} }
return result; return result;
}
}, },
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) { /*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
return Math.sin((x * PI) / 128); return Math.sin(x / (128 / Math.PI));
}, },
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) { /*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
return Math.cos((x * PI) / 128); return Math.cos(x / (128 / Math.PI));
}, },
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) { /*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
return Math.tan((x * PI) / 128); return Math.tan(x / (128 / Math.PI));
}, },
/*converts t into a string composed of its bits; regexes that*/ regG: function (t, X) { /*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) {
return X.test(t.toString(2)); return X.test(t.toString(2));
}, },
}; };
@@ -857,7 +877,7 @@ const chyx = {
// Create shortened Math functions // Create shortened Math functions
let mathParams, byteBeatHelperFuncs; let mathParams, byteBeatHelperFuncs;
function getByteBeatFunc(codetext) { function getByteBeatFunc(codetext) {
if (mathParams == null) { if ((mathParams || byteBeatHelperFuncs) == null) {
mathParams = Object.getOwnPropertyNames(Math); mathParams = Object.getOwnPropertyNames(Math);
byteBeatHelperFuncs = mathParams.map((k) => Math[k]); byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
const chyxNames = Object.getOwnPropertyNames(chyx); const chyxNames = Object.getOwnPropertyNames(chyx);
@@ -890,7 +910,7 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
this.func = getByteBeatFunc(codeText); this.func = getByteBeatFunc(codeText);
}; };
this.initialOffset = 0; this.initialOffset = null;
this.t = null; this.t = null;
this.func = null; this.func = null;
} }
@@ -937,19 +957,18 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
this.t = params.begin[0] * sampleRate; this.t = params.begin[0] * sampleRate;
} }
const output = outputs[0]; const output = outputs[0];
const scale = 256 * INVSR;
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i); const detune = getParamValue(i, params.detune);
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
const local_t = scale * freq * this.t + this.initialOffset; let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset;
const funcValue = this.func(local_t); const funcValue = this.func(local_t);
const signal = (funcValue & 255) / 127.5 - 1; let signal = (funcValue & 255) / 127.5 - 1;
//prevent speaker blowout via clipping if threshold exceeds const out = signal * 0.2;
const out = clamp(signal * 0.2, -0.4, 0.4);
for (let c = 0; c < output.length; c++) { for (let c = 0; c < output.length; c++) {
output[c][i] = out; //prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(out, -0.4, 0.4);
} }
this.t++; this.t = this.t + 1;
} }
return true; // keep the audio processing going return true; // keep the audio processing going
@@ -958,102 +977,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
registerProcessor('byte-beat-processor', ByteBeatProcessor); registerProcessor('byte-beat-processor', ByteBeatProcessor);
class EnvelopeProcessor extends 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 },
{ name: 'release', defaultValue: 0.1, minValue: 0 },
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
{ name: 'peak', defaultValue: 1 },
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
];
}
constructor() {
super();
this.val = 0;
this.segIdx = 0;
this.state = 0;
this.beginTime = 0;
this.endTime = 0;
this.attackStart = 0;
}
_warp(phase, curvature, strength = 8) {
if (phase === 0 || phase === 1) return phase; // fast exit
if (curvature > 0) {
// snappier
const exp = 1 + strength * curvature;
return 1 - Math.pow(1 - phase, exp);
} else {
// more calm
const exp = 1 - strength * curvature;
return Math.pow(phase, exp);
}
}
_advance(start, target, time, curvature) {
if (time === 0 || start === target) {
this.val = target;
} else {
// We compute our progress through this section of the envelope in time
// as a `phase` value, which is warped by the curvature, and then used
// to compute the value of the envelope at that time
const phase = Math.min(1, (currentTime - this.beginTime) / time);
const phaseWarped = this._warp(phase, curvature);
this.val = start + (target - start) * phaseWarped;
}
}
process(_inputs, outputs, params) {
const out = outputs[0][0];
if (!out) return true;
const begin = pv(params.begin, 0);
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
// triggered
this.beginTime = begin;
this.state = 1;
this.endTime = pv(params.end, 0);
this.attackStart = this.val;
}
const susTime = this.endTime - this.beginTime;
for (let i = 0; i < out.length; i++) {
const attack = pv(params.attack, i);
const decay = pv(params.decay, i);
const sustain = pv(params.sustain, i);
const release = pv(params.release, i);
const aCurve = pv(params.attackCurve, i);
const dCurve = pv(params.decayCurve, i);
const rCurve = pv(params.releaseCurve, i);
const peak = pv(params.peak, i);
const states = [
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
{ time: attack + decay, start: 1, target: sustain, curve: dCurve },
{ time: susTime, start: sustain, target: sustain },
{ time: susTime + release, start: sustain, target: 0, curve: rCurve },
];
let { time, start, target, curve } = states[this.state];
this._advance(start, target, time, curve);
while (currentTime - this.beginTime >= time) {
this.state = (this.state + 1) % states.length;
time = states[this.state].time;
}
out[i] = this.val * peak;
}
return true;
}
}
registerProcessor('envelope-processor', EnvelopeProcessor);
export const WarpMode = Object.freeze({ export const WarpMode = Object.freeze({
NONE: 0, NONE: 0,
ASYM: 1, ASYM: 1,
@@ -1132,8 +1055,10 @@ 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, automationRate: 'k-rate' }, { name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1 },
{ 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 },
]; ];
} }
@@ -1141,32 +1066,36 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
super(options); super(options);
this.frameLen = 0; this.frameLen = 0;
this.numFrames = 0; this.numFrames = 0;
this.phases = new Float32Array(MAX_VOICES); this.phase = [];
this.invSR = 1 / sampleRate;
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]) {
tablesCache[key] = payload.frames; const tables = [payload.frames];
let table = tables[0];
for (let level = 1; level < 1; level++) {
const nextLen = table.length >> 1;
const nextTable = table.map((frame) => {
const avg = new Float32Array(nextLen);
for (let i = 0; i < nextLen; i++) {
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
} }
this.table = tablesCache[key]; return avg;
this.numFrames = this.table.length; });
} else if (type === 'reset') { tables.push(nextTable);
this.reset(payload.processorOptions); table = nextTable;
if (nextLen <= 32) break;
}
tablesCache[key] = tables;
}
this.tables = tablesCache[key];
this.numFrames = this.tables[0].length;
} }
}; };
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) {
@@ -1175,7 +1104,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_toBits(amt, min = 2, max = 12) { _toBits(amt, min = 2, max = 12) {
const b = max + (min - max) * amt; const b = max + (min - max) * amt;
return { b, n: fround(Math.pow(2, b)) }; return { b, n: Math.round(Math.pow(2, b)) };
} }
_warpPhase(phase, amt, mode) { _warpPhase(phase, amt, mode) {
@@ -1201,7 +1130,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2); return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
} }
case WarpMode.SYNC: { case WarpMode.SYNC: {
const syncRatio = Math.pow(16, amt ** 2); const syncRatio = Math.pow(16, amt * amt);
return (phase * syncRatio) % 1; return (phase * syncRatio) % 1;
} }
case WarpMode.QUANT: { case WarpMode.QUANT: {
@@ -1210,8 +1139,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
case WarpMode.FOLD: { case WarpMode.FOLD: {
const K = 7; const K = 7;
const k = 1 + Math.max(1, fround(K * amt)); const k = 1 + Math.max(1, Math.round(K * amt));
return Math.abs(ffrac(k * phase) - 0.5) * 2; return Math.abs(frac(k * phase) - 0.5) * 2;
} }
case WarpMode.PWM: { case WarpMode.PWM: {
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1); const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
@@ -1221,12 +1150,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.ORBIT: { case WarpMode.ORBIT: {
const depth = 0.5 * amt; const depth = 0.5 * amt;
const n = 3; const n = 3;
return frac(phase + depth * Math.sin(TWO_PI * n * phase)); return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
} }
case WarpMode.SPIN: { case WarpMode.SPIN: {
const depth = 0.5 * amt; const depth = 0.5 * amt;
const { n } = this._toBits(amt, 1, 6); const { n } = this._toBits(amt, 1, 6);
return frac(phase + depth * Math.sin(TWO_PI * n * phase)); return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
} }
case WarpMode.CHAOS: { case WarpMode.CHAOS: {
const r = 3.7 + 0.3 * amt; const r = 3.7 + 0.3 * amt;
@@ -1237,7 +1166,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const isPrime = (n) => { const isPrime = (n) => {
if (n < 2) return false; if (n < 2) return false;
if (n % 2 === 0) return n === 2; if (n % 2 === 0) return n === 2;
for (let d = 3; d ** 2 <= n; d += 2) if (n % d === 0) return false; for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
return true; return true;
}; };
let { n } = this._toBits(amt, 3); let { n } = this._toBits(amt, 3);
@@ -1246,12 +1175,18 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
case WarpMode.BINARY: { case WarpMode.BINARY: {
let { b } = this._toBits(amt, 3); let { b } = this._toBits(amt, 3);
b = fround(b); b = Math.round(b);
const n = 1 << b; const n = 1 << b;
const idx = ffloor(phase * n); const idx = ffloor(phase * n);
const ridx = bitReverse(idx, b); const ridx = bitReverse(idx, b);
return ridx / n; return ridx / n;
} }
case WarpMode.MODULAR: {
const { n } = this._toBits(amt);
const depth = 0.5 * amt;
const jump = frac(phase * n) / n;
return frac(phase + depth * jump);
}
case WarpMode.BROWNIAN: { case WarpMode.BROWNIAN: {
const disp = 0.25 * amt * brownian(64 * phase, 4); const disp = 0.25 * amt * brownian(64 * phase, 4);
return frac(phase + disp); return frac(phase + disp);
@@ -1274,7 +1209,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.LOGISTIC: { case WarpMode.LOGISTIC: {
let x = phase; let x = phase;
const r = 3.6 + 0.4 * amt; const r = 3.6 + 0.4 * amt;
const iters = 1 + fround(2 * amt); const iters = 1 + Math.round(2 * amt);
for (let i = 0; i < iters; i++) x = r * x * (1 - x); for (let i = 0; i < iters; i++) x = r * x * (1 - x);
return clamp(x, 0, 1); return clamp(x, 0, 1);
} }
@@ -1287,7 +1222,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return (y - y0) / (y1 - y0); return (y - y0) / (y1 - y0);
} }
case WarpMode.FRACTAL: { case WarpMode.FRACTAL: {
const d = 0.5 * Math.sin(TWO_PI * phase) * amt; const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
return frac(phase + d); return frac(phase + d);
} }
case WarpMode.FLIP: { case WarpMode.FLIP: {
@@ -1311,63 +1246,72 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return a + (b - a) * frac; return a + (b - a) * frac;
} }
process(_inputs, outputs, params) { _chooseMip(dphi) {
const started = currentTime >= params.begin[0]; const approxHarm = clamp(dphi, 1e-6, 64);
const ended = currentTime >= params.end[0]; let level = 0;
if (!started || ended) { while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
level++;
}
return level;
}
process(_inputs, outputs, parameters) {
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= parameters.begin[0]) {
return true; 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];
// fast k-rate paths if (!this.tables) {
let freq, detune, freqspread, detuner; outL.fill(0);
if (params.freqspread.length === 1) { if (outR !== outL) outR.set(outL);
freqspread = params.freqspread[0]; return true;
detuner = getDetuner(this.voices, freqspread);
} }
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 tablePos = clamp(pv(params.position, i), 0, 1); const detune = pv(parameters.detune, i);
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 frac = idx - fIdx;
const warpAmount = clamp(pv(params.warp, i), 0, 1); const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
panspread ??= this.voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0; const warpMode = pv(parameters.warpMode, i);
gainL ??= Math.sqrt(0.5 - 0.5 * panspread); const voices = pv(parameters.voices, i);
gainR ??= Math.sqrt(0.5 + 0.5 * panspread); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
detune ??= pv(params.detune, i); const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
freqspread ??= pv(params.freqspread, i); const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
freq ??= applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); // overall detune const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
detuner ??= getDetuner(this.voices, freqspread); let f = pv(parameters.frequency, i);
for (let n = 0; n < this.voices; n++) { f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const freqVoice = applyFastDetune(freq, detuner(n)); // voice detune const normalizer = 1 / Math.sqrt(voices);
const dPhase = freqVoice * INVSR; for (let n = 0; n < voices; n++) {
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, getUnisonDetune(voices, freqspread, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
// warp phase then sample // warp phase then sample
const ph = this._warpPhase(this.phases[n], warpAmount, warpMode); this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const s0 = this._sampleFrame(this.table[fIdx], ph); const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph); const s0 = this._sampleFrame(table[fIdx], ph);
let s = lerp(s0, s1, interpT); const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
if (warpMode === WarpMode.FLIP && this.phases[n] < warpAmount) { let s = s0 + (s1 - s0) * frac;
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s; s = -s;
} }
outL[i] += s * gainL * this.normalizer; outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * this.normalizer; outR[i] += s * gainR * normalizer;
this.phases[n] = frac(this.phases[n] + dPhase); this.phase[n] = wrapPhase(this.phase[n] + dPhase);
// invert right and left gain
const tmp = gainL;
gainR = gainL;
gainL = tmp;
} }
} }
return true; return true;
+1 -5
View File
@@ -1,10 +1,6 @@
import { Dough } from './dough.mjs'; import { Dough } from './dough.mjs';
const clamp = (num, min, max) => { const clamp = (num, min, max) => Math.min(Math.max(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() {
+1 -5
View File
@@ -6,11 +6,7 @@ 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) => { const clamp = (num, min, max) => Math.min(Math.max(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);
+132 -20
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7 version: 2.2.7
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 3.0.4 specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)) version: 3.0.4(vitest@3.0.4)
'@vitest/ui': '@vitest/ui':
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4) version: 3.0.4(vitest@3.0.4)
@@ -85,29 +85,29 @@ importers:
examples/codemirror-repl: examples/codemirror-repl:
dependencies: dependencies:
'@strudel/codemirror': '@strudel/codemirror':
specifier: workspace:* specifier: 1.2.6
version: link:../../packages/codemirror version: 1.2.6(@codemirror/lint@6.8.4)
'@strudel/core': '@strudel/core':
specifier: workspace:* specifier: 1.2.5
version: link:../../packages/core version: 1.2.5
'@strudel/draw': '@strudel/draw':
specifier: workspace:* specifier: 1.2.5
version: link:../../packages/draw version: 1.2.5
'@strudel/mini': '@strudel/mini':
specifier: workspace:* specifier: 1.2.5
version: link:../../packages/mini version: 1.2.5
'@strudel/soundfonts': '@strudel/soundfonts':
specifier: workspace:* specifier: 1.2.6
version: link:../../packages/soundfonts version: 1.2.6
'@strudel/tonal': '@strudel/tonal':
specifier: workspace:* specifier: 1.2.5
version: link:../../packages/tonal version: 1.2.5
'@strudel/transpiler': '@strudel/transpiler':
specifier: workspace:* specifier: 1.2.5
version: link:../../packages/transpiler version: 1.2.5
'@strudel/webaudio': '@strudel/webaudio':
specifier: workspace:* specifier: 1.2.6
version: link:../../packages/webaudio version: 1.2.6
devDependencies: devDependencies:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
@@ -116,8 +116,8 @@ importers:
examples/headless-repl: examples/headless-repl:
dependencies: dependencies:
'@strudel/web': '@strudel/web':
specifier: workspace:* specifier: 1.2.6
version: link:../../packages/web version: 1.2.6
devDependencies: devDependencies:
vite: vite:
specifier: ^6.0.11 specifier: ^6.0.11
@@ -2544,6 +2544,33 @@ packages:
'@sinclair/typebox@0.27.8': '@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@strudel/codemirror@1.2.6':
resolution: {integrity: sha512-zjKbnbnOY40dlIAQu1poq28rcngYliLsuDwXXVnJsaXzCZSVuXEOMq91LqY4IGz/2QUOIDGm663ziz6Nb4IXeg==}
'@strudel/core@1.2.5':
resolution: {integrity: sha512-XO6ICbfuGfvaQ4sZmfIFsPDcjFez/+AVpNgJnmks5gtMMQNXpr0reuee61tvMiu2k5QYKzxWgzDl2FF9pJeIgA==}
'@strudel/draw@1.2.5':
resolution: {integrity: sha512-ZI4WPxUsVsxgDoeZMHo6VfFf+S1tsj8ubKBeSLcf6lH29JqPp7Nz6tPmbomOV1fzazZ7+xpkC7z772MdtgWy2g==}
'@strudel/mini@1.2.5':
resolution: {integrity: sha512-GmTWSBt49C3ogBy0hjM3bmf6K12PaJ1JJLsEvQu7j5J+lUd2n7GmLYR1UL6fntY4yVqT8ZwMhfUf6vIgUCTNbw==}
'@strudel/soundfonts@1.2.6':
resolution: {integrity: sha512-F7eBhohkCXCMo8WiufSAhD3JCfL5GTgCh9RlOJVT+1swiFeIss1vUAWJR3WjonB6moL8BiuNpcYOqO6i6cFy4A==}
'@strudel/tonal@1.2.5':
resolution: {integrity: sha512-mX3uW0nWH8Sa52HTLwnxlR5BrejFX77abeaiMbp+kQCMrX9I+XRdf8rEr7gdeInB6f/y2TzTVFpnsNsB5jLdoA==}
'@strudel/transpiler@1.2.5':
resolution: {integrity: sha512-mBZWfQBzknvnIYsjjzR8D67A+uIZ1iZ60U2WoXT21q5luXKz2c95RcTpqdx3oOGpLK9K00lDzcKjhQDJtj4OKw==}
'@strudel/web@1.2.6':
resolution: {integrity: sha512-e6VltuX90zZz8JVe1FtN23DCaXA9p8zGDakp5OuUb5kZ5hC6cySFkkOsHdGC6dJ0EAsZSCKXMihRxgk9GhBwKA==}
'@strudel/webaudio@1.2.6':
resolution: {integrity: sha512-4LnyNvyeAXYNmXQGGw2f0chIkDr9Rbwo+SK6Y0p8C3W8juDiOfKq2ihSVph95yaBBPRYFJ5Kttj1kLknRHYsog==}
'@supabase/auth-js@2.67.3': '@supabase/auth-js@2.67.3':
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==} resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
@@ -7094,6 +7121,9 @@ packages:
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
hasBin: true hasBin: true
superdough@1.2.6:
resolution: {integrity: sha512-zHEFfG+N4zhxJ9lUDf8p4Oj4sdGGQ5SrOPksdc0cKv5Yn0hzlNlMehEa5TkeIfDCiR27Y4H81ks7S8/CGSByhQ==}
supports-color@7.2.0: supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -7102,6 +7132,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
supradough@1.2.4:
resolution: {integrity: sha512-/v3cL9fq05QCQVwbttLHNZcjyyFFz5wdKvg3j6V6moRXpDlQG89l8dQtUIpQvHsxYI5sXNod9rh5bf+Y4NMGeA==}
tabbable@6.2.0: tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
@@ -10019,6 +10052,79 @@ snapshots:
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
'@strudel/codemirror@1.2.6(@codemirror/lint@6.8.4)':
dependencies:
'@codemirror/autocomplete': 6.18.4
'@codemirror/commands': 6.8.0
'@codemirror/lang-javascript': 6.2.2
'@codemirror/language': 6.10.8
'@codemirror/search': 6.5.8
'@codemirror/state': 6.5.1
'@codemirror/view': 6.36.2
'@lezer/highlight': 1.2.1
'@nanostores/persistent': 0.10.2(nanostores@0.11.3)
'@replit/codemirror-emacs': 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vim': 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vscode-keymap': 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@tonaljs/tonal': 4.10.0
nanostores: 0.11.3
superdough: 1.2.6
transitivePeerDependencies:
- '@codemirror/lint'
'@strudel/core@1.2.5':
dependencies:
fraction.js: 5.2.1
'@strudel/draw@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/soundfonts@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/webaudio': 1.2.6
sfumato: 0.1.2
soundfont2: 0.5.0
'@strudel/tonal@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@tonaljs/tonal': 4.10.0
chord-voicings: 0.0.1
webmidi: 3.1.12
'@strudel/transpiler@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 3.0.3
'@strudel/web@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@strudel/webaudio': 1.2.6
'@strudel/webaudio@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
superdough: 1.2.6
supradough: 1.2.4
'@supabase/auth-js@2.67.3': '@supabase/auth-js@2.67.3':
dependencies: dependencies:
'@supabase/node-fetch': 2.6.15 '@supabase/node-fetch': 2.6.15
@@ -10554,7 +10660,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))': '@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
@@ -15665,12 +15771,18 @@ snapshots:
pirates: 4.0.6 pirates: 4.0.6
ts-interface-checker: 0.1.13 ts-interface-checker: 0.1.13
superdough@1.2.6:
dependencies:
nanostores: 0.11.3
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
supradough@1.2.4: {}
tabbable@6.2.0: {} tabbable@6.2.0: {}
tailwindcss@3.4.17: tailwindcss@3.4.17:
+48 -292
View File
@@ -7219,112 +7219,6 @@ exports[`runs examples > example "panchor" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "partials" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 1/16 → 1/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 1/8 → 3/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 3/16 → 1/4 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 1/4 → 5/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 5/16 → 3/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 3/8 → 7/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 7/16 → 1/2 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 1/2 → 9/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 9/16 → 5/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 5/8 → 11/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 11/16 → 3/4 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 3/4 → 13/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 13/16 → 7/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 7/8 → 15/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 15/16 → 1/1 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 1/1 → 17/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 17/16 → 9/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 9/8 → 19/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 19/16 → 5/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 5/4 → 21/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 21/16 → 11/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 11/8 → 23/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 23/16 → 3/2 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 3/2 → 25/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 25/16 → 13/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 13/8 → 27/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 27/16 → 7/4 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 7/4 → 29/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 29/16 → 15/8 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 15/8 → 31/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 31/16 → 2/1 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 2/1 → 33/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 33/16 → 17/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 17/8 → 35/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 35/16 → 9/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 9/4 → 37/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 37/16 → 19/8 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 19/8 → 39/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 39/16 → 5/2 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 5/2 → 41/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 41/16 → 21/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 21/8 → 43/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 43/16 → 11/4 | s:user note:E4 partials:[1 0 1 0 0 1] ]",
"[ 11/4 → 45/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 45/16 → 23/8 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 23/8 → 47/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 47/16 → 3/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 3/1 → 49/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]",
"[ 49/16 → 25/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 25/8 → 51/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 51/16 → 13/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 13/4 → 53/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 53/16 → 27/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
"[ 27/8 → 55/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 55/16 → 7/2 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 7/2 → 57/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]",
"[ 57/16 → 29/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 29/8 → 59/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 59/16 → 15/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]",
"[ 15/4 → 61/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]",
"[ 61/16 → 31/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]",
"[ 31/8 → 63/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]",
"[ 63/16 → 4/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]",
]
`;
exports[`runs examples > example "partials" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:saw note:G#3 partials:[1] ]",
"[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 1 1 0 0 0 0] ]",
"[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 1 1 1 1 1] ]",
"[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 1 0 0 1 1 1] ]",
"[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 1 1] ]",
"[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 1 1] ]",
"[ 3/4 → 7/8 | s:saw note:B3 partials:[1 1 0 0 1 1] ]",
"[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 1 0 0 1 1 0] ]",
"[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 1 1 0] ]",
"[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 1 0 1 1 0 1] ]",
"[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 1 1 1 0 1 1] ]",
"[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 1 1 1 1] ]",
"[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 1 1 1 0 0] ]",
"[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0] ]",
"[ 7/4 → 15/8 | s:saw note:B3 partials:[1 1 0 1 0 1] ]",
"[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 1 1 0 0 1] ]",
"[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 1 1 1 0 1 1 0] ]",
"[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 1 1 0 1 0 0 0] ]",
"[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 1 1 0 0 1] ]",
"[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 1 0 1 0 1 1 1] ]",
"[ 5/2 → 21/8 | s:saw note:E4 partials:[1 1 1 0 1 1 0] ]",
"[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 1 0 1 0] ]",
"[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 1 0 0 0 1 1] ]",
"[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 1] ]",
"[ 3/1 → 25/8 | s:saw note:B3 partials:[1 1 1 0 0 0] ]",
"[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 1 1 0 0 0] ]",
"[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 1 1 1 1 1 1 1] ]",
"[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]",
"[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]",
"[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 1 1 1 0 0 0 1] ]",
"[ 15/4 → 31/8 | s:saw note:B4 partials:[1 1 0 1 0 0 0 0] ]",
"[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 1 1 1 1 0 1] ]",
]
`;
exports[`runs examples > example "pattack" example index 0 1`] = ` exports[`runs examples > example "pattack" example index 0 1`] = `
[ [
"[ 0/1 → 1/2 | note:c pattack:0 ]", "[ 0/1 → 1/2 | note:c pattack:0 ]",
@@ -7578,75 +7472,6 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "phases" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} ]",
"[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} ]",
"[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} ]",
"[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} ]",
"[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} ]",
"[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} ]",
"[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} ]",
"[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} ]",
"[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} ]",
"[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} ]",
"[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} ]",
"[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} ]",
"[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} ]",
"[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} ]",
"[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} ]",
"[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} ]",
"[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} ]",
"[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} ]",
"[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} ]",
"[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} ]",
"[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} ]",
"[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} ]",
"[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} ]",
"[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} ]",
"[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} ]",
"[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} ]",
"[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} ]",
"[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} ]",
"[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} ]",
"[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} ]",
"[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} ]",
"[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
"[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} ]",
"[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]",
]
`;
exports[`runs examples > example "pianoroll" example index 0 1`] = ` exports[`runs examples > example "pianoroll" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
@@ -8368,75 +8193,6 @@ exports[`runs examples > example "rand" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "randL" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:saw note:F1 partials:[0 0 0 0 0 0 0 0] ]",
"[ 1/16 → 1/8 | s:saw note:Bb2 partials:[0.8426077850162983 0.0886186733841896 0.9342893119901419 0.06848422810435295 0.6248505394905806 0.5372848063707352 0.8283301629126072 0.7969411127269268] ]",
"[ 1/8 → 3/16 | s:saw note:G2 partials:[0.6852155700325966 0.17723728902637959 0.8521428182721138 0.5022399611771107 0.9780306946486235 0.3186125475913286 0.1851645242422819 0.8495976086705923] ]",
"[ 3/16 → 1/4 | s:saw note:Ab1 partials:[0.20066574029624462 0.17195586115121841 0.2159541044384241 0.17005567252635956 0.6841662060469389 0.07906394638121128 0.004815371707081795 0.027107616886496544] ]",
"[ 1/4 → 5/16 | s:saw note:C2 partials:[0.36975969187915325 0.18273563869297504 0.012781793251633644 0.5423613861203194 0.12467948533594608 0.7188410349190235 0.856887087225914 0.8818203993141651] ]",
"[ 5/16 → 3/8 | s:saw note:Eb2 partials:[0.5675661638379097 0.15932588279247284 0.31376867927610874 0.32359412126243114 0.3281281068921089 0.011714376509189606 0.4433113746345043 0.68459103256464] ]",
"[ 3/8 → 7/16 | s:saw note:C2 partials:[0.40139251574873924 0.15799915604293346 0.9604704882949591 0.03873417526483536 0.4157217647880316 0.27756719291210175 0.8800551909953356 0.5692523363977671] ]",
"[ 7/16 → 1/2 | s:saw note:Bb1 partials:[0.3015887886285782 0.22311350144445896 0.47645803540945053 0.001545613631606102 0.53841289319098 0.330710094422102 0.2747743520885706 0.9471044968813658] ]",
"[ 1/2 → 9/16 | s:saw note:Bb1 partials:[0.2604806162416935 0.3654713351279497 0.9900747090578079 0.22572625242173672 0.9328999016433954 0.5273839123547077 0.7047769222408533 0.7311522178351879] ]",
"[ 9/16 → 5/8 | s:saw note:G1 partials:[0.16399178467690945 0.9559339117258787 0.21830322034657001 0.4903192054480314 0.2504696864634752 0.7302105724811554 0.6580934692174196 0.9855979979038239] ]",
"[ 5/8 → 11/16 | s:saw note:G1 partials:[0.1356358677148819 0.5403102282434702 0.07321739941835403 0.9646544624119997 0.7707359045743942 0.46888123638927937 0.9489036612212658 0.366748945787549] ]",
"[ 11/16 → 3/4 | s:saw note:C2 partials:[0.408811716362834 0.9613851886242628 0.7683626655489206 0.5862949229776859 0.9568102769553661 0.7154356613755226 0.6283384170383215 0.2193678840994835] ]",
"[ 3/4 → 13/16 | s:saw note:Ab1 partials:[0.19582648016512394 0.0024610888212919235 0.08272589556872845 0.09592138417065144 0.9678201265633106 0.3249557167291641 0.9463537875562906 0.20766610652208328] ]",
"[ 13/16 → 7/8 | s:saw note:Ab2 partials:[0.7516226731240749 0.15610851533710957 0.9039078876376152 0.8602268267422915 0.928601048886776 0.4479671400040388 0.5080656576901674 0.562442347407341] ]",
"[ 7/8 → 15/16 | s:saw note:C2 partials:[0.3976310808211565 0.13476407527923584 0.963376859202981 0.6579397786408663 0.5203975513577461 0.865439185872674 0.5583905670791864 0.2736878804862499] ]",
"[ 15/16 → 1/1 | s:saw note:G2 partials:[0.7029578909277916 0.6351367030292749 0.26143294386565685 0.6411824338138103 0.5676082745194435 0.4803342465311289 0.5140306428074837 0.9477859679609537] ]",
"[ 1/1 → 17/16 | s:saw note:Eb2 partials:[0.5195421651005745 0.9349692352116108 0.1718774326145649 0.9601866770535707 0.2802200373262167 0.8732441551983356 0.01810968853533268 0.42737805284559727] ]",
"[ 17/16 → 9/8 | s:saw note:Eb2 partials:[0.5486328881233931 0.7088070474565029 0.07877279818058014 0.9422610364854336 0.7696988433599472 0.8289324026554823 0.5925844628363848 0.3920764606446028] ]",
"[ 9/8 → 19/16 | s:saw note:G2 partials:[0.6724895145744085 0.21410975232720375 0.5229047331959009 0.20957534946501255 0.7402758821845055 0.2681974694132805 0.4059371296316385 0.9422509074211121] ]",
"[ 19/16 → 5/4 | s:saw note:F1 partials:[0.08174665085971355 0.2762963864952326 0.5428560189902782 0.29452037811279297 0.17946280725300312 0.366960596293211 0.26327736489474773 0.5002023074775934] ]",
"[ 5/4 → 21/16 | s:saw note:G2 partials:[0.7287282031029463 0.9341024849563837 0.5098182689398527 0.5608645845204592 0.6812942810356617 0.5015129633247852 0.4602120481431484 0.927369873970747] ]",
"[ 21/16 → 11/8 | s:saw note:F1 partials:[0.08104278706014156 0.5200720727443695 0.5363391060382128 0.9824271816760302 0.10652091167867184 0.23473932035267353 0.7656102329492569 0.4850195776671171] ]",
"[ 11/8 → 23/16 | s:saw note:Ab1 partials:[0.18280375190079212 0.40527783520519733 0.30504362285137177 0.7622128054499626 0.842598931863904 0.053796686232089996 0.5124214049428701 0.9178454764187336] ]",
"[ 23/16 → 3/2 | s:saw note:Eb2 partials:[0.5081270858645439 0.1421387754380703 0.8852288406342268 0.9066353775560856 0.7700740322470665 0.35437702015042305 0.20032598450779915 0.963155921548605] ]",
"[ 3/2 → 25/16 | s:saw note:F2 partials:[0.6084080748260021 0.4424846563488245 0.755185678601265 0.04328125901520252 0.04244415462017059 0.3994515985250473 0.787989066913724 0.3180440980941057] ]",
"[ 25/16 → 13/8 | s:saw note:Ab1 partials:[0.17092766426503658 0.7808954659849405 0.8234915658831596 0.8643151633441448 0.9781719036400318 0.5102015696465969 0.018259897828102112 0.25844234600663185] ]",
"[ 13/8 → 27/16 | s:saw note:Db2 partials:[0.49675471149384975 0.2965749856084585 0.09848833456635475 0.19172007404267788 0.22677889838814735 0.9841996673494577 0.11156083643436432 0.6079028844833374] ]",
"[ 27/16 → 7/4 | s:saw note:Bb1 partials:[0.29589061066508293 0.13011129200458527 0.8021425940096378 0.38040103763341904 0.33274981752038 0.29745868407189846 0.9898250997066498 0.743482418358326] ]",
"[ 7/4 → 29/16 | s:saw note:Ab1 partials:[0.20473789609968662 0.7480021081864834 0.30757393687963486 0.5827204789966345 0.22094514779746532 0.6370698790997267 0.16573002003133297 0.0638319905847311] ]",
"[ 29/16 → 15/8 | s:saw note:Db2 partials:[0.4695742893964052 0.6690364442765713 0.6216684486716986 0.41342394426465034 0.9508822970092297 0.8403679896146059 0.91759910620749 0.6591395419090986] ]",
"[ 15/8 → 31/16 | s:saw note:F2 partials:[0.5945571791380644 0.3962489552795887 0.563431927934289 0.4742323160171509 0.4185752831399441 0.9703940469771624 0.5282467231154442 0.6896266285330057] ]",
"[ 31/16 → 2/1 | s:saw note:Bb2 partials:[0.8672592639923096 0.3438429981470108 0.9795673936605453 0.46547594852745533 0.9770395755767822 0.36851615831255913 0.7725539114326239 0.876962523907423] ]",
"[ 2/1 → 33/16 | s:saw note:C3 partials:[0.9595271199941635 0.5427457969635725 0.45864437520504 0.8057199101895094 0.18388565629720688 0.17221237532794476 0.8838487900793552 0.4976818822324276] ]",
"[ 33/16 → 17/8 | s:saw note:Bb1 partials:[0.3324784208089113 0.6005446836352348 0.1564352661371231 0.765217661857605 0.12899602390825748 0.20880493707954884 0.9156261626631021 0.5550615340471268] ]",
"[ 17/8 → 35/16 | s:saw note:Bb2 partials:[0.9033902939409018 0.22919894196093082 0.8388008493930101 0.9108077362179756 0.32246968522667885 0.16122018359601498 0.31932324543595314 0.18524732999503613] ]",
"[ 35/16 → 9/4 | s:saw note:F1 partials:[0.037000780925154686 0.30360451713204384 0.7516817897558212 0.17065968923270702 0.24669718369841576 0.978821286931634 0.9620356522500515 0.12417634017765522] ]",
"[ 9/4 → 37/16 | s:saw note:C2 partials:[0.34548251144587994 0.30419893004000187 0.10205957666039467 0.9906709585338831 0.3156223688274622 0.3745057098567486 0.7668170686811209 0.9379972033202648] ]",
"[ 37/16 → 19/8 | s:saw note:F2 partials:[0.6649543270468712 0.41613837145268917 0.9078915324062109 0.2232209537178278 0.2918607946485281 0.4931973237544298 0.7035325299948454 0.29280414804816246] ]",
"[ 19/8 → 39/16 | s:saw note:Bb2 partials:[0.8365066405385733 0.5369812995195389 0.2263860274106264 0.21759207546710968 0.2172116208821535 0.09454222768545151 0.8075542915612459 0.9017798062413931] ]",
"[ 39/16 → 5/2 | s:saw note:G1 partials:[0.14231421053409576 0.6801821701228619 0.7958894111216068 0.9494249671697617 0.179213372990489 0.22531690262258053 0.7458387855440378 0.3595987129956484] ]",
"[ 5/2 → 41/16 | s:saw note:Db2 partials:[0.45861607417464256 0.6807645913213491 0.07009582966566086 0.1450389064848423 0.2579921595752239 0.612881500273943 0.5503941811621189 0.7027011960744858] ]",
"[ 41/16 → 21/8 | s:saw note:F2 partials:[0.6068673655390739 0.8489279896020889 0.027898555621504784 0.90520747192204 0.18605150654911995 0.30939464271068573 0.16843407973647118 0.5127317048609257] ]",
"[ 21/8 → 43/16 | s:saw note:G1 partials:[0.16019348427653313 0.47758268006145954 0.9749126061797142 0.8025561925023794 0.3600110989063978 0.324309878051281 0.5655391272157431 0.13364790193736553] ]",
"[ 43/16 → 11/4 | s:saw note:Eb2 partials:[0.538035349920392 0.4103843830525875 0.6443832442164421 0.7532152868807316 0.32128027081489563 0.29064710810780525 0.5972099266946316 0.8252892810851336] ]",
"[ 11/4 → 45/16 | s:saw note:F2 partials:[0.6332327704876661 0.7342763151973486 0.16935866326093674 0.4754706546664238 0.528122790157795 0.21934260986745358 0.7476693484932184 0.2995396424084902] ]",
"[ 45/16 → 23/8 | s:saw note:Bb2 partials:[0.8912940509617329 0.5256196465343237 0.25639201514422894 0.46860250271856785 0.08611978217959404 0.6175916157662868 0.990638293325901 0.11189854890108109] ]",
"[ 23/8 → 47/16 | s:saw note:F1 partials:[0.01625417172908783 0.28409438766539097 0.3075305689126253 0.40343056432902813 0.5556836389005184 0.6926821582019329 0.5635769125074148 0.017873913049697876] ]",
"[ 47/16 → 3/1 | s:saw note:Bb1 partials:[0.2830589488148689 0.3613663297146559 0.6216250211000443 0.6083405166864395 0.27202711440622807 0.771099541336298 0.3156145606189966 0.736228458583355] ]",
"[ 3/1 → 49/16 | s:saw note:Ab1 partials:[0.21728911064565182 0.7605195846408606 0.11240843310952187 0.4372697602957487 0.15630115941166878 0.1653979979455471 0.5303416550159454 0.07218753732740879] ]",
"[ 49/16 → 25/8 | s:saw note:F1 partials:[0.012518879026174545 0.6565517522394657 0.783835593611002 0.2481316588819027 0.27786846831440926 0.6153149046003819 0.39914070069789886 0.2175555843859911] ]",
"[ 25/8 → 51/16 | s:saw note:C2 partials:[0.34324387833476067 0.14098095521330833 0.012934491038322449 0.827436288818717 0.5283997394144535 0.8942463714629412 0.4924008697271347 0.9298017006367445] ]",
"[ 51/16 → 13/4 | s:saw note:Bb2 partials:[0.9057559575885534 0.5731389932334423 0.06561482697725296 0.02156665176153183 0.4685337021946907 0.000788738951086998 0.6995994579046965 0.9893404543399811] ]",
"[ 13/4 → 53/16 | s:saw note:C3 partials:[0.9923650715500116 0.7797339502722025 0.1992435697466135 0.9005183521658182 0.718992218375206 0.23745290748775005 0.28872333094477654 0.35448253713548183] ]",
"[ 53/16 → 27/8 | s:saw note:Bb1 partials:[0.3235324025154114 0.5165992900729179 0.6231792941689491 0.8034896682947874 0.6663950439542532 0.4207208137959242 0.9748435858637094 0.27676148526370525] ]",
"[ 27/8 → 55/16 | s:saw note:C2 partials:[0.40869180113077164 0.4018078800290823 0.34696186147630215 0.9572948440909386 0.400035472586751 0.3531211093068123 0.7240961641073227 0.8125612027943134] ]",
"[ 55/16 → 7/2 | s:saw note:Ab2 partials:[0.775677714496851 0.0502743236720562 0.3246541116386652 0.503994770348072 0.8252716399729252 0.89872563816607 0.060319963842630386 0.25497629307210445] ]",
"[ 7/2 → 57/16 | s:saw note:C2 partials:[0.40994881466031075 0.6455810181796551 0.35704658553004265 0.2732649203389883 0.31587288342416286 0.23243548162281513 0.22569947130978107 0.17241661623120308] ]",
"[ 57/16 → 29/8 | s:saw note:Ab2 partials:[0.8133465722203255 0.8327706772834063 0.32914630882442 0.23131784796714783 0.7526696771383286 0.1361286472529173 0.11441296897828579 0.36366880126297474] ]",
"[ 29/8 → 59/16 | s:saw note:C3 partials:[0.9391485787928104 0.3380728308111429 0.7723016366362572 0.998674126341939 0.7193406894803047 0.6963680125772953 0.9260678570717573 0.7829763628542423] ]",
"[ 59/16 → 15/4 | s:saw note:C3 partials:[0.9731374885886908 0.0494034755975008 0.990720022469759 0.9819309785962105 0.22877203114330769 0.7265191469341516 0.4922411348670721 0.7549834884703159] ]",
"[ 15/4 → 61/16 | s:saw note:Ab2 partials:[0.8121673222631216 0.25548945367336273 0.5759696904569864 0.6668333616107702 0.9233786761760712 0.11143362522125244 0.9734930321574211 0.369325939565897] ]",
"[ 61/16 → 31/8 | s:saw note:G1 partials:[0.1008925698697567 0.7558664139360189 0.1523460578173399 0.6523381881415844 0.9690635204315186 0.7343240566551685 0.9544064309448004 0.9150135722011328] ]",
"[ 31/8 → 63/16 | s:saw note:G2 partials:[0.7361665386706591 0.7657648548483849 0.21474426984786987 0.8228576295077801 0.5647660344839096 0.08148551359772682 0.9714624118059874 0.13992334716022015] ]",
"[ 63/16 → 4/1 | s:saw note:Bb1 partials:[0.3056422360241413 0.8881660811603069 0.7683485243469477 0.08641545102000237 0.5427133180201054 0.20803124643862247 0.5347504671663046 0.5977730695158243] ]",
]
`;
exports[`runs examples > example "range" example index 0 1`] = ` exports[`runs examples > example "range" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:hh cutoff:2250 ]", "[ 0/1 → 1/8 | s:hh cutoff:2250 ]",
@@ -12484,54 +12240,54 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
[ [
"[ 0/1 → 1/12 | note:c ]", "[ 0/1 → 1/12 | s:bd ]",
"[ 1/12 → 1/6 | note:c ]", "[ 1/12 → 1/6 | s:bd ]",
"[ 1/6 → 1/4 | note:c ]", "[ 1/6 → 1/4 | s:bd ]",
"[ 1/4 → 1/3 | note:c ]", "[ 1/4 → 1/3 | s:bd ]",
"[ 1/3 → 5/12 | note:c ]", "[ 1/3 → 5/12 | s:bd ]",
"[ 5/12 → 1/2 | note:c ]", "[ 5/12 → 1/2 | s:bd ]",
"[ 1/2 → 7/12 | note:f ]", "[ 1/2 → 7/12 | s:sd ]",
"[ 7/12 → 2/3 | note:f ]", "[ 7/12 → 2/3 | s:sd ]",
"[ 2/3 → 3/4 | note:f ]", "[ 2/3 → 3/4 | s:sd ]",
"[ 3/4 → 5/6 | note:c ]", "[ 3/4 → 5/6 | s:bd ]",
"[ 5/6 → 11/12 | note:c ]", "[ 5/6 → 11/12 | s:bd ]",
"[ 11/12 → 1/1 | note:c ]", "[ 11/12 → 1/1 | s:bd ]",
"[ 1/1 → 13/12 | note:c ]", "[ 1/1 → 13/12 | s:bd ]",
"[ 13/12 → 7/6 | note:c ]", "[ 13/12 → 7/6 | s:bd ]",
"[ 7/6 → 5/4 | note:c ]", "[ 7/6 → 5/4 | s:bd ]",
"[ 5/4 → 4/3 | note:c ]", "[ 5/4 → 4/3 | s:bd ]",
"[ 4/3 → 17/12 | note:c ]", "[ 4/3 → 17/12 | s:bd ]",
"[ 17/12 → 3/2 | note:c ]", "[ 17/12 → 3/2 | s:bd ]",
"[ 3/2 → 19/12 | note:a ]", "[ 3/2 → 19/12 | s:hh ]",
"[ 19/12 → 5/3 | note:a ]", "[ 19/12 → 5/3 | s:hh ]",
"[ 5/3 → 7/4 | note:a ]", "[ 5/3 → 7/4 | s:hh ]",
"[ 7/4 → 11/6 | note:c ]", "[ 7/4 → 11/6 | s:bd ]",
"[ 11/6 → 23/12 | note:c ]", "[ 11/6 → 23/12 | s:bd ]",
"[ 23/12 → 2/1 | note:c ]", "[ 23/12 → 2/1 | s:bd ]",
"[ 2/1 → 25/12 | note:a ]", "[ 2/1 → 25/12 | s:hh ]",
"[ 25/12 → 13/6 | note:a ]", "[ 25/12 → 13/6 | s:hh ]",
"[ 13/6 → 9/4 | note:a ]", "[ 13/6 → 9/4 | s:hh ]",
"[ 9/4 → 7/3 | note:a ]", "[ 9/4 → 7/3 | s:hh ]",
"[ 7/3 → 29/12 | note:a ]", "[ 7/3 → 29/12 | s:hh ]",
"[ 29/12 → 5/2 | note:a ]", "[ 29/12 → 5/2 | s:hh ]",
"[ 5/2 → 31/12 | note:c ]", "[ 5/2 → 31/12 | s:bd ]",
"[ 31/12 → 8/3 | note:c ]", "[ 31/12 → 8/3 | s:bd ]",
"[ 8/3 → 11/4 | note:c ]", "[ 8/3 → 11/4 | s:bd ]",
"[ 11/4 → 17/6 | note:c ]", "[ 11/4 → 17/6 | s:bd ]",
"[ 17/6 → 35/12 | note:c ]", "[ 17/6 → 35/12 | s:bd ]",
"[ 35/12 → 3/1 | note:c ]", "[ 35/12 → 3/1 | s:bd ]",
"[ 3/1 → 37/12 | note:c ]", "[ 3/1 → 37/12 | s:bd ]",
"[ 37/12 → 19/6 | note:c ]", "[ 37/12 → 19/6 | s:bd ]",
"[ 19/6 → 13/4 | note:c ]", "[ 19/6 → 13/4 | s:bd ]",
"[ 13/4 → 10/3 | note:f ]", "[ 13/4 → 10/3 | s:sd ]",
"[ 10/3 → 41/12 | note:f ]", "[ 10/3 → 41/12 | s:sd ]",
"[ 41/12 → 7/2 | note:f ]", "[ 41/12 → 7/2 | s:sd ]",
"[ 7/2 → 43/12 | note:a ]", "[ 7/2 → 43/12 | s:hh ]",
"[ 43/12 → 11/3 | note:a ]", "[ 43/12 → 11/3 | s:hh ]",
"[ 11/3 → 15/4 | note:a ]", "[ 11/3 → 15/4 | s:hh ]",
"[ 15/4 → 23/6 | note:c ]", "[ 15/4 → 23/6 | s:bd ]",
"[ 23/6 → 47/12 | note:c ]", "[ 23/6 → 47/12 | s:bd ]",
"[ 47/12 → 4/1 | note:c ]", "[ 47/12 → 4/1 | s:bd ]",
] ]
`; `;
+7 -73
View File
@@ -48,94 +48,28 @@ You can also use the `crackle` type to play some subtle noise crackles. You can
### Additive Synthesis ### Additive Synthesis
Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality. To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions.
#### Partials
`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness:
<MiniRepl <MiniRepl
client:idle client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth") .sound("sawtooth")
.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"]) .n("<32 16 8 4>")
._scope()`} ._scope()`}
/> />
`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source: When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting.
You can also set `n` directly in mini notation with `sound`:
<MiniRepl <MiniRepl
client:idle client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("user") .sound("sawtooth:<32 16 8 4>")
.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3])
._scope()`} ._scope()`}
/> />
We may algorithmically construct lists of magnitudes with Javascript code like: Note for tidal users: `n` in tidal is synonymous to `note` for synths only.
In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms.
<MiniRepl
client:idle
tune={`const numHarmonics = 22;
note("c2 <eb2 <g2 g1>>".fast(2))
.sound("saw")
.partials(new Array(numHarmonics).fill(1))
._scope()`}
/>
which acts as a spectral filter. Or:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
.sound("user")
.partials(new Array(50).fill(0)
.map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1))
)
._scope()`}
/>
which recovers a familiar waveform.
`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
.sound("user")
.partials(randL(10))
._scope()`}
/>
and with lists _of_ patterns:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(4))
.sound("user")
.partials([1, 0, "0 1", "0 1 0.3", rand])
._scope()`}
/>
Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0.
#### Phases
Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built.
These phases too can be declared in Strudel and can give your sounds interesting depth.
<MiniRepl
client:idle
tune={`s("saw").seg(16).n(irand(12)).scale("F1:minor")
.penv(48).panchor(0).pdec(0.05)
.delay(0.25).room(0.25)
.compressor(-20).vib(0.3)
.partials(randL(200))
.phases(randL(200))`}
/>
## Vibrato ## Vibrato
@@ -126,7 +126,7 @@ export function SoundsTab() {
try { try {
// Pre-load the sample by calling onTrigger with a future time // Pre-load the sample by calling onTrigger with a future time
// This triggers the loading but schedules playback for later // This triggers the loading but schedules playback for later
const time = ctx.currentTime + 0.05; const time = ctx.currentTime + 0.5; // Give 500ms for loading
const ref = await onTrigger(time, params, onended); const ref = await onTrigger(time, params, onended);
trigRef.current = ref; trigRef.current = ref;
if (ref?.node) { if (ref?.node) {