Compare commits

..

2 Commits

Author SHA1 Message Date
Felix Roos 50f04e8cc8 fix: echoWith / echoForEach in tests 2025-09-15 22:57:18 +02:00
Felix Roos 5e3218a67b breaking: tidal style echoWith + add echoForEach 2025-09-15 22:41:51 +02:00
36 changed files with 589 additions and 2427 deletions
+25 -309
View File
@@ -87,239 +87,6 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/**
* Position in the wavetable of the wavetable oscillator
*
* @name wt
* @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition
* @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
/**
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
/**
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
/**
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
/**
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
/**
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
/**
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
/**
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
/**
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
/**
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
/**
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
/**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
*
* @name warp
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp
* @example
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("spin")
*/
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
/**
* Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
/**
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
/**
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
/**
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
/**
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
/**
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
/**
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
/**
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
/**
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
/**
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
*
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
*
* @name warpmode
* @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode
* @example
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
*
*/
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
/**
* Amount of randomness of the initial phase of the wavetable oscillator.
*
* @name wtphaserand
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
* @synonyms wavetablePhaseRand
* @example
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
*
*/
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
/**
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
/**
* Define a custom webaudio node to use as a sound source.
*
@@ -347,7 +114,7 @@ export const { n } = registerControl('n');
*
* - a letter (a-g or A-G)
* - optional accidentals (b or #)
* - optional (possibly negative) octave number (0-9). Defaults to 3
* - optional octave number (0-9). Defaults to 3
*
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
*
@@ -360,8 +127,6 @@ export const { n } = registerControl('n');
* note("c4 a4 f4 e4")
* @example
* note("60 69 65 64")
* @example
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
*/
export const { note } = registerControl(['note', 'n']);
@@ -377,8 +142,8 @@ export const { note } = registerControl(['note', 'n']);
*/
export const { accelerate } = registerControl('accelerate');
/**
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
* @name velocity
* @example
* s("hh*8")
@@ -490,7 +255,7 @@ export const { fmenv } = registerControl('fmenv');
export const { fmattack } = registerControl('fmattack');
/**
* Waveform of the fm modulator
* waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
@@ -624,7 +389,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
// ['bpq'],
export const { bandq, bpq } = registerControl('bandq', 'bpq');
/**
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
* @memberof Pattern
* @name begin
@@ -685,7 +450,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
*/
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
/**
* Bit crusher effect.
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
@@ -696,7 +461,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
// ['clhatdecay'],
export const { crush } = registerControl('crush');
/**
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
@@ -707,7 +472,7 @@ export const { crush } = registerControl('crush');
export const { coarse } = registerControl('coarse');
/**
* Modulate the amplitude of a sound with a continuous waveform
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
@@ -719,7 +484,7 @@ export const { coarse } = registerControl('coarse');
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* Modulate the amplitude of a sound with a continuous waveform
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
@@ -734,7 +499,7 @@ export const { tremolosync } = registerControl(
);
/**
* Depth of amplitude modulation
* depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
@@ -745,7 +510,7 @@ export const { tremolosync } = registerControl(
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* Alter the shape of the modulation waveform
* alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
@@ -757,7 +522,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* Alter the phase of the modulation waveform
* alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
@@ -769,7 +534,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* Shape of amplitude modulation
* shape of amplitude modulation
*
* @name tremoloshape
* @synonyms tremshape
@@ -780,7 +545,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* Filter overdrive for supported filter types
* filter overdrive for supported filter types
*
* @name drive
* @param {number | Pattern} amount
@@ -791,9 +556,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
export const { drive } = registerControl('drive');
/**
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
*
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
* modulate the amplitude of an orbit to create a "sidechain" like effect
*
* @name duckorbit
* @synonyms duck
@@ -801,77 +564,30 @@ export const { drive } = registerControl('drive');
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
*
*/
export const { duck } = registerControl('duckorbit', 'duck');
/**
* The amount of ducking applied to target orbit
*
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
* the amount of ducking applied to target orbit
*
* @name duckdepth
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
*
*/
export const { duckdepth } = registerControl('duckdepth');
/**
* The time required for the ducked signal(s) to reach their lowest volume.
* Can be used to prevent clicking or for creative rhythmic effects.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckonset
* @synonyms duckons
*
* @param {number | Pattern} time The onset time in seconds
* @example
* // Clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
* @example
* // No clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
* @example
* // Rhythmic
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
* hhat: s("hh*16").orbit(7)
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
*
*/
export const { duckonset } = registerControl('duckonset', 'duckons');
/**
* The time required for the ducked signal(s) to return to their normal volume.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
* the attack time of the duck effect
*
* @name duckattack
* @synonyms duckatt
*
* @param {number | Pattern} time The attack time in seconds
* @param {number | Pattern} time
* @example
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
* @example
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* lessduck: s("hh*16").orbit(5)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
@@ -916,7 +632,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
export const { channels, ch } = registerControl('channels', 'ch');
/**
* Controls the pulsewidth of the pulse oscillator
* controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
@@ -928,7 +644,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* Controls the lfo rate for the pulsewidth of the pulse oscillator
* controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
@@ -940,7 +656,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
export const { pwrate } = registerControl('pwrate');
/**
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
* controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
@@ -981,7 +697,7 @@ export const { phaserrate, ph, phaser } = registerControl(
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
/**
* The center frequency of the phaser in HZ. Defaults to 1000
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
@@ -1009,7 +725,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
/**
* Choose the channel the pattern is sent to in superdirt
* choose the channel the pattern is sent to in superdirt
*
* @name channel
* @param {number | Pattern} channel channel number
@@ -1361,7 +1077,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* @name djf
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @example
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
*
*/
export const { djf } = registerControl('djf');
-2
View File
@@ -126,8 +126,6 @@ export const lcm = (...fractions) => {
);
};
export const isFraction = (x) => x instanceof Fraction;
fraction._original = Fraction;
export default fraction;
+2 -3
View File
@@ -5,9 +5,8 @@ let debounce = 1000,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
+30 -8
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import TimeSpan from './timespan.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Fraction, { lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
@@ -999,7 +999,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
// compose matrix functions
function _nonArrayObject(x) {
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
return !Array.isArray(x) && typeof x === 'object';
}
function _composeOp(a, b, func) {
if (_nonArrayObject(a) || _nonArrayObject(b)) {
@@ -2354,20 +2354,42 @@ export const jux = register('jux', function (func, pat) {
/**
* Superimpose and offset multiple times, applying the given function each time.
* @name echoWith
* @synonyms echowith, stutWith, stutwith
* @name echoForEach
* @param {number} times how many times to repeat
* @param {number} time cycle offset between iterations
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale("C:minor").note()
* .n().scale("C:minor")
*/
export const { echoForEach } = register(['echoForEach'], function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
});
/**
* Superimpose and offset multiple times, applying the given function each time.
* @name echoWith
* @synonyms echowith, stutWith, stutwith
* @param {number} times how many times to repeat
* @param {number} time cycle offset between iterations
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>".echoWith(4, 1/8, add(2))
* .n().scale("C:minor")
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
if (times < 1) {
return silence;
}
let pats = pat;
for (let i = 1; i < times; i++) {
pat = func(pat.late(time));
pats = pats.stack(pat);
}
return pats;
},
);
@@ -2383,7 +2405,7 @@ export const { echoWith, echowith, stutWith, stutwith } = register(
* s("bd sd").echo(3, 1/6, .8)
*/
export const echo = register('echo', function (times, time, feedback, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
return pat._echoForEach(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
/**
@@ -2396,7 +2418,7 @@ export const echo = register('echo', function (times, time, feedback, pat) {
* s("bd sd").stut(3, .8, 1/6)
*/
export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
return pat._echoForEach(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
export const applyN = register('applyN', function (n, func, p) {
+2 -16
View File
@@ -74,14 +74,6 @@ export function repl({
return silence;
};
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
return pat.__pure;
}
return pat;
}
const setPattern = async (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
await scheduler.setPattern(pattern, autostart);
@@ -93,10 +85,7 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => {
scheduler.setCps(unpure(cps));
return silence;
};
const setCps = (cps) => scheduler.setCps(cps);
/**
* Changes the global tempo to the given cycles per minute
@@ -108,10 +97,7 @@ export function repl({
* setcpm(140/4) // =140 bpm in 4/4
* $: s("bd*4,[- sd]*2").bank('tr707')
*/
const setCpm = (cpm) => {
scheduler.setCps(unpure(cpm) / 60);
return silence;
};
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
+2 -2
View File
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
+1 -1
View File
@@ -85,7 +85,7 @@ function evaluator(node, scope) {
let pat;
if (type === 'plain' && typeof variable !== 'undefined') {
// some function names are not patternable, so we skip reification here
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
return variable;
}
pat = reify(variable);
+16 -26
View File
@@ -4,13 +4,21 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
## Usage
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
OSC will only work if you run the REPL locally + the OSC server besides it:
```sh
npx @strudel/osc
From the project root:
```js
npm run repl
```
You should see something like:
and in a seperate shell:
```js
npm run osc
```
This should give you
```log
osc client running on port 57120
@@ -18,32 +26,14 @@ osc server running on port 57121
websocket server running on port 8080
```
### --port
Now open Supercollider (with the super dirt startup file)
By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option:
```sh
npx @strudel/osc --port 7771 # classic dirt
```
### --debug
To log all incoming osc messages, add the `--debug` flag:
```sh
npx @strudel/osc --debug
```
## Usage in Strudel
To test it in strudel, you have can use `all(osc)` to send all events through osc:
Now open the REPL and type:
```js
$: s("bd*4")
all(osc)
s("<bd sd> hh").osc()
```
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
+4 -2
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
@@ -81,4 +81,6 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* @memberof Pattern
* @returns Pattern
*/
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
Pattern.prototype.osc = function () {
return this.onTrigger(oscTrigger);
};
+1 -2
View File
@@ -1,9 +1,8 @@
{
"name": "@strudel/osc",
"version": "1.2.10",
"version": "1.2.4",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
+2 -43
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env node
/*
server.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
@@ -8,19 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
let udpClientPort = Number(getArgValue('--port')) || 57120;
let debug = Number(getArgValue('--debug')) || 0;
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
@@ -32,7 +17,7 @@ const config = {
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: udpClientPort, // @param {number} Port of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
@@ -42,34 +27,8 @@ const config = {
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
if (debug) {
osc.on('*', (message) => {
const { address, args } = message;
let str = '';
for (let i = 0; i < args.length; i += 2) {
str += `${args[i]}: ${args[i + 1]} `;
}
console.log(`${address} ${str}`);
});
}
osc.on('error', (message) => {
if (message.toString().includes('EADDRINUSE')) {
console.log(`------ ERROR -------
a server is already running on port 57121! to stop it:
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
2. re-run the osc server
`);
} else {
console.log(message);
}
});
osc.open();
osc.open(); // start a WebSocket server on port 8080
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
if (debug) {
console.log('debug logs enabled. incoming messages will appear below');
}
+2 -97
View File
@@ -1,5 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { clamp, nanFallback } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -10,13 +10,6 @@ export function gainNode(value) {
return node;
}
export function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
const getSlope = (y1, y2, x1, x2) => {
const denom = x2 - x1;
if (denom === 0) {
@@ -28,9 +21,7 @@ const getSlope = (y1, y2, x1, x2) => {
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
node.parameters.get(key).value = value;
}
node.parameters.get(key).value = value;
});
return node;
}
@@ -97,35 +88,6 @@ export const getParamADSR = (
param[ramp](min, end + release);
};
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = {
threshold: threshold ?? -3,
@@ -153,41 +115,6 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
// helper utility for applying standard modulators to a parameter
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
if (amount == null) {
const hasADSRParams = values.some((p) => p != null);
amount = hasADSRParams ? defaultAmount : 0;
}
const min = offset ?? 0;
const max = amount + min;
const diff = Math.abs(max - min);
if (diff) {
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
let lfo;
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
@@ -380,25 +307,3 @@ export function applyFM(param, value, begin) {
}
return { stop };
}
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
export const destroyAudioWorkletNode = (node) => {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
};
-1
View File
@@ -11,4 +11,3 @@ export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
export * from './wavetable.mjs';
+3 -4
View File
@@ -1,9 +1,8 @@
let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'superdough') {
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
+39 -29
View File
@@ -1,5 +1,5 @@
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -22,16 +22,39 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
const { s, n = 0, speed = 1.0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
sampleUrl = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
const label = `${s}:${index}`;
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate };
return { transpose, sampleUrl, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
@@ -56,14 +79,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.buffer = buffer;
bufferSource.playbackRate.value = playbackRate;
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
const loop = hapValue.loop;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
@@ -244,12 +267,16 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
return samples(json, baseUrl || base, options);
}
const { prebake, tag } = options;
processSampleMap(
sampleMap,
(key, bank) => {
registerSampleSource(key, bank, { baseUrl, prebake, tag });
},
(key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
baseUrl,
prebake,
tag,
}),
baseUrl,
);
};
@@ -339,20 +366,3 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
return handle;
}
function registerSample(key, bank, params) {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
...params,
});
}
export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_');
if (isWavetable) {
registerWaveTable(key, bank, params);
} else {
registerSample(key, bank, params);
}
}
+187 -39
View File
@@ -7,13 +7,12 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { logger, errorLogger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
@@ -29,6 +28,13 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -295,16 +301,88 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
let controller;
function getSuperdoughAudioController() {
if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext());
}
return controller;
const maxfeedback = 0.98;
let channelMerger, destinationGain;
//update the output channel configuration to match user's audio device
export function initializeAudioOutput() {
const audioContext = getAudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
destinationGain = new GainNode(audioContext);
channelMerger.connect(destinationGain);
destinationGain.connect(audioContext.destination);
}
export function connectToDestination(input, channels) {
const controller = getSuperdoughAudioController();
controller.output.connectToDestination(input, channels);
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
if (channelMerger == null) {
initializeAudioOutput();
}
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(ctx);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(ctx, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
export const panic = () => {
if (destinationGain == null) {
return;
}
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destinationGain = null;
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!orbits[orbit].delayNode) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(dly, orbit);
orbits[orbit].delayNode = dly;
}
orbits[orbit].delayNode.delayTime.value !== delaytime &&
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -338,6 +416,78 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
//type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
}
if (
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
orbits[orbit].reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return orbits[orbit].reverbNode;
}
export let analysers = {},
analysersData = {};
@@ -370,8 +520,15 @@ export function getAnalyzerData(type = 'time', id = 1) {
return analysersData[id];
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
export function resetGlobalEffects() {
controller?.reset();
orbits = {};
analysers = {};
analysersData = {};
}
@@ -386,7 +543,6 @@ function mapChannelNumbers(channels) {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
const audioController = getSuperdoughAudioController();
let { stretch } = value;
if (stretch != null) {
@@ -426,10 +582,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckonset,
duckattack,
duckdepth,
djf,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
@@ -506,9 +660,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
const orbitBus = audioController.getOrbit(orbit, channels);
setOrbit(ac, orbit, channels, t, cycle, cps);
if (duckorbit != null) {
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
@@ -555,7 +710,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded, cps);
const soundHandle = await onTrigger(t, value, onEnded);
if (soundHandle) {
sourceNode = soundHandle.node;
@@ -653,14 +808,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
tremolo = cps * tremolosync;
}
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
@@ -706,11 +853,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(post);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
orbitBus.getDelay(delaytime, delayfeedback, t);
orbitBus.sendDelay(post, delay);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
// reverb
let reverbSend;
if (room > 0) {
let roomIR;
if (ir !== undefined) {
@@ -723,27 +873,25 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
orbitBus.sendReverb(post, room);
}
if (djf != null) {
orbitBus.getDjf(djf, t);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
const analyserSend = effectSend(post, analyserNode, 1);
analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
orbitBus.connectToOutput(dryGain);
connectToOrbit(dryGain, orbit);
} else {
orbitBus.connectToOutput(post);
connectToOrbit(post, orbit);
}
// connect chain elements together
-206
View File
@@ -1,206 +0,0 @@
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { clamp } from './util.mjs';
let hasChanged = (now, before) => now !== undefined && now !== before;
export class Orbit {
reverbNode;
delayNode;
output;
summingNode;
djfNode;
audioContext;
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode.connect(this.output);
}
disconnect() {
this.output.disconnect();
this.summingNode.disconnect();
this.delayNode?.disconnect();
this.reverbNode?.disconnect();
}
getDjf(value, t = 0) {
if (this.djfNode == null) {
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
this.summingNode.disconnect();
this.summingNode.connect(this.djfNode);
this.djfNode.connect(this.output);
}
const val = this.djfNode.parameters.get('value');
val.setValueAtTime(value, t);
}
getDelay(delaytime = 0, feedback = 0.5, t) {
const maxfeedback = 0.98;
if (feedback > maxfeedback) {
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
}
feedback = clamp(feedback, 0, 0.98);
if (this.delayNode == null) {
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
this.delayNode.connect(this.summingNode);
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
}
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
return this.delayNode;
}
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (this.reverbNode == null) {
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
this.reverbNode.connect(this.summingNode);
}
if (
hasChanged(duration, this.reverbNode.duration) ||
hasChanged(fade, this.reverbNode.fade) ||
hasChanged(lp, this.reverbNode.lp) ||
hasChanged(dim, this.reverbNode.dim) ||
hasChanged(irspeed, this.reverbNode.irspeed) ||
hasChanged(irbegin, this.reverbNode.irbegin) ||
this.reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return this.reverbNode;
}
sendReverb(node, amount) {
effectSend(node, this.reverbNode, amount);
}
sendDelay(node, amount) {
effectSend(node, this.delayNode, amount);
}
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
const onset = onsettime;
const attack = Math.max(attacktime, 0.002);
const gainParam = this.output.gain;
webAudioTimeout(
this.audioContext,
() => {
const now = this.audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
}
connectToOutput(node) {
node.connect(this.summingNode);
}
}
export class SuperdoughOutput {
channelMerger;
destinationGain;
constructor(audioContext) {
this.audioContext = audioContext;
this.initializeAudio();
}
initializeAudio() {
const audioContext = this.audioContext;
const maxChannelCount = audioContext.destination.maxChannelCount;
this.audioContext.destination.channelCount = maxChannelCount;
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
this.destinationGain = new GainNode(audioContext);
this.channelMerger.connect(this.destinationGain);
this.destinationGain.connect(audioContext.destination);
}
reset() {
this.channelMerger.disconnect();
this.destinationGain.disconnect();
this.destinationGain = null;
this.channelMerger = null;
this.nodes = {};
this.initializeAudio();
}
connectToDestination = (input, channels = [0, 1]) => {
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(this.audioContext);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(this.audioContext, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
});
};
}
export class SuperdoughAudioController {
audioContext;
output;
nodes = {};
constructor(audioContext) {
this.audioContext = audioContext;
this.output = new SuperdoughOutput(audioContext);
}
reset() {
Array.from(this.nodes).forEach((node) => {
node.disconnect();
});
this.output.reset();
}
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
const targetArr = [targetOrbits].flat();
const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat();
const depthArr = [depth].flat();
targetArr.forEach((target, idx) => {
const orbit = this.nodes[target];
if (orbit == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0];
orbit.duck(t, onset, attack, depth);
});
}
getOrbit(orbitNum, channels) {
if (this.nodes[orbitNum] == null) {
this.nodes[orbitNum] = new Orbit(this.audioContext);
this.output.connectToDestination(this.nodes[orbitNum].output, channels);
}
return this.nodes[orbitNum];
}
}
+24 -6
View File
@@ -1,21 +1,39 @@
import { clamp } from './util.mjs';
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import {
applyFM,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
+1 -30
View File
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -76,32 +76,3 @@ export function cycleToSeconds(cycle, cps) {
export function secondsToCycle(t, cps) {
return t * cps;
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
}
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
}
-346
View File
@@ -1,346 +0,0 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo } from './util.mjs';
import {
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
const WT_MAX_MIP_LEVELS = 6;
export const Warpmode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
async function loadWavetableFrames(url, label, frameLen = 2048) {
const buf = await loadBuffer(url, label);
const ch0 = buf.getChannelData(0);
const total = ch0.length;
const numFrames = Math.max(1, Math.floor(total / frameLen));
const frames = new Array(numFrames);
for (let i = 0; i < numFrames; i++) {
const start = i * frameLen;
frames[i] = ch0.subarray(start, start + frameLen);
}
// build mipmaps
const mipmaps = [frames];
let levelFrames = frames;
for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) {
const prevLen = levelFrames[0].length;
if (prevLen <= 32) break;
const nextLen = prevLen >> 1;
const next = levelFrames.map((src) => {
const out = new Float32Array(nextLen);
for (let j = 0; j < nextLen; j++) {
out[j] = (src[2 * j] + src[2 * j + 1]) / 2;
}
return out;
});
mipmaps.push(next);
levelFrames = next;
}
return { frames, mipmaps, frameLen, numFrames };
}
const loadCache = {};
function humanFileSize(bytes, si) {
var thresh = si ? 1000 : 1024;
if (bytes < thresh) return bytes + ' B';
var units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
var u = -1;
do {
bytes /= thresh;
++u;
} while (bytes >= thresh);
return bytes.toFixed(1) + ' ' + units[u];
}
// Extract the sample rate of a .wav file
function parseWavSampleRate(arrBuf) {
const dv = new DataView(arrBuf);
// Header is "RIFF<chunk size (4 bytes)>WAVE", so 12 bytes
let p = 12;
// Look through chunks for the format header
// (they will always have an 8 byte header (id and size) followed by a payload)
while (p + 8 <= dv.byteLength) {
// Parse id
const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3));
// Parse chunk size
const size = dv.getUint32(p + 4, true);
if (id === 'fmt ') {
// The format chunk contains the sample rate after
// 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels
// (for a total of 12)
return dv.getUint32(p + 12, true);
}
// Advance to next chunk
p += 8 + size + (size & 1);
}
return null;
}
async function decodeAtNativeRate(arr) {
const sr = parseWavSampleRate(arr) || 44100;
const tempAC = new OfflineAudioContext(1, 1, sr);
return await tempAC.decodeAudioData(arr);
}
const loadBuffer = (url, label) => {
url = url.replace('#', '%23');
if (!loadCache[url]) {
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
const timestamp = Date.now();
loadCache[url] = fetch(url)
.then((res) => res.arrayBuffer())
.then(async (res) => {
const took = Date.now() - timestamp;
const size = humanFileSize(res.byteLength);
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url });
const decoded = await decodeAtNativeRate(res);
return decoded;
});
}
return loadCache[url];
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
const _processTables = (json, baseUrl, frameLen, options = {}) => {
baseUrl = json._base || baseUrl;
return Object.entries(json).forEach(([key, tables]) => {
if (key === '_base') return false;
if (typeof tables === 'string') {
tables = [tables];
}
if (typeof tables !== 'object') {
throw new Error('wrong json format for ' + key);
}
let resolvedUrl = baseUrl;
if (resolvedUrl.startsWith('github:')) {
resolvedUrl = githubPath(resolvedUrl, '');
}
tables = tables
.map((t) => resolvedUrl + t)
.filter((t) => {
if (!t.toLowerCase().endsWith('.wav')) {
logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`);
return false;
}
return true;
});
if (tables.length) {
registerWaveTable(key, tables, { baseUrl, frameLen });
}
});
};
export function registerWaveTable(key, tables, params) {
registerSound(
key,
(t, hapValue, onended, cps) => {
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
},
{
type: 'wavetable',
tables,
...params,
},
);
}
/**
* Loads a collection of wavetables to use with `s`
*
* @name tables
*/
export const tables = async (url, frameLen, json, options = {}) => {
if (json !== undefined) return _processTables(json, url, frameLen);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(url)
.then((res) => res.json())
.then((json) => _processTables(json, url, frameLen, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
};
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const { s, n = 0, duration } = value;
const ac = getAudioContext();
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
let { warpmode } = value;
if (typeof warpmode === 'string') {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
}
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfo(value, tables);
const payload = await loadWavetableFrames(url, label, frameLen);
const holdEnd = t + duration;
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
detune: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
source.port.postMessage({ type: 'tables', payload });
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
}
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease];
const wtParams = source.parameters;
const positionParam = wtParams.get('position');
const warpParam = wtParams.get('warp');
let wtrate = value.wtrate;
if (value.wtsync != null) {
wtrate = cps * value.wtsync;
}
const wtPosModulators = applyParameterModulators(
ac,
positionParam,
t,
endWithRelease,
{
offset: value.wt,
amount: value.wtenv,
defaultAmount: 0.5,
shape: 'linear',
values: posADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: wtrate,
depth: value.wtdepth,
defaultDepth: 0.5,
shape: value.wtshape,
skew: value.wtskew,
dcoffset: value.wtdc ?? 0,
},
);
let warprate = value.warprate;
if (value.warpsync != null) {
warprate = warprate = cps * value.warpsync;
}
const wtWarpModulators = applyParameterModulators(
ac,
warpParam,
t,
endWithRelease,
{
offset: value.warp,
amount: value.warpenv,
defaultAmount: 0.5,
shape: 'linear',
values: warpADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: warprate,
depth: value.warpdepth,
defaultDepth: 0.5,
shape: value.warpshape,
skew: value.warpskew,
dcoffset: value.warpdc ?? 0,
},
);
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
const envGain = ac.createGain();
const node = source.connect(envGain);
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
const handle = { node, source };
const timeoutNode = webAudioTimeout(
ac,
() => {
source.disconnect();
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
},
t,
envEnd,
);
handle.stop = (time) => {
timeoutNode.stop(time);
};
return handle;
}
+36 -424
View File
@@ -6,21 +6,7 @@ import OLAProcessor from './ola-processor';
import FFT from './fft.js';
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 frac = (x) => x - Math.floor(x);
const ffloor = (x) => x | 0; // fast floor for non-negative
const getUnisonDetune = (unison, detune, voiceIndex) => {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
};
const applySemitoneDetuneToFrequency = (frequency, detune) => {
return frequency * Math.pow(2, detune / 12);
};
const _mod = (n, m) => ((n % m) + m) % m;
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
@@ -164,7 +150,7 @@ class LFOProcessor extends AudioWorkletProcessor {
const blockSize = output[0].length ?? 0;
if (this.phase == null) {
this.phase = mod(time * frequency + phaseoffset, 1);
this.phase = _mod(time * frequency + phaseoffset, 1);
}
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
@@ -285,73 +271,6 @@ class ShapeProcessor extends AudioWorkletProcessor {
}
registerProcessor('shape-processor', ShapeProcessor);
class TwoPoleFilter {
s0 = 0;
s1 = 0;
update(s, cutoff, resonance = 0) {
// Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf
return this.s1; // return lpf by default
}
}
class DJFProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{ name: 'value', defaultValue: 0.5 }];
}
constructor() {
super();
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
this.started = hasInput;
const value = clamp(parameters.value[0], 0, 1);
let filterType = 'none';
let cutoff;
let v = 1;
if (value > 0.51) {
filterType = 'hipass';
v = (value - 0.5) * 2;
} else if (value < 0.49) {
filterType = 'lopass';
v = value * 2;
}
cutoff = Math.pow(v * 11, 4);
for (let i = 0; i < input.length; i++) {
for (let n = 0; n < blockSize; n++) {
if (filterType == 'none') {
output[i][n] = input[i][n];
} else {
this.filters[i].update(input[i][n], cutoff, 0.1);
if (filterType === 'lopass') {
output[i][n] = this.filters[i].s1;
} else if (filterType === 'hipass') {
output[i][n] = input[i][n] - this.filters[i].s1;
} else {
output[i][n] = input[i][n];
}
}
}
}
return true;
}
}
registerProcessor('djf-processor', DJFProcessor);
function fast_tanh(x) {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
@@ -459,6 +378,21 @@ class DistortProcessor extends AudioWorkletProcessor {
registerProcessor('distort-processor', DistortProcessor);
// SUPERSAW
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
function applySemitoneDetuneToFrequency(frequency, detune) {
return frequency * Math.pow(2, detune / 12);
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
@@ -520,31 +454,29 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
}
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
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 gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
let freq = pv(params.frequency, i);
// Main detuning
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
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;
}
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;
}
for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
// Individual voice detuning
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = mod(freqVoice / sampleRate, 1);
const dt = _mod(freq / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
@@ -975,323 +907,3 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
}
registerProcessor('byte-beat-processor', ByteBeatProcessor);
export const WarpMode = Object.freeze({
NONE: 0,
ASYM: 1,
MIRROR: 2,
BENDP: 3,
BENDM: 4,
BENDMP: 5,
SYNC: 6,
QUANT: 7,
FOLD: 8,
PWM: 9,
ORBIT: 10,
SPIN: 11,
CHAOS: 12,
PRIMES: 13,
BINARY: 14,
BROWNIAN: 15,
RECIPROCAL: 16,
WORMHOLE: 17,
LOGISTIC: 18,
SIGMOID: 19,
FRACTAL: 20,
FLIP: 21,
});
function hash32(u) {
u = u + 0x7ed55d16 + (u << 12);
u = u ^ 0xc761c23c ^ (u >>> 19);
u = u + 0x165667b1 + (u << 5);
u = (u + 0xd3a2646c) ^ (u << 9);
u = u + 0xfd7046c5 + (u << 3);
u = u ^ 0xb55a4f09 ^ (u >>> 16);
return u >>> 0;
}
const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000;
function bitReverse(i, n) {
let r = 0;
for (let b = 0; b < n; b++) {
r = (r << 1) | (i & 1);
i >>>= 1;
}
return r;
}
function noise(x) {
const i = Math.floor(x),
f = x - i;
const a = hash01(i),
b = hash01(i + 1);
return a + (b - a) * f;
}
function brownian(x, oct = 4) {
let amp = 0.5,
sum = 0,
norm = 0,
freq = 1;
for (let o = 0; o < oct; o++) {
sum += amp * noise(x * freq);
norm += amp;
amp *= 0.5;
freq *= 2;
}
return (sum / norm) * 2 - 1;
}
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
{ name: 'detune', defaultValue: 0.18 },
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
];
}
constructor(options) {
super(options);
this.tables = null;
this.frameLen = 0;
this.numFrames = 0;
this.phase = [];
this.syncRatio = 1;
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'tables') {
this.tables = payload.mipmaps;
this.frameLen = payload.frameLen;
this.numFrames = this.tables[0].length;
}
};
}
_chooseMip(dphi) {
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
let level = 0;
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
level++;
}
return level;
}
_mirror(x) {
return 1 - Math.abs(2 * x - 1);
}
_toBits(amt, min = 2, max = 12) {
const b = max + (min - max) * amt;
return { b, n: Math.round(Math.pow(2, b)) };
}
_warpPhase(phase, amt, mode) {
switch (mode) {
case WarpMode.NONE: {
return phase;
}
case WarpMode.ASYM: {
const a = 0.01 + 0.99 * amt;
return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a);
}
case WarpMode.MIRROR: {
// Asym, then mirror
return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM));
}
case WarpMode.BENDP: {
return Math.pow(phase, 1 + 3 * amt);
}
case WarpMode.BENDM: {
return Math.pow(phase, 1 / (1 + 3 * amt));
}
case WarpMode.BENDMP: {
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
}
case WarpMode.SYNC: {
const syncRatio = Math.pow(16, amt * amt);
return (phase * syncRatio) % 1;
}
case WarpMode.QUANT: {
const { n } = this._toBits(amt);
return ffloor(phase * n) / n;
}
case WarpMode.FOLD: {
const K = 7;
const k = 1 + Math.max(1, Math.round(K * amt));
return Math.abs(frac(k * phase) - 0.5) * 2;
}
case WarpMode.PWM: {
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
if (phase < w) return (phase / w) * 0.5;
return 0.5 + ((phase - w) / (1 - w)) * 0.5;
}
case WarpMode.ORBIT: {
const depth = 0.5 * amt;
const n = 3;
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
}
case WarpMode.SPIN: {
const depth = 0.5 * amt;
const { n } = this._toBits(amt, 1, 6);
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
}
case WarpMode.CHAOS: {
const r = 3.7 + 0.3 * amt;
const logistic = r * phase * (1 - phase);
return clamp((1 - amt) * phase + amt * logistic, 0, 1);
}
case WarpMode.PRIMES: {
const isPrime = (n) => {
if (n < 2) return false;
if (n % 2 === 0) return n === 2;
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
return true;
};
let { n } = this._toBits(amt, 3);
while (!isPrime(n)) n++;
return ffloor(phase * n) / n;
}
case WarpMode.BINARY: {
let { b } = this._toBits(amt, 3);
b = Math.round(b);
const n = 1 << b;
const idx = ffloor(phase * n);
const ridx = bitReverse(idx, b);
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: {
const disp = 0.25 * amt * brownian(64 * phase, 4);
return frac(phase + disp);
}
case WarpMode.RECIPROCAL: {
const g = 2 + 4 * amt;
const num = phase * g;
const den = phase + (1 - phase) * g;
const y = den > 1e-12 ? num / den : 0;
return clamp(y, 0, 1);
}
case WarpMode.WORMHOLE: {
const gap = clamp(0.8 * amt, 0, 1);
const a = 0.5 * (1 - gap);
const b = 0.5 * (1 + gap);
if (phase < a) return (phase / a) * 0.5;
if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b));
return 0.5;
}
case WarpMode.LOGISTIC: {
let x = phase;
const r = 3.6 + 0.4 * amt;
const iters = 1 + Math.round(2 * amt);
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
return clamp(x, 0, 1);
}
case WarpMode.SIGMOID: {
const k = 1 + 10 * amt;
const x = phase - 0.5;
const y = 1 / (1 + Math.exp(-k * x));
const y0 = 1 / (1 + Math.exp(0.5 * k));
const y1 = 1 / (1 + Math.exp(-0.5 * k));
return (y - y0) / (y1 - y0);
}
case WarpMode.FRACTAL: {
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
return frac(phase + d);
}
case WarpMode.FLIP: {
return phase;
}
default:
return phase;
}
}
_sampleFrame(frame, phase) {
const pos = phase * frame.length;
const i = pos | 0;
const frac = pos - i;
const a = frame[i % frame.length];
const b = frame[(i + 1) % frame.length];
return a + (b - a) * frac;
}
process(_inputs, outputs, parameters) {
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= parameters.begin[0]) {
return true;
}
const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0];
if (!this.tables) {
outL.fill(0);
if (outR !== outL) outR.set(outL);
return true;
}
for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i);
const tablePos = pv(parameters.position, i);
const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0;
const frac = idx - fIdx;
const warpAmount = pv(parameters.warp, i);
const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i);
const phaseRand = pv(parameters.phaserand, i);
const spread = voices > 1 ? pv(parameters.spread, i) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 0.3 / Math.sqrt(voices);
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, detune, n)); // voice detune
const dPhase = fVoice / sampleRate;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
// warp phase then sample
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
let s = s0 + (s1 - s0) * frac;
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s;
}
outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * normalizer;
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
}
}
return true;
}
}
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
+69 -119
View File
@@ -7,130 +7,80 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
import { pure, n, seq, note } from '@strudel/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
describe('tonal', () => {
describe('scaleTranspose', () => {
it('transposes notes by scale degrees', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
describe('scale', () => {
it('converts plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (no tonic)', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (explicit mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts decorated n values', () => {
expect(
n(seq('0b', '1#', '-2', '3##', '4bb'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
});
it('produces silence for mixed sharps and flats', () => {
expect(
n(seq('0b#', '1#b', '2#b#'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual([]);
});
it('snaps notes (upwards) to scale', () => {
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
expect(
note(seq(inputNotes))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('snaps notes to the correct octave', () => {
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
expect(
note(seq(inputNotes))
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('handles scale names provided with colons', () => {
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
expect(
note(seq(inputNotes))
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('scale with plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
describe('transpose', () => {
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
it('scale with n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale without tonic', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
});
+53 -101
View File
@@ -6,28 +6,19 @@ This program is free software: you can redistribute it and/or modify it under th
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
import { noteToMidi } from '../core/util.mjs';
import { stepInNamedScale } from './tonleiter.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
function getScale(scaleName) {
scaleName = scaleName.replaceAll(':', ' ');
const scale = Scale.get(scaleName);
const { tonic, empty } = scale;
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
throw new Error(
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
);
} else if (empty) {
throw new Error(`Invalid scale name "${scaleName}"`);
}
return scale;
}
function scaleStep(step, scale) {
scale = scale.replaceAll(':', ' ');
step = Math.ceil(step);
let { intervals, tonic } = getScale(scale);
let { intervals, tonic, empty } = Scale.get(scale);
if ((empty && isNote(scale)) || (empty && !tonic)) {
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
} else if (empty) {
throw new Error(`invalid scale "${scale}"`);
}
tonic = tonic || 'C';
const { pc, oct = 3 } = Note.get(tonic);
const octaveOffset = Math.floor(step / intervals.length);
@@ -39,7 +30,8 @@ function scaleStep(step, scale) {
// transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) {
function scaleOffset(scale, offset, note) {
let { notes } = getScale(scale);
let [tonic, scaleName] = Scale.tokenize(scale);
let { notes } = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note) => Note.get(note).pc); // use only pc!
offset = Number(offset);
if (isNaN(offset)) {
@@ -128,7 +120,10 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones)
: String(intervalOrSemitones);
const targetNote = Note.transpose(note, interval);
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
// TODO: check if this is still relevant..
const targetNote = Note.simplify(Note.transpose(note, interval));
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
@@ -176,59 +171,8 @@ export const { scaleTranspose, scaleTrans, strans } = register(
},
);
// Converts a step value, which is a number optionally decorated with sharps and flats,
// to a number and an `offset` number of semitones
function _convertStepToNumberAndOffset(step) {
let asNumber = Number(step);
let offset = 0;
if (isNaN(asNumber)) {
step = String(step);
// Check to see if the step matches the expected format:
// - A number (possibly negative)
// - Some number of sharps or flats (but not both)
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
if (!match) {
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
}
asNumber = Number(match[1]);
// These decorations will determine the semitone offset based on the number of
// sharps or flats
const decorations = match[2] || '';
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
}
return [asNumber, offset];
}
let scaleToMidisAndNotes = {};
// Finds the nearest scale note to `note`
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
if (scaleToMidisAndNotes[scaleName] === undefined) {
const { intervals, tonic } = getScale(scaleName);
const { pc } = Note.get(tonic);
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
const sMidi = sNotes.map(noteToMidi);
// Cache
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
}
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
const rootMidi = scaleMidis[0];
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
const noteMatch = scaleNotes[noteIdx];
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
}
/**
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
*
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
*
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
@@ -247,12 +191,6 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* n(rand.range(0,12).segment(8))
* .scale("C:ritusen")
* .s("piano")
* @example
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
* .scale("C:<major minor>/2")
* .s("piano")
* @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
*/
export const scale = register(
@@ -266,35 +204,49 @@ export const scale = register(
pat
.fmap((value) => {
const isObject = typeof value === 'object';
// The case where the note has been defined via `n` or `pure`
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
const step = isObject ? (value.n ?? value.value) : value;
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
if (isNote(step)) {
// legacy..
return pure(step);
}
try {
const [number, offset] = _convertStepToNumberAndOffset(step);
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(number, scale, value.anchor);
} else {
note = scaleStep(number, scale);
}
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
}
if (isNote(step)) {
// legacy..
return pure(step);
}
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
return value;
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
}
// The case where the note has been defined via `note`
else {
const note = _getNearestScaleNote(scale, value.note);
return pure(isObject ? { ...value, note } : note);
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
+4 -3
View File
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
let scaleSteps = {}; // [scaleName]: semitones[]
export function stepInNamedScale(step, scale, anchor, preferHigher) {
const [root, scaleName] = Scale.tokenize(scale);
let [root, scaleName] = Scale.tokenize(scale);
const rootMidi = x2midi(root);
const rootChroma = midi2chroma(rootMidi);
if (!scaleSteps[scaleName]) {
const { intervals } = Scale.get(`C ${scaleName}`);
let { intervals } = Scale.get(`C ${scaleName}`);
// cache result
scaleSteps[scaleName] = intervals.map(step2semitones);
}
@@ -222,7 +222,6 @@ export const Note = {
};
// TODO: support octave numbers
// Example: Note("Bb3").transpose("c3")
export function transpose(note, step) {
// example: E, 3
const stepNumber = Step.tokenize(step)[1]; // 3
@@ -236,3 +235,5 @@ export function transpose(note, step) {
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
return [targetNote, offsetAccidentals].join('');
}
//Note("Bb3").transpose("c3")
+74 -533
View File
@@ -2831,38 +2831,26 @@ exports[`runs examples > example "distort" example index 1 1`] = `
exports[`runs examples > example "djf" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
"[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]",
"[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]",
"[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]",
"[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]",
"[ 5/83/4 | note:F3 s:supersaw djf:0.5 ]",
"[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]",
"[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]",
"[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]",
"[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]",
"[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]",
"[ 11/83/2 | note:F3 s:supersaw djf:0.3 ]",
"[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]",
"[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]",
"[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]",
"[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]",
"[ 2/117/8 | note:Eb5 s:supersaw djf:0.2 ]",
"[ 17/89/4 | note:D5 s:supersaw djf:0.2 ]",
"[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]",
"[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]",
"[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]",
"[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]",
"[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]",
"[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]",
"[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]",
"[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]",
"[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]",
"[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]",
"[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]",
"[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]",
"[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]",
"[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]",
"[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]",
"[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]",
"[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]",
"[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]",
"[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]",
"[ 1/15/4 | n:0 s:superzow octave:3 djf:0.25 ]",
"[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]",
"[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]",
"[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]",
"[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]",
"[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]",
"[ 9/45/2 | n:3 s:superzow octave:3 djf:0.5 ]",
"[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]",
"[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]",
"[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]",
"[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]",
"[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]",
"[ 7/215/4 | n:7 s:superzow octave:3 djf:0.75 ]",
"[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]",
"[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]",
]
`;
@@ -3105,50 +3093,57 @@ exports[`runs examples > example "dry" example index 0 1`] = `
exports[`runs examples > example "duckattack" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
]
`;
exports[`runs examples > example "duckattack" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
]
`;
@@ -3209,94 +3204,6 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = `
]
`;
exports[`runs examples > example "duckdepth" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
]
`;
exports[`runs examples > example "duckonset" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
]
`;
exports[`runs examples > example "duckonset" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
]
`;
exports[`runs examples > example "duckonset" example index 2 1`] = `
[
"[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
"[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
]
`;
exports[`runs examples > example "duckorbit" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
@@ -3322,31 +3229,6 @@ exports[`runs examples > example "duckorbit" example index 0 1`] = `
]
`;
exports[`runs examples > example "duckorbit" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
]
`;
exports[`runs examples > example "duration" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
@@ -3419,20 +3301,28 @@ exports[`runs examples > example "echo" example index 0 1`] = `
]
`;
exports[`runs examples > example "echoForEach" example index 0 1`] = `[]`;
exports[`runs examples > example "echoWith" example index 0 1`] = `
[
"[ -3/8 ⇜ (0/1 → 1/8) | note:Bb3 ]",
"[ -1/4 ⇜ (0/1 → 1/4) | note:D4 ]",
"[ -1/8 ⇜ (0/1 → 3/8) | note:F4 ]",
"[ -1/4 ⇜ (0/1 → 1/8) ⇝ 1/4 | note:D4 ]",
"[ -1/8 ⇜ (0/1 → 1/8) ⇝ 3/8 | note:F4 ]",
"[ 0/1 → 1/1 | note:C3 ]",
"[ -1/4 ⇜ (1/8 → 1/4) | note:D4 ]",
"[ -1/8 ⇜ (1/8 → 1/4) ⇝ 3/8 | note:F4 ]",
"[ (1/8 → 1/1) ⇝ 9/8 | note:Eb3 ]",
"[ -1/8 ⇜ (1/4 → 3/8) | note:F4 ]",
"[ (1/4 → 1/1) ⇝ 5/4 | note:G3 ]",
"[ (3/8 → 1/1) ⇝ 11/8 | note:Bb3 ]",
"[ 1/8 ⇜ (1/1 → 9/8) | note:Eb3 ]",
"[ 1/4 ⇜ (1/1 → 5/4) | note:G3 ]",
"[ 3/8 ⇜ (1/1 → 11/8) | note:Bb3 ]",
"[ 1/4 ⇜ (1/1 → 9/8) ⇝ 5/4 | note:G3 ]",
"[ 3/8 ⇜ (1/1 → 9/8) ⇝ 11/8 | note:Bb3 ]",
"[ 1/1 → 3/2 | note:Eb3 ]",
"[ 1/4 ⇜ (9/8 → 5/4) | note:G3 ]",
"[ 3/8 ⇜ (9/8 → 5/4) ⇝ 11/8 | note:Bb3 ]",
"[ 9/8 → 13/8 | note:G3 ]",
"[ 3/8 ⇜ (5/4 → 11/8) | note:Bb3 ]",
"[ 5/4 → 7/4 | note:Bb3 ]",
"[ 11/8 → 15/8 | note:D4 ]",
"[ 3/2 → 2/1 | note:G3 ]",
@@ -3440,17 +3330,23 @@ exports[`runs examples > example "echoWith" example index 0 1`] = `
"[ (7/4 → 2/1) ⇝ 9/4 | note:D4 ]",
"[ (15/8 → 2/1) ⇝ 19/8 | note:F4 ]",
"[ 13/8 ⇜ (2/1 → 17/8) | note:Bb3 ]",
"[ 7/4 ⇜ (2/1 → 9/4) | note:D4 ]",
"[ 15/8 ⇜ (2/1 → 19/8) | note:F4 ]",
"[ 7/4 ⇜ (2/1 → 17/8) ⇝ 9/4 | note:D4 ]",
"[ 15/8 ⇜ (2/1 → 17/8) ⇝ 19/8 | note:F4 ]",
"[ 2/1 → 3/1 | note:C3 ]",
"[ 7/4 ⇜ (17/8 → 9/4) | note:D4 ]",
"[ 15/8 ⇜ (17/8 → 9/4) ⇝ 19/8 | note:F4 ]",
"[ (17/8 → 3/1) ⇝ 25/8 | note:Eb3 ]",
"[ 15/8 ⇜ (9/4 → 19/8) | note:F4 ]",
"[ (9/4 → 3/1) ⇝ 13/4 | note:G3 ]",
"[ (19/8 → 3/1) ⇝ 27/8 | note:Bb3 ]",
"[ 17/8 ⇜ (3/1 → 25/8) | note:Eb3 ]",
"[ 9/4 ⇜ (3/1 → 13/4) | note:G3 ]",
"[ 19/8 ⇜ (3/1 → 27/8) | note:Bb3 ]",
"[ 9/4 ⇜ (3/1 → 25/8) ⇝ 13/4 | note:G3 ]",
"[ 19/8 ⇜ (3/1 → 25/8) ⇝ 27/8 | note:Bb3 ]",
"[ 3/1 → 7/2 | note:Eb3 ]",
"[ 9/4 ⇜ (25/8 → 13/4) | note:G3 ]",
"[ 19/8 ⇜ (25/8 → 13/4) ⇝ 27/8 | note:Bb3 ]",
"[ 25/8 → 29/8 | note:G3 ]",
"[ 19/8 ⇜ (13/4 → 27/8) | note:Bb3 ]",
"[ 13/4 → 15/4 | note:Bb3 ]",
"[ 27/8 → 31/8 | note:D4 ]",
"[ 7/2 → 4/1 | note:G3 ]",
@@ -3762,8 +3658,6 @@ exports[`runs examples > example "fast" example index 0 1`] = `
exports[`runs examples > example "fastChunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | color:red note:0 ]",
"[ 1/4 → 1/2 | color:red note:1 ]",
"[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]",
@@ -3772,8 +3666,6 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
"[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]",
"[ 5/2 → 11/4 | color:red note:2 ]",
"[ 11/4 → 3/1 | color:red note:3 ]",
"[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]",
@@ -6682,27 +6574,6 @@ exports[`runs examples > example "note" example index 2 1`] = `
]
`;
exports[`runs examples > example "note" example index 3 1`] = `
[
"[ 0/1 → 1/4 | note:fbb1 s:saw ]",
"[ 1/4 → 1/2 | note:a#0 s:saw ]",
"[ 1/2 → 3/4 | note:cbbb-1 s:saw ]",
"[ 3/4 → 1/1 | note:e##-2 s:saw ]",
"[ 1/1 → 5/4 | note:fbb1 s:saw ]",
"[ 5/4 → 3/2 | note:a#0 s:saw ]",
"[ 3/2 → 7/4 | note:cbbb-1 s:saw ]",
"[ 7/4 → 2/1 | note:e##-2 s:saw ]",
"[ 2/1 → 9/4 | note:fbb1 s:saw ]",
"[ 9/4 → 5/2 | note:a#0 s:saw ]",
"[ 5/2 → 11/4 | note:cbbb-1 s:saw ]",
"[ 11/4 → 3/1 | note:e##-2 s:saw ]",
"[ 3/1 → 13/4 | note:fbb1 s:saw ]",
"[ 13/4 → 7/2 | note:a#0 s:saw ]",
"[ 7/2 → 15/4 | note:cbbb-1 s:saw ]",
"[ 15/4 → 4/1 | note:e##-2 s:saw ]",
]
`;
exports[`runs examples > example "nrpnn" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
@@ -8991,108 +8862,6 @@ exports[`runs examples > example "scale" example index 2 1`] = `
]
`;
exports[`runs examples > example "scale" example index 3 1`] = `
[
"[ 0/1 → 1/4 | note:C3 s:piano ]",
"[ 0/1 → 1/4 | note:B3 s:piano ]",
"[ 1/4 → 3/8 | note:Gb2 s:piano ]",
"[ 3/8 → 1/2 | note:F2 s:piano ]",
"[ 1/2 → 3/4 | note:A2 s:piano ]",
"[ 1/2 → 3/4 | note:D4 s:piano ]",
"[ 3/4 → 1/1 | note:G3 s:piano ]",
"[ 1/1 → 5/4 | note:C3 s:piano ]",
"[ 1/1 → 5/4 | note:C4 s:piano ]",
"[ 5/4 → 11/8 | note:Gb2 s:piano ]",
"[ 11/8 → 3/2 | note:E2 s:piano ]",
"[ 3/2 → 7/4 | note:A2 s:piano ]",
"[ 3/2 → 7/4 | note:Eb4 s:piano ]",
"[ 7/4 → 2/1 | note:F#3 s:piano ]",
"[ 2/1 → 9/4 | note:C3 s:piano ]",
"[ 2/1 → 9/4 | note:B3 s:piano ]",
"[ 9/4 → 19/8 | note:Gb2 s:piano ]",
"[ 19/8 → 5/2 | note:F2 s:piano ]",
"[ 5/2 → 11/4 | note:Ab2 s:piano ]",
"[ 5/2 → 11/4 | note:D4 s:piano ]",
"[ 11/4 → 3/1 | note:G3 s:piano ]",
"[ 3/1 → 13/4 | note:C3 s:piano ]",
"[ 3/1 → 13/4 | note:C4 s:piano ]",
"[ 13/4 → 27/8 | note:Gb2 s:piano ]",
"[ 27/8 → 7/2 | note:E2 s:piano ]",
"[ 7/2 → 15/4 | note:Ab2 s:piano ]",
"[ 7/2 → 15/4 | note:Eb4 s:piano ]",
"[ 15/4 → 4/1 | note:F#3 s:piano ]",
]
`;
exports[`runs examples > example "scale" example index 4 1`] = `
[
"[ 0/1 → 1/16 | note:Gb1 ]",
"[ 1/16 → 1/8 | note:Gb1 ]",
"[ 1/8 → 3/16 | note:Gb1 ]",
"[ 3/16 → 1/4 | note:Gb1 ]",
"[ 1/4 → 5/16 | note:Gb1 ]",
"[ 5/16 → 3/8 | note:Gb1 ]",
"[ 3/8 → 7/16 | note:Gb1 ]",
"[ 7/16 → 1/2 | note:Gb1 ]",
"[ 1/2 → 9/16 | note:Gb1 ]",
"[ 9/16 → 5/8 | note:Gb1 ]",
"[ 5/8 → 11/16 | note:Gb1 ]",
"[ 11/16 → 3/4 | note:Gb1 ]",
"[ 3/4 → 13/16 | note:Gb1 ]",
"[ 13/16 → 7/8 | note:Gb1 ]",
"[ 7/8 → 15/16 | note:Gb1 ]",
"[ 15/16 → 1/1 | note:Gb1 ]",
"[ 1/1 → 17/16 | note:Cb3 ]",
"[ 17/16 → 9/8 | note:Cb3 ]",
"[ 9/8 → 19/16 | note:Cb3 ]",
"[ 19/16 → 5/4 | note:Cb3 ]",
"[ 5/4 → 21/16 | note:Cb3 ]",
"[ 21/16 → 11/8 | note:Cb3 ]",
"[ 11/8 → 23/16 | note:Cb3 ]",
"[ 23/16 → 3/2 | note:Cb3 ]",
"[ 3/2 → 25/16 | note:Cb3 ]",
"[ 25/16 → 13/8 | note:Cb3 ]",
"[ 13/8 → 27/16 | note:Cb3 ]",
"[ 27/16 → 7/4 | note:Cb3 ]",
"[ 7/4 → 29/16 | note:Cb3 ]",
"[ 29/16 → 15/8 | note:Cb3 ]",
"[ 15/8 → 31/16 | note:Cb3 ]",
"[ 31/16 → 2/1 | note:Cb3 ]",
"[ 2/1 → 33/16 | note:Eb4 ]",
"[ 33/16 → 17/8 | note:Eb4 ]",
"[ 17/8 → 35/16 | note:Eb4 ]",
"[ 35/16 → 9/4 | note:Eb4 ]",
"[ 9/4 → 37/16 | note:Eb4 ]",
"[ 37/16 → 19/8 | note:Eb4 ]",
"[ 19/8 → 39/16 | note:Eb4 ]",
"[ 39/16 → 5/2 | note:Eb4 ]",
"[ 5/2 → 41/16 | note:Eb4 ]",
"[ 41/16 → 21/8 | note:Eb4 ]",
"[ 21/8 → 43/16 | note:Eb4 ]",
"[ 43/16 → 11/4 | note:Eb4 ]",
"[ 11/4 → 45/16 | note:Eb4 ]",
"[ 45/16 → 23/8 | note:Eb4 ]",
"[ 23/8 → 47/16 | note:Eb4 ]",
"[ 47/16 → 3/1 | note:Eb4 ]",
"[ 3/1 → 49/16 | note:Db2 ]",
"[ 49/16 → 25/8 | note:Db2 ]",
"[ 25/8 → 51/16 | note:Db2 ]",
"[ 51/16 → 13/4 | note:Db2 ]",
"[ 13/4 → 53/16 | note:Db2 ]",
"[ 53/16 → 27/8 | note:Db2 ]",
"[ 27/8 → 55/16 | note:Db2 ]",
"[ 55/16 → 7/2 | note:Db2 ]",
"[ 7/2 → 57/16 | note:Db2 ]",
"[ 57/16 → 29/8 | note:Db2 ]",
"[ 29/8 → 59/16 | note:Db2 ]",
"[ 59/16 → 15/4 | note:Db2 ]",
"[ 15/4 → 61/16 | note:Db2 ]",
"[ 61/16 → 31/8 | note:Db2 ]",
"[ 31/8 → 63/16 | note:Db2 ]",
"[ 63/16 → 4/1 | note:Db2 ]",
]
`;
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@@ -11789,112 +11558,6 @@ exports[`runs examples > example "vowel" example index 1 1`] = `
]
`;
exports[`runs examples > example "warp" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
]
`;
exports[`runs examples > example "warpmode" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
"[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
]
`;
exports[`runs examples > example "wchoose" example index 0 1`] = `
[
"[ 0/1 → 1/5 | note:c2 s:sine ]",
@@ -12099,128 +11762,6 @@ exports[`runs examples > example "withValue" example index 0 1`] = `
]
`;
exports[`runs examples > example "wt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
]
`;
exports[`runs examples > example "wtphaserand" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/16 → 1/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/8 → 3/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/16 → 1/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/4 → 5/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/16 → 3/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/8 → 7/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 7/16 → 1/2 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/2 → 9/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 9/16 → 5/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/8 → 11/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 11/16 → 3/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/4 → 13/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 13/16 → 7/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 7/8 → 15/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 15/16 → 1/1 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 1/1 → 17/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 17/16 → 9/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 9/8 → 19/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 19/16 → 5/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 5/4 → 21/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 21/16 → 11/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 11/8 → 23/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 23/16 → 3/2 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 3/2 → 25/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 25/16 → 13/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 13/8 → 27/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 27/16 → 7/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 7/4 → 29/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 29/16 → 15/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 15/8 → 31/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 31/16 → 2/1 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 2/1 → 33/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 33/16 → 17/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 17/8 → 35/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 35/16 → 9/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 9/4 → 37/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 37/16 → 19/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 19/8 → 39/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 39/16 → 5/2 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 5/2 → 41/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 41/16 → 21/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 21/8 → 43/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 43/16 → 11/4 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 11/4 → 45/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 45/16 → 23/8 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 23/8 → 47/16 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 47/16 → 3/1 | s:basique bank:wt_digital wtphaserand:0 ]",
"[ 3/1 → 49/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 49/16 → 25/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 25/8 → 51/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 51/16 → 13/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 13/4 → 53/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 53/16 → 27/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 27/8 → 55/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 55/16 → 7/2 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 7/2 → 57/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 57/16 → 29/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 29/8 → 59/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 59/16 → 15/4 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 15/4 → 61/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 61/16 → 31/8 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 31/8 → 63/16 | s:basique bank:wt_digital wtphaserand:1 ]",
"[ 63/16 → 4/1 | s:basique bank:wt_digital wtphaserand:1 ]",
]
`;
exports[`runs examples > example "xfade" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh gain:0 ]",
-5
View File
@@ -1,5 +0,0 @@
The FontStruction “TIC-80 wide font”
(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed
under a Creative Commons CC0 Public Domain Dedication license
(http://creativecommons.org/publicdomain/zero/1.0/).
[ancestry]
-16
View File
@@ -1,16 +0,0 @@
The font file in this archive was created using Fontstruct the free, online
font-building tool.
This font was created by “nesbox”.
This font has a homepage where this archive and other versions may be found:
https://fontstruct.com/fontstructions/show/1388526
[ancestry]
Try Fontstruct at https://fontstruct.com
Its easy and its fun.
Fontstruct is copyright ©2017-2025 Rob Meek
LEGAL NOTICE:
In using this font you must comply with the licensing terms described in the
file “license.txt” included with this archive.
If you redistribute the font file in this archive, it must be accompanied by all
the other files from this archive, including this one.
Binary file not shown.
-28
View File
@@ -1,28 +0,0 @@
{
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/",
"wt_digital": [
"wt_digital/wt_bad_day.wav",
"wt_digital/wt_basique.wav",
"wt_digital/wt_crickets.wav",
"wt_digital/wt_curses.wav",
"wt_digital/wt_echoes.wav"
],
"wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"],
"wt_digital_basique": ["wt_digital/wt_basique.wav"],
"wt_digital_crickets": ["wt_digital/wt_crickets.wav"],
"wt_digital_curses": ["wt_digital/wt_curses.wav"],
"wt_digital_echoes": ["wt_digital/wt_echoes.wav"],
"wt_vgame": [
"wt_vgame/wt_vgame10.wav",
"wt_vgame/wt_vgame11.wav",
"wt_vgame/wt_vgame12.wav",
"wt_vgame/wt_vgame13.wav",
"wt_vgame/wt_vgame14.wav",
"wt_vgame/wt_vgame15.wav",
"wt_vgame/wt_vgame16.wav",
"wt_vgame/wt_vgame17.wav",
"wt_vgame/wt_vgame18.wav",
"wt_vgame/wt_vgame19.wav",
"wt_vgame/wt_vgame20.wav"
]
}
@@ -74,7 +74,6 @@ const fontFamilyOptions = {
FiraCode: 'FiraCode',
'FiraCode-SemiBold': 'FiraCode SemiBold',
teletext: 'teletext',
tic80: 'tic80',
mode7: 'mode7',
BigBlueTerminal: 'BigBlueTerminal',
x3270: 'x3270',
@@ -44,9 +44,6 @@ export function SoundsTab() {
if (soundsFilter === soundFilterType.SYNTHS) {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
if (soundsFilter === soundFilterType.WAVETABLES) {
return filtered.filter(([_, { data }]) => data.type === 'wavetable');
}
//TODO: tidy this up, it does not need to be saved in settings
if (soundsFilter === 'importSounds') {
return [];
@@ -57,9 +54,6 @@ export function SoundsTab() {
// holds mutable ref to current triggered sound
const trigRef = useRef();
// Used to cycle through sound previews on banks with multiple sounds
let soundPreviewIdx = 0;
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
@@ -80,7 +74,6 @@ export function SoundsTab() {
samples: 'samples',
drums: 'drum-machines',
synths: 'Synths',
wavetables: 'Wavetables',
user: 'User',
importSounds: 'import-sounds',
}}
@@ -117,13 +110,11 @@ export function SoundsTab() {
const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name,
n: soundPreviewIdx,
clip: 1,
release: 0.5,
sustain: 1,
duration: 0.5,
};
soundPreviewIdx++;
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
@@ -135,7 +126,6 @@ export function SoundsTab() {
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span>
);
+8 -2
View File
@@ -1,4 +1,4 @@
import { registerSampleSource } from '@strudel/webaudio';
import { registerSound, onTriggerSample } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core';
@@ -76,7 +76,13 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
})
.map((title) => titlePathMap.get(title));
registerSampleSource(key, value, { prebake: false });
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: undefined,
});
});
logger('imported sounds registered!', 'success');
-3
View File
@@ -32,9 +32,6 @@ export async function prebake() {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
prebake: true,
}),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
+4 -4
View File
@@ -285,7 +285,7 @@ stack(
.mul(gain(sine.struct("x*8").add(3/5).mul(2/5).fast(8)))
.piano()`;
// iter, echo, echoWith
// iter, echo, echoForEach
export const undergroundPlumber = `// "Underground plumber"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
@@ -307,7 +307,7 @@ stack(
.cutoff(400).decay(.12).sustain(0)
,
"[g2,[c3 eb3]]".iter(4)
.echoWith(4, 1/8, (x,n)=>x.transpose(n*12).gain(Math.pow(.4,n)))
.echoForEach(4, 1/8, (x,n)=>x.transpose(n*12).gain(Math.pow(.4,n)))
.note().layer(h)
.clip(.1)
)
@@ -432,7 +432,7 @@ export const festivalOfFingers3 = `// "Festival of fingers 3"
setcps(1)
n("[-7*3],0,2,6,[8 7]")
.echoWith(
.echoForEach(
4, // echo 4 times
1/4, // 1/4s between echos
(x,i)=>x
@@ -485,7 +485,7 @@ stack(
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
.echoForEach(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush()
)
.slow(3/2)`;
-1
View File
@@ -13,7 +13,6 @@ export const soundFilterType = {
DRUMS: 'drums',
SAMPLES: 'samples',
SYNTHS: 'synths',
WAVETABLES: 'wavetables',
ALL: 'all',
};
-5
View File
@@ -50,11 +50,6 @@
src: url('/fonts/teletext/EuropeanTeletext.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'tic80';
src: url('/fonts/tic80/tic-80-wide-font.otf');
size-adjust: 60%;
}
@font-face {
font-family: 'mode7';
src: url('/fonts/mode7/MODE7GX3.TTF');