diff --git a/.forgejo/workflows/deploy-hot.yml b/.forgejo/workflows/deploy-hot.yml index 6c8205ae7..a19917dfa 100644 --- a/.forgejo/workflows/deploy-hot.yml +++ b/.forgejo/workflows/deploy-hot.yml @@ -13,10 +13,11 @@ jobs: build: if: ${{ github.event.label.name == 'serve-hot' }} runs-on: docker + container: node:22-bookworm steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} - uses: pnpm/action-setup@v4 with: version: 9.12.2 @@ -41,4 +42,4 @@ jobs: ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts rsync -atv --delete --delete-after --progress \ ./website/dist/ \ - strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc \ No newline at end of file + strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml index 0265fcebd..264f716d0 100644 --- a/.forgejo/workflows/deploy-warm-beta.yml +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -13,6 +13,7 @@ concurrency: jobs: build: runs-on: docker + container: node:22-bookworm env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} steps: @@ -37,4 +38,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc \ No newline at end of file + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index e102618e4..2b47e5e17 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -10,6 +10,7 @@ concurrency: jobs: build: runs-on: docker + container: node:22-bookworm env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} steps: @@ -34,4 +35,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc \ No newline at end of file + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 6b3759571..c1db1c5f9 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -5,6 +5,7 @@ on: [push, pull_request] jobs: build: runs-on: docker + container: node:22-bookworm strategy: matrix: node-version: [20] diff --git a/examples/codemirror-repl/main.js b/examples/codemirror-repl/main.js index 5b5f20289..2a3673758 100644 --- a/examples/codemirror-repl/main.js +++ b/examples/codemirror-repl/main.js @@ -16,7 +16,7 @@ const drawContext = canvas.getContext('2d'); const drawTime = [-2, 2]; // time window of drawn haps const editor = new StrudelMirror({ - defaultOutput: webaudioOutput, + onSchedule: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, root: document.getElementById('editor'), diff --git a/examples/minimal-repl/main.js b/examples/minimal-repl/main.js index aa3bc7cf4..205e9f439 100644 --- a/examples/minimal-repl/main.js +++ b/examples/minimal-repl/main.js @@ -12,7 +12,7 @@ registerSynthSounds(); evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal')); const { evaluate } = repl({ - defaultOutput: webaudioOutput, + onSchedule: webaudioOutput, getTime: () => ctx.currentTime, transpiler, }); diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index d4e05393c..e6d5c89e5 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -118,7 +118,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * @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") + * s("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1") */ export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition'); @@ -1988,11 +1988,10 @@ export const { spread } = registerControl('spread'); * Set dryness of reverb. See `room` and `size` for more information about reverb. * * @name dry - * @tags superdirt + * @tags superdirt, superdough * @param {number | Pattern} dry 0 = wet, 1 = dry * @example * n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc() - * @superdirtOnly * */ export const { dry } = registerControl('dry'); @@ -2226,7 +2225,7 @@ export const { orbit } = registerControl('orbit', 'o'); * otherPat.bmod(..) (to modulate another pattern with the bus) * * @name bus - * @tags superdirt + * @tags superdough * @param {number | Pattern} number */ export const { bus } = registerControl('bus'); @@ -2235,7 +2234,7 @@ export const { bus } = registerControl('bus'); * Postgain multiplier prior to sending the signal to the audio bus. * * @name busgain - * @tags superdirt + * @tags superdough * @synonyms bgain * @param {number | Pattern} number */ @@ -2377,7 +2376,7 @@ export const { mode } = registerControl(['mode', 'anchor']); * * @name room * @tags orbit, superdough - * @param {number | Pattern} level between 0 and 1 + * @param {number | Pattern} level * @example * s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>") * @example diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 59e410c53..cc68bb102 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -10,6 +10,7 @@ import { errorLogger, logger } from './logger.mjs'; export class Cyclist { constructor({ interval, + onSchedule, onTrigger, onToggle, onError, @@ -19,6 +20,11 @@ export class Cyclist { clearInterval, beforeStart, }) { + if (onTrigger) { + throw new Error( + 'onTrigger has been dropped. use onSchedule(hap, time, cps) instead, see https://codeberg.org/uzu/strudel/pulls/2138', + ); + } this.started = false; this.beforeStart = beforeStart; this.cps = 0.5; @@ -63,12 +69,7 @@ export class Cyclist { if (hap.hasOnset()) { const targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; - const duration = hap.duration / this.cps; - // the following line is dumb and only here for backwards compatibility - // see https://codeberg.org/uzu/strudel/pulls/1004 - const deadline = targetTime - phase; - // this onTrigger has another signature - onTrigger?.(hap, deadline, duration, this.cps, targetTime); + onSchedule?.(hap, targetTime, this.cps); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { this.cps = hap.value.cps; this.num_ticks_since_cps_change = 0; diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 49f96384b..eba3f3303 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -59,6 +59,11 @@ export const bjorklund = function (ons, steps) { * really useful for computer/algorithmic music because they can * describe a large number of rhythms with a couple of numbers. * + * A negative number of pulses inverts the pattern + * so pulses happen in the gaps of the normal euclidean rhythm. + * + * A negative number of steps changes it into `euclidLegato`. + * * @memberof Pattern * @name euclid * @tags temporal @@ -137,8 +142,38 @@ const _euclidRot = function (pulses, steps, rotation) { return b; }; +const _euclidLegato = function (pulses, steps, rotation, pat) { + var bin_pat = _euclidRot(pulses, steps, 0); + // Because the `gapless` algorithm logic assumes the first entry in bin_pat is a 1, + // we rotate the pattern so the first onset is at the start. + const firstOnset = bin_pat.indexOf(1); + bin_pat = rotate(bin_pat, firstOnset); + steps = Math.abs(steps); + const gapless = bin_pat + .join('') + .split('1') + .slice(1) + .map((s) => [s.length + 1, true]); + return pat.struct(timeCat(...gapless).late(Fraction(rotation + firstOnset).div(steps))); +}; + +const _euclid = function (pulses, steps, rotation, pat) { + if (steps === 0 || pulses === 0) { + return silence; + } + const cycles = Math.ceil(Math.abs(pulses) / Math.abs(steps)); + const totalSteps = cycles * steps; + if (totalSteps < 0) { + return _euclidLegato(pulses, -totalSteps, rotation, pat.fast(cycles)).slow(cycles); + } + return pat + .fast(cycles) + .struct(_euclidRot(pulses, totalSteps, rotation)) + .slow(cycles); +}; + export const euclid = register('euclid', function (pulses, steps, pat) { - return pat.struct(_euclidRot(pulses, steps, 0)); + return _euclid(pulses, steps, 0, pat); }); export const bjork = register('bjork', function (euc, pat) { @@ -146,16 +181,18 @@ export const bjork = register('bjork', function (euc, pat) { euc = [euc]; } const [pulses, steps = pulses, rot = 0] = euc; - return pat.struct(_euclidRot(pulses, steps, rot)); + return _euclid(pulses, steps, rot, pat); }); export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { - return pat.struct(_euclidRot(pulses, steps, rotation)); + return _euclid(pulses, steps, rotation, pat); }); /** * Similar to `euclid`, but each pulse is held until the next pulse, * so there will be no gaps. + * + * A negative number of steps changes it into `euclid`. * @name euclidLegato * @memberof Pattern * @tags temporal @@ -167,27 +204,16 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun * note("c3").euclidLegato(3,8) */ -const _euclidLegato = function (pulses, steps, rotation, pat) { - if (pulses < 1) { - return silence; - } - const bin_pat = _euclidRot(pulses, steps, 0); - const gapless = bin_pat - .join('') - .split('1') - .slice(1) - .map((s) => [s.length + 1, true]); - return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps)); -}; - export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { - return _euclidLegato(pulses, steps, 0, pat); + return _euclid(pulses, -steps, 0, pat); }); /** * Similar to `euclid`, but each pulse is held until the next pulse, * so there will be no gaps, and has an additional parameter for 'rotating' - * the resulting sequence + * the resulting sequence. + * + * A negative number of steps changes it into `euclidRot`. * @name euclidLegatoRot * @memberof Pattern * @tags temporal @@ -198,7 +224,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * note("c3").euclidLegatoRot(3,5,2) */ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { - return _euclidLegato(pulses, steps, rotation, pat); + return _euclid(pulses, -steps, rotation, pat); }); /** diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 3e412074d..52ef698bb 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -8,7 +8,12 @@ import { logger } from './logger.mjs'; import { ClockCollator, cycleToSeconds } from './util.mjs'; export class NeoCyclist { - constructor({ onTrigger, onToggle, getTime }) { + constructor({ onSchedule, onToggle, getTime, onTrigger }) { + if (onTrigger) { + throw new Error( + 'onTrigger has been dropped. use onSchedule(hap, time, cps) instead, see https://codeberg.org/uzu/strudel/pulls/2138', + ); + } this.started = false; this.cps = 0.5; this.getTime = getTime; // get absolute time @@ -43,8 +48,7 @@ export class NeoCyclist { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); const targetTime = timeUntilTrigger + currentTime + this.latency; - const duration = cycleToSeconds(hap.duration, this.cps); - onTrigger?.(hap, 0, duration, this.cps, targetTime); + onSchedule?.(hap, targetTime, this.cps); } }); }; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 0f5083901..d6f6e5986 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -27,6 +27,7 @@ import { import drawLine from './drawLine.mjs'; import { errorLogger, logger } from './logger.mjs'; import { strudelScope } from './evaluate.mjs'; +import { getClockTime } from './schedulerState.mjs'; let stringParser; @@ -504,10 +505,25 @@ export class Pattern { /** * Returns a new pattern with the given function applied to the list of haps returned by every query. + * + * As an end user, you will most likely not need to use this; look at `withContext`, `withHapSpan`, etc. instead. * @tags internals * @param {Function} func * @returns Pattern * @noAutocomplete + * @example + * note("[a b, c d] a b c d").s("gm_piano").withHaps(x=>{ + * if (x.length > 1) { + * x = x.map(hap => hap.withValue(val => { + * return { + * n: 1, + * // select gm_piano:1 sample + * ...val + * } + * })); + * } + * return x + * }) */ withHaps(func) { const result = new Pattern((state) => func(this.query(state), state)); @@ -517,10 +533,23 @@ export class Pattern { /** * As with `withHaps`, but applies the function to every hap, rather than every list of haps. + * + * As an end user, you will most likely not need to use this; look at `withContext`, `withHapSpan`, etc. instead. * @tags internals * @param {Function} func * @returns Pattern * @noAutocomplete + * @example + * note("c a f e").withHap(x=>{ + * if (x.value.note == 'c') { + * x.value.s = 'gm_piano' + * } + * else { + * x.value.s = 'gm_piano' + * x.value.n = 2 + * } + * return x + * }) */ withHap(func) { return this.withHaps((haps) => haps.map(func)); @@ -850,6 +879,26 @@ export class Pattern { return stack(this, ...pats); } + stackLeft(...pats) { + return stackLeft(this, ...pats); + } + + stackRight(...pats) { + return stackRight(this, ...pats); + } + + stackCenter(...pats) { + return stackCenter(this, ...pats); + } + + stackCentre(...pats) { + return stackCentre(this, ...pats); + } + + stackRepeat(...pats) { + return stackRepeat(this, ...pats); + } + sequence(...pats) { return sequence(this, ...pats); } @@ -872,23 +921,41 @@ export class Pattern { ////////////////////////////////////////////////////////////////////// // Context methods - ones that deal with metadata - onTrigger(onTrigger, dominant = true) { + /** + * Attaches a function that is called for each hap onset. + * @name onSchedule + * @memberof Pattern + * @example + * s("bd sd").onSchedule((hap, time, cps) => { + * console.log('hap scheduled', time, cps); + * }) + */ + onSchedule(onSchedule, dominant = true) { return this.withHap((hap) => hap.setContext({ ...hap.context, - onTrigger: (...args) => { + onSchedule: (scheduleHap, t, cps) => { // run previously set trigger, if it exists - hap.context.onTrigger?.(...args); - onTrigger(...args); + hap.context.onSchedule?.(scheduleHap, t, cps); + onSchedule(scheduleHap, t, cps); }, - // if dominantTrigger is set to true, the default output (webaudio) will be disabled + // if dominantSchedule is set to true, the default output (webaudio) will be disabled // when using multiple triggers, you cannot flip this flag to false again! // example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same - dominantTrigger: hap.context.dominantTrigger || dominant, + dominantSchedule: hap.context.dominantSchedule || dominant, }), ); } + // legacy: + onTrigger(onTrigger, dominant = true) { + console.warn('onTrigger is deprecated.. use onSchedule(hap, time, cps) instead'); + return this.onSchedule((hap, t, cps) => { + const currentTime = getClockTime(); + return onTrigger(hap, currentTime, cps, t); + }, dominant); + } + /** * Writes the content of the current event to the console (visible in the side menu). * @tags visualization @@ -898,7 +965,7 @@ export class Pattern { * s("bd sd").log() */ log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { - return this.onTrigger((...args) => { + return this.onSchedule((...args) => { logger(func(...args), undefined, getData(...args)); }, false); } @@ -1074,7 +1141,7 @@ const COMPOSERS = { * @param {Pattern} pat * @returns {Pattern} * @memberof Pattern - * @tags internal, combiners + * @tags internals, combiners * @example * // because input pattern has `s` set, * // it overrides the "sine" declared earlier @@ -1099,7 +1166,7 @@ const COMPOSERS = { * @param {Pattern} pat * @memberof Pattern * @returns {Pattern} - * @tags internal, combiners + * @tags internals, combiners * @example * // notes, already defined, will stay "c a f e", * // while "s", not defined, will be set to "piano" @@ -1156,24 +1223,163 @@ const COMPOSERS = { * @tags math */ div: [numeralArgs((a, b) => a / b)], + /** + * + * Divides each number by the given factor and returns the remainder + * @name mod + * @memberof Pattern + * @tags math + */ mod: [numeralArgs(_mod)], + /** + * + * Raises each number to the power of the given factor + * @name pow + * @memberof Pattern + * @tags math + */ pow: [numeralArgs(Math.pow)], + /** + * + * Performs a bitwise `and` on each number and the given factor + * @name band + * @memberof Pattern + * @tags math + */ band: [numeralArgs((a, b) => a & b)], + /** + * + * Performs a bitwise `or` on each number and the given factor + * @name bor + * @memberof Pattern + * @tags math + */ bor: [numeralArgs((a, b) => a | b)], + /** + * + * Performs a bitwise `xor` on each number and the given factor + * @name bxor + * @memberof Pattern + * @tags math + */ bxor: [numeralArgs((a, b) => a ^ b)], + /** + * + * Performs a bitwise `left shift` on each number by the given factor + * @name blshift + * @memberof Pattern + * @tags math + */ blshift: [numeralArgs((a, b) => a << b)], + /** + * + * Performs a bitwise `right shift` on each number by the given factor + * @name brshift + * @memberof Pattern + * @tags math + */ brshift: [numeralArgs((a, b) => a >> b)], // TODO - force numerical comparison if both look like numbers? + /** + * performs a < b + * @name lt + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ lt: [(a, b) => a < b], + /** + * performs a > b + * @name gt + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ gt: [(a, b) => a > b], + /** + * performs a <= b + * @name lte + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ lte: [(a, b) => a <= b], + /** + * performs a >= b + * @name gte + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ gte: [(a, b) => a >= b], + /** + * performs a == b + * @name eq + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ eq: [(a, b) => a == b], + /** + * performs a === b (strictly equal) + * @name eqt + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ eqt: [(a, b) => a === b], + /** + * performs a != b + * @name ne + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ ne: [(a, b) => a != b], + /** + * performs a !== b (strictly not equal) + * @name net + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ net: [(a, b) => a !== b], + /** + * performs a && b + * @name and + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ and: [(a, b) => a && b], + /** + * performs a || b + * @name or + * @memberof Pattern + * @tags math + * @param {any} a + * @param {any} b + * @returns Pattern with boolean value(s) + */ or: [(a, b) => a || b], // bitwise ops @@ -1449,7 +1655,19 @@ export function sequenceP(pats) { export function stack(...pats) { // Array test here is to avoid infinite recursions.. pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); - const query = (state) => flatten(pats.map((pat) => pat.query(state))); + const query = (state) => + flatten( + pats.map((pat) => { + try { + // An error occurring in one part of the stack should not break + // playback of the other parts of the stack. + return pat.query(state); + } catch (e) { + errorLogger(e); + return []; + } + }), + ); const result = new Pattern(query); if (__steps) { result._steps = lcm(...pats.map((pat) => pat._steps)); @@ -1470,6 +1688,18 @@ function _stackWith(func, pats) { return stack(...func(steps, pats)); } +/** + * The input patterns are aligned to the left, with silence padded to the shorter pattern(s) in the final steps if there's a stepcount disparity. See also `stackRight` and `stackCenter/stackCentre` + * + * @name stackLeft + * @tags temporal, stepwise, combiners + * @return {Pattern} + * @example + * stackLeft("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + * @example + * // chainable + * "0 1 2 3".stackLeft("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + */ export function stackLeft(...pats) { return _stackWith( (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(pat, gap(steps.sub(pat._steps))))), @@ -1477,6 +1707,18 @@ export function stackLeft(...pats) { ); } +/** + * The input patterns are aligned to the right, with silence padded to the shorter pattern(s) in the initial steps if there's a stepcount disparity. See also `stackLeft` and `stackCenter/stackCentre` + * + * @name stackRight + * @tags temporal, stepwise, combiners + * @return {Pattern} + * @example + * stackRight("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + * @example + * // chainable + * "0 1 2 3".stackRight("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + */ export function stackRight(...pats) { return _stackWith( (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(gap(steps.sub(pat._steps)), pat))), @@ -1484,7 +1726,19 @@ export function stackRight(...pats) { ); } -export function stackCentre(...pats) { +/** + * The input patterns are aligned to the right, with silence padded to the shorter pattern(s) in the initial steps if there's a stepcount disparity. See also `stackLeft` and `stackRight` + * @name stackCenter + * @tags temporal, stepwise, combiners + * @synonyms stackCentre + * @return {Pattern} + * @example + * stackCenter("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + * @example + * // chainable + * "0 1 2 3".stackCenter("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + */ +export function stackCenter(...pats) { return _stackWith( (steps, pats) => pats.map((pat) => { @@ -1498,6 +1752,25 @@ export function stackCentre(...pats) { ); } +export function stackCentre(...pats) { + return stackCenter(...pats); +} + +/** + * Shorthand for `pm(patterns).pace(m)` where m is the max step count in the input patterns + * @name stackRepeat + * @tags temporal, stepwise, combiners + * @return {Pattern} + * @example + * stackRepeat("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + * @example + * // chainable + * "0 1 2 3".stackRepeat("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note() + */ +export function stackRepeat(...pats) { + return stackBy(reify('repeat'), ...pats); +} + export function stackBy(by, ...pats) { const [left, ...right] = pats.map((pat) => pat._steps); const steps = left.maximum(...right); @@ -1612,10 +1885,10 @@ export function arrange(...sections) { * @tags combiners * @return {Pattern} * @example -seqPLoop( - [0, 2, "bd(3,8)"], - [1, 3, "cp(3,8)"] -).sound() + * seqPLoop( + * [0, 2, "bd(3,8)"], + * [1, 3, "cp(3,8)"] + * ).sound() */ export function seqPLoop(...parts) { let total = Fraction(0); @@ -2292,11 +2565,11 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a * @param {number} slices to play * @example * note("0 1 2 3 4 5 6 7".scale('c:mixolydian')) - *.bite(4, "3 2 1 0") + * .bite(4, "3 2 1 0") * @example * sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]") - .bank("RolandTR909").speed(1.2) - .bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3") + * .bank("RolandTR909").speed(1.2) + * .bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3") */ export const bite = register( 'bite', @@ -2313,6 +2586,74 @@ export const bite = register( false, ); +/** + * Similar to `struct`, but every hap in the structure pattern plays the next + * cycle of the main pattern. + * With a different number of cycles in the main pattern and haps in the + * structure pattern, this creates an isorhythm. + * + * Note: This function works on the assumption that the structure pattern + * repeats every cycle. If the structure pattern only repeats after multiple + * cycles, it may skip or repeat certain parts of the melody. + * @tags temporal + * @name isobite + * @synonyms cycleStruct, cyclestruct + * @memberof Pattern + * @param {Pattern} structure structure of the pattern + * @example + * "<0 1 2 3 4 5>".isobite("[x - x*2]*2").n().scale("c:major") + * // the same as "0 - [1 2] 3 - [4 5]".n().scale("c:major") + * + * @example + * "<0 1 2 3 4>".isobite("x - x").n().scale("c:major") + * // the same as "<[0 - 1] [2 - 3] [4 - 0] [1 - 2] [3 - 4]>".n().scale("c:major") + * + * @example + * // Here the structure pattern repeats after 2 cycles, + * // so certain haps from the melody are skipped. + * "<0 1 2 3>".isobite("x x*2".slow(2)).n().scale("c:major") + * // the same as "<[0] [2 3] [2] [2 3]>".n().scale("c:major") + * + * @example + * // This can be avoided by making the struct pattern 1 cycle, + * // and slowing the whole pattern down as follows: + * "<0 1 2 3>".isobite("x x*2").slow(2).n().scale("c:major") + * // the same as "<[0] [1 2] [3] [0 1] [2] [3 0] [1] [2 3]>".n().scale("c:major") + */ +export const { isobite, cyclestruct, cycleStruct } = register( + ['isobite', 'cyclestruct', 'cycleStruct'], + (structure, pat) => { + const structPat = new Pattern((state) => { + const haps_cycle = structure + .withQuerySpan((span) => span.begin.wholeCycle()) + .query(state) + .filter((hap) => hap.value != 0) + .sort((a, b) => a.whole.begin - b.whole.begin) + .map((hap, i) => hap.withValue((_) => i)); + + const haps = []; + for (const hap of haps_cycle) { + const new_part = state.span.intersection(hap.part); + if (new_part) { + haps.push(new Hap(hap.whole, new_part, hap.value, hap.context)); + } + } + return haps; + }).splitQueries(); + + const stepsPattern = new Pattern((state) => { + const nrHaps = structure + .withQuerySpan((span) => span.begin.wholeCycle()) + .query(state) + .filter((hap) => hap.value != 0).length; + return [new Hap(state.span.begin.wholeCycle(), state.span, nrHaps, {})]; + }).splitQueries(); + + return pat.fast(stepsPattern).bite(stepsPattern, structPat); + }, + false, +); + /** * Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle. * @tags temporal @@ -2419,8 +2760,12 @@ export const off = register('off', function (time_pat, func, pat) { * Returns a new pattern where every other cycle is played once, twice as * fast, and offset in time by one quarter of a cycle. Creates a kind of * breakbeat feel. + * @name brak * @tags temporal * @returns Pattern + * @memberof Pattern + * @example + * s("bd hh!2 rim").brak() */ export const brak = register('brak', function (pat) { return pat.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25)); @@ -2909,10 +3254,29 @@ export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) pat.early(offset).restart(pure(1).slow(cycles)), ); +/** + * Sets the pattern's color to the given [hsla](https://www.w3schools.com/htmL/html_colors_hsl.asp) + * + * @name hsla + * @param {Number} hue (0 - 360) + * @param {Number} saturation percentage (0 - 100) + * @param {Number} lightness percentage (0 - 100) + * @param {Number} alpha (transparency) (0.0 - 1.0) + * @tags visualization + */ export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); }); +/** + * Sets the pattern's color to the given [hsl](https://www.w3schools.com/htmL/html_colors_hsl.asp) + * + * @name hsl + * @param {Number} hue (0 - 360) + * @param {Number} saturation percentage (0 - 100) + * @param {Number} lightness percentage (0 - 100) + * @tags visualization + */ export const hsl = register('hsl', (h, s, l, pat) => { return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`); }); @@ -3268,7 +3632,7 @@ export const drop = stepRegister('drop', function (i, pat) { * @tags stepwise * @example * stepcat( - * sound("bd bd - cp").extend(2), + * sound("bd bd - ").extend(2), * sound("bd - sd -") * ).pace(8) */ @@ -3279,15 +3643,12 @@ export const extend = stepRegister('extend', function (factor, pat) { /** * *Experimental* * - * `replicate` is similar to `fast` in that it increases its density, but it also increases the step count - * accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas - * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`. + * `replicate` is similar to `extend`, except that `replicate` will "replicate" the thing being extended. This will only differ from `extend` when the input pattern differs from cycle to cycle. * - * TODO: find out how this function differs from extend * @tags stepwise * @example * stepcat( - * sound("bd bd - cp").replicate(2), + * sound("bd bd - ").replicate(2), * sound("bd - sd -") * ).pace(8) */ @@ -3383,8 +3744,8 @@ export const growlist = (amount, pat) => pat.growlist(amount); * "tha dhi thom nam".shrink("1 -1").sound().bank("mridangam").pace(4) * @example * note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp") - .shrink("1 -1").pace(8) - + * .shrink("1 -1").pace(8) + * */ export const shrink = register( @@ -3423,7 +3784,7 @@ export const shrink = register( * "tha dhi thom nam".grow("1 -1").sound().bank("mridangam").pace(4) * @example * note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp") - .grow("1 -1").pace(8) + * .grow("1 -1").pace(8) */ export const grow = register( @@ -3447,17 +3808,17 @@ export const grow = register( /** * *Experimental* - * - * Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list - * on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the + * + * Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list + * on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the * number of steps per cycle is therefore usually recommended. * * @tags stepwise * @return {Pattern} * @example * "[c g]".tour("e f", "e f g", "g f e c").note() - .sound("folkharp") - .pace(8) + * .sound("folkharp") + * .pace(8) */ export const tour = function (pat, ...many) { return pat.tour(...many); @@ -3475,16 +3836,16 @@ Pattern.prototype.tour = function (...many) { /** * *Experimental* - * - * 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle. + * + * 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle. * Using `pace` to set the number of steps per cycle is therefore usually recommended. - * + * * @tags stepwise * @returns {Pattern} * @example * zip("e f", "e f g", "g [f e] a f4 c").note() - .sound("folkharp") - .pace(8) + * .sound("folkharp") + * .pace(8) */ export const zip = function (...pats) { pats = pats.filter((pat) => pat.hasSteps); @@ -3647,9 +4008,9 @@ export const slice = register( * s("bd!8").onTriggerTime((hap) => {console.log(hap)}) */ Pattern.prototype.onTriggerTime = function (func) { - return this.onTrigger((hap, currentTime, _cps, targetTime) => { - const diff = targetTime - currentTime; - window.setTimeout(() => { + return this.onSchedule((hap, targetTime) => { + const diff = targetTime - getClockTime(); + setTimeout(() => { func(hap); }, diff * 1000); }, false); diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 3974c695b..35bd5fc2c 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -104,6 +104,8 @@ export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) * @param {Pattern} pat * @param {*} xs * @returns {Pattern} + * @example + * note("<0*4 1 2*2 3*3>".pickOut(["g a", "e f", "f g f g" , "g c d"])) */ export const pickOut = register('pickOut', function (lookup, pat) { return _pick(lookup, pat, false).outerJoin(); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index ff2734967..194b335c7 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -7,13 +7,15 @@ import { setIsStarted, setPattern as exposeSchedulerPattern, setTime, - setTriggerFunc, + setScheduleFunc, + setClockTime, } from './schedulerState.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; import { reset_state } from './impure.mjs'; export function repl({ + onSchedule, defaultOutput, onEvalError, beforeEval, @@ -30,6 +32,11 @@ export function repl({ id, mondo = false, }) { + if (defaultOutput) { + throw new Error( + 'defaultOutput has been removed. use onSchedule(hap, time, cps) instead. see https://codeberg.org/uzu/strudel/pulls/2138/', + ); + } const state = { schedulerError: undefined, evalError: undefined, @@ -55,7 +62,7 @@ export function repl({ }; const schedulerOptions = { - onTrigger: getTrigger({ defaultOutput, getTime }), + onSchedule: getSchedule({ onSchedule }), getTime, onToggle: (started) => { updateState({ started }); @@ -73,7 +80,7 @@ export function repl({ // NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome const scheduler = sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); - setTriggerFunc(schedulerOptions.onTrigger); + setScheduleFunc(schedulerOptions.onSchedule); setCpsFunc(() => scheduler.cps); let pPatterns = {}; let anonymousIndex = 0; @@ -224,6 +231,7 @@ export function repl({ return pattern; }; setTime(() => scheduler.now()); // TODO: refactor? + setClockTime(() => scheduler.getTime()); // TODO: refactor? // Helper function to apply pattern transformations (solo, each, all) // this should be abstracted more @@ -407,6 +415,7 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? + setClockTime(() => scheduler.getTime()); // TODO: refactor? await beforeEval?.({ code, blockBased: false }); allTransforms = []; // reset all transforms @@ -452,6 +461,7 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? + setClockTime(() => scheduler.getTime()); // TODO: refactor? await beforeEval?.({ code, blockBased: true }); allTransforms = []; // reset all transforms @@ -560,20 +570,17 @@ export function repl({ return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } -export const getTrigger = - ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 +export const getSchedule = + ({ onSchedule }) => + async (hap, t, cps) => { try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + if (!hap.context.onSchedule || !hap.context.dominantSchedule) { + await onSchedule(hap, t, cps); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); + if (hap.context.onSchedule) { + await hap.context.onSchedule(hap, t, cps); } } catch (err) { - errorLogger(err, 'getTrigger'); + errorLogger(err, 'getSchedule'); } }; diff --git a/packages/core/schedulerState.mjs b/packages/core/schedulerState.mjs index 14b4c9054..bc5b44aad 100644 --- a/packages/core/schedulerState.mjs +++ b/packages/core/schedulerState.mjs @@ -7,8 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th let time; let cpsFunc; let pattern; -let triggerFunc; +let scheduleFunc; let isStarted; + +/** + * @name getTime + * @tags internals + * @returns {Number} the current cycle time as a number + */ export function getTime() { if (!time) { throw new Error('no time set! use setTime to define a time source'); @@ -20,10 +26,32 @@ export function setTime(func) { time = func; } +let clockTime; +/** + * @name getClockTime + * @tags internals + * @returns {Number} the current clock time in seconds (onSchedule time) + */ +export function getClockTime() { + if (!clockTime) { + throw new Error('no clockTime set! use setClockTime to define a time source'); + } + return clockTime(); +} + +export function setClockTime(func) { + clockTime = func; +} + export function setCpsFunc(func) { cpsFunc = func; } +/** + * @name getCps + * @tags internals + * @returns {Number} the current cycles per second + */ export function getCps() { return cpsFunc?.(); } @@ -36,12 +64,12 @@ export function getPattern() { return pattern; } -export function setTriggerFunc(func) { - triggerFunc = func; +export function setScheduleFunc(func) { + scheduleFunc = func; } -export function getTriggerFunc() { - return triggerFunc; +export function getScheduleFunc() { + return scheduleFunc; } export function setIsStarted(val) { diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index b86f16e8a..0316c86fe 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Hap } from './hap.mjs'; -import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs'; +import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP, isPattern } from './pattern.mjs'; import { _mod } from './util.mjs'; import Fraction from './fraction.mjs'; @@ -16,6 +16,24 @@ export function steady(value) { return new Pattern((state) => [new Hap(undefined, state.span, value)]); } +/** + * Creates a signal given by the input function + * + * @name signal + * @param {*} func + * @returns {Pattern} signal + * @tags generators + * @example + * // sawtooth signal definition + * signal(t=>t%1).segment(16).range(30,40).note() + * @example + * signal(t=>Math.pow(t,2)%1).range(40, 60).add(5).segment(16) + * .off(0, x=>x.add(7)) + * .note() + * .s("gm_electric_guitar_jazz,gm_epiano") + * .gain(1.7) + * .pianoroll() + */ export const signal = (func) => { const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))]; return new Pattern(query); @@ -189,6 +207,7 @@ export const time = signal(id); /** * The mouse's x position value ranges from 0 to 1. * @name mousex + * @synonyms mouseX * @return {Pattern} * @tags external_io * @example @@ -199,6 +218,7 @@ export const time = signal(id); /** * The mouse's y position value ranges from 0 to 1. * @name mousey + * @synonyms mouseY * @return {Pattern} * @tags external_io * @example @@ -559,6 +579,9 @@ export const __chooseWith = (pat, xs) => { * @returns {Pattern} * @example * note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"])) + * @example + * // Compare with chooseInWith + * note(chooseWith("<0 0.5 1>*3", ["c3", "b3", "d3"])) */ export const chooseWith = (pat, xs) => { return __chooseWith(pat, xs).outerJoin(); @@ -570,6 +593,13 @@ export const chooseWith = (pat, xs) => { * @tags temporal * @param {Pattern} pat * @param {*} xs + * @example + * // Because the structure comes from the chosen values (the notes, here) + * // The choosing pattern will only advance once per cycle (not 3 times per cycle) + * note(chooseInWith("<0 0.5 1>*3", ["c3", "b3", "d3"])) + * @example + * // Here, the chosen values have a structure such that the choosing pattern will advance 3 times per cycle + * note(chooseInWith("<0 0.5 1>*3", ["c3*3", "b3*3", "d3*3"])) * @returns {Pattern} */ export const chooseInWith = (pat, xs) => { @@ -586,9 +616,7 @@ export const chooseInWith = (pat, xs) => { */ export const choose = (...xs) => chooseWith(rand, xs); -// todo: doc export const chooseIn = (...xs) => chooseInWith(rand, xs); -export const chooseOut = choose; /** * Chooses from the given list of values (or patterns of values), according @@ -734,12 +762,30 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed)); */ export const berlin = signal((t, controls) => _berlin(t, controls.randSeed)); -export const degradeByWith = register( - 'degradeByWith', - (withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)), - true, - true, -); +/** + * `degradeByWith` is like `degradeBy`, but with control over the random or other signal used to filter out the events. + * + * `degradeBy(x)` is essentially `degradeByWith(rand, x)` + * + * This allows you to do things other than with `rand`, see examples below + * + * @name degradeByWith + * @param {Pattern|number} test + * @param {Pattern|number} testAgainst + * @memberof Pattern + * @tags temporal + * @returns {Pattern} + * @example + * note("c a f e").degradeByWith(sine, .1) + * @example + * note("c a f e").degradeByWith(perlin, .5) + * @example + * note("c a f e").degradeByWith(sine, cosine) + */ +export const degradeByWith = register('degradeByWith', (withPat, x, pat) => { + if (!isPattern(withPat)) withPat = pure(withPat); + return pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)); +}); /** * Randomly removes events from the pattern by a given amount. diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index e0f60184e..dca182dec 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) { } export const speak = register('speak', function (lang, voice, pat) { - return pat.onTrigger((hap) => { + return pat.onSchedule((hap) => { triggerSpeech(hap.value, lang, voice); }); }); diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js index 6169abd71..a5fc23857 100644 --- a/packages/core/test/euclid.test.js +++ b/packages/core/test/euclid.test.js @@ -29,6 +29,44 @@ describe('euclid', () => { .map((a) => a.showWhole(true)), ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); }); + it('Can create euclidLegato using negative steps', () => { + expect( + fastcat('a') + .euclid(3, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclidLegato(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + expect( + fastcat('a') + .euclid(5, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclidLegato(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + }); + it('stretches over multiple cycles if pulses is greater than steps', () => { + expect( + fastcat('a') + .euclid(5, 4) + .fast(2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); + }); }); describe('euclidRot', () => { @@ -67,6 +105,45 @@ describe('euclidLegato', () => { .map((a) => a.showWhole(true)), ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); }); + it('Can create euclid using negative steps', () => { + expect( + fastcat('a') + .euclidLegato(3, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclid(3, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + expect( + fastcat('a') + .euclidLegato(5, -8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual( + fastcat('a') + .euclid(5, 8) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ); + }); + it('stretches over multiple cycles if pulses is greater than steps', () => { + expect( + fastcat('a') + .euclidLegato(5, 4) + .fast(2) + .firstCycle() + .filter((hap) => hap.hasOnset()) + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.showWhole(true)), + ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); + }); }); describe('euclidLegatoRot', () => { diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 4eb18c647..777ed7ee3 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -58,7 +58,7 @@ import { arp, } from '../index.mjs'; -import { log, logValues } from '../pattern.mjs'; +import { log, logValues, stackCenter } from '../pattern.mjs'; import { steady } from '../signal.mjs'; @@ -75,6 +75,10 @@ const sameFirst = (a, b) => { return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle()); }; +const sameFirstX = (x, a, b) => { + return expect(a.fast(x).sortHapsByPart().firstCycle()).toStrictEqual(b.fast(x).sortHapsByPart().firstCycle()); +}; + describe('TimeSpan', () => { describe('equals()', () => { it('Should be equal to the same value', () => { @@ -1215,6 +1219,7 @@ describe('Pattern', () => { expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3)); // maybe this should double when they are either all even or all odd expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3)); + expect(stackCenter(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3)); expect(fastcat(0, 1).ply(3)._steps).toStrictEqual(Fraction(6)); expect(fastcat(0, 1).setSteps(undefined).ply(3)._steps).toStrictEqual(undefined); expect(fastcat(0, 1).fast(3)._steps).toStrictEqual(Fraction(2)); @@ -1320,6 +1325,21 @@ describe('Pattern', () => { ); }); }); + describe('isobite', () => { + it('can play a melody with a structure', () => { + sameFirst( + slowcat(0, 1, 2, 3, 4, 5).isobite(fastcat(true, silence, [true, true]).fast(2)), + fastcat(0, silence, [1, 2], 3, silence, [4, 5]), + ); + }); + it('syncopates over multiple cycles if the number of haps in the patterns differ', () => { + sameFirstX( + 4, + slowcat(0, 1, 2, 3).isobite(fastcat(true, [true, true])), + slowcat(0, [1, 2], 3, [0, 1], 2, [3, 0], 1, [2, 3]).fast(2), + ); + }); + }); describe('unjoin', () => { it('destructures a pattern into subcycles', () => { sameFirst( @@ -1363,7 +1383,7 @@ describe('Pattern', () => { // Force a trigger haps.forEach((hap) => { - hap.context?.onTrigger?.(hap); + hap.context?.onSchedule?.(hap); }); expect(mockConsoleLog).toHaveBeenCalledWith( @@ -1381,7 +1401,7 @@ describe('Pattern', () => { // Force a trigger haps.forEach((hap) => { - hap.context?.onTrigger?.(hap); + hap.context?.onSchedule?.(hap); }); expect(mockConsoleLog).toHaveBeenCalledWith( diff --git a/packages/core/util.mjs b/packages/core/util.mjs index caf2bc054..c4dd22d3b 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -70,17 +70,6 @@ export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => { return (targetTimeSeconds - currentTimeSeconds) * 1000; }; -/** - * @deprecated does not appear to be referenced or invoked anywhere in the codebase - * @noAutocomplete - */ -export const getFreq = (noteOrMidi) => { - if (typeof noteOrMidi === 'number') { - return midiToFreq(noteOrMidi); - } - return midiToFreq(noteToMidi(noteOrMidi)); -}; - const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; /** * @deprecated only used in workshop (first-notes) @@ -130,8 +119,31 @@ export const getPlayableNoteValue = (hap) => { return note; }; +/** + * Given a hap, returns the frequency of the note or freq, if defined + * + * Given a note, returns the frequency of that note + * `getFreq('c3')` + * + * Given a midi note number, returns the frequency of that midi note + * `getFreq('c3')` + * + * @name getFrequency + * @tags pitch + * @synonyms getFreq + * @param {Hap | note | number} input + * @returns {Number} + * @example + * i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3") + * .mul(getFreq('c3')) + * .freq().clip(.5).room(1) + */ export const getFrequency = (hap) => { - let { value, context } = hap; + let { value, context } = + typeof hap == 'object' + ? hap + : // default: interpret value as midi note. + { value: hap, context: { type: 'note' } }; // if value is number => interpret as midi number as long as its not marked as frequency if (typeof value === 'object') { if (value.freq) { @@ -140,15 +152,17 @@ export const getFrequency = (hap) => { return getFreq(value.note ?? value.n ?? value.value); } if (typeof value === 'number' && context.type !== 'frequency') { - value = midiToFreq(hap.value); + value = midiToFreq(value); } else if (typeof value === 'string' && isNote(value)) { - value = midiToFreq(noteToMidi(hap.value)); + value = midiToFreq(noteToMidi(value)); } else if (typeof value !== 'number') { throw new Error('not a note or frequency: ' + value); } return value; }; +export const getFreq = (hap) => getFrequency(hap); + // rotate array by n steps (to the left) export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n)); diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index e459f3873..c070aa293 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => { instrument = instrument || 'triangle'; init(); // not async to support csound inside other patterns + to be able to call pattern methods after it // TODO: find a alternative way to wait for csound to load (to wait with first time playback) - return pat.onTrigger((hap, currentTime, _cps, targetTime) => { + return pat.onSchedule((hap, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -36,9 +36,10 @@ export const csound = register('csound', (instrument, pat) => { const controls = Object.entries({ ...hap.value, freq }) .flat() .join('/'); + // TODO: find out how to send a precise ctx based time // http://www.csounds.com/manual/html/i.html - const timeOffset = targetTime - currentTime; // latency ? + const timeOffset = targetTime - getAudioContext().currentTime; // latency ? //const timeOffset = time_deprecate - getAudioContext().currentTime const params = [ `"${instrument}"`, // p1: instrument name @@ -144,7 +145,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { p1 = `"${instrument}"`; } init(); // not async to support csound inside other patterns + to be able to call pattern methods after it - return pat.onTrigger((hap, currentTime, _cps, targetTime) => { + return pat.onSchedule((hap, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -152,6 +153,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { if (typeof hap.value !== 'object') { throw new Error('csound only support objects as hap values'); } + const currentTime = getAudioContext().currentTime; // Time in seconds counting from now. const p2 = targetTime - currentTime; const p3 = hap.duration.valueOf() + 0; diff --git a/packages/desktopbridge/midibridge.mjs b/packages/desktopbridge/midibridge.mjs index 2436d8ad7..26c4154f9 100644 --- a/packages/desktopbridge/midibridge.mjs +++ b/packages/desktopbridge/midibridge.mjs @@ -1,3 +1,4 @@ +import { getAudioContext } from '@strudel/webaudio/index.mjs'; import { Invoke } from './utils.mjs'; import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core'; @@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80; const CC_MESSAGE = 0xb0; Pattern.prototype.midi = function (output) { - return this.onTrigger((hap, currentTime, cps, targetTime) => { + return this.onSchedule((hap, targetTime, cps) => { + const currentTime = getAudioContext().currentTime; let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; //magic number to get audio engine to line up, can probably be calculated somehow const latencyMs = 34; diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 2568c78e5..ebf9775a7 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -1,12 +1,14 @@ import { Pattern, ClockCollator } from '@strudel/core'; -import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs'; +import { parseControlsFromHap } from '@strudel/osc/osc.mjs'; import { Invoke } from './utils.mjs'; +import { getAudioContext } from '@strudel/webaudio/index.mjs'; const collator = new ClockCollator({}); -export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { +export async function tauriOscOutput(hap, targetTime, cps = 1) { const controls = parseControlsFromHap(hap, cps); const params = []; + const currentTime = getAudioContext().currentTime; const timestamp = collator.calculateTimestamp(currentTime, targetTime); Object.keys(controls).forEach((key) => { @@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { }); } Pattern.prototype.osc = function () { - return this.onTrigger(oscTriggerTauri); + return this.onSchedule(tauriOscOutput); }; diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index a7352d6b0..8654c65c7 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -23,6 +23,8 @@ }, "dependencies": { "@strudel/core": "workspace:*", + "@strudel/osc": "workspace:*", + "@strudel/webaudio": "workspace:*", "@tauri-apps/api": "^2.2.0" }, "homepage": "https://codeberg.org/uzu/strudel#readme", diff --git a/packages/dough/dough.mjs b/packages/dough/dough.mjs index 745bdf60b..942e8fbe0 100644 --- a/packages/dough/dough.mjs +++ b/packages/dough/dough.mjs @@ -41,7 +41,7 @@ export function initDough() { return D.ready; } -export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) { +export async function doughOutput(hap, targetTime, cps = 1) { const offset = D.context_offset?.[0]; if (!offset) { return; // not ready @@ -75,5 +75,5 @@ export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) { export const dough = register('dough', (pat) => { initDough(); ensureMinimalOutput(); - return pat.onTrigger(doughTrigger); + return pat.onSchedule(doughOutput); }); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 68a0d744f..6ce264cd1 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -13,7 +13,7 @@ import { getIsStarted, getPattern, getTime, - getTriggerFunc, + getScheduleFunc, isPattern, logger, ref, @@ -342,7 +342,7 @@ Pattern.prototype.midi = function (midiport, options = {}) { ensureMinimalOutput(); - return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => { + return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => { if (!WebMidi.enabled) { logger('Midi not enabled'); return; @@ -570,8 +570,8 @@ const kListeners = {}; function _triggerKeyboard(input, cps, now, latencyCycles) { const pattern = getPattern(); - const trigger = getTriggerFunc(); - if (!pattern || !trigger) { + const schedule = getScheduleFunc(); + if (!pattern || !schedule) { return false; } const t = now + latencyCycles; @@ -588,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) { return; } const t = ctxNow + (hap.whole.begin - now) / cps; - const duration = hap.duration / cps; - trigger(hap, t - ctxNow, duration, cps, t); + schedule(hap, t, cps); }); return true; @@ -624,7 +623,7 @@ export async function midikeys(input) { noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff const key = `${input}_${note}`; const cps = getCps() ?? 0.5; - const triggerAvailable = !!(getPattern() && getTriggerFunc()); + const triggerAvailable = !!(getPattern() && getScheduleFunc()); const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency const now = getTime(); const t = now + latencySeconds * cps; diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index c38997308..c84819e87 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -84,6 +84,22 @@ describe('mini', () => { it('supports patterning euclidean rhythms', () => { expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)')); }); + it('supports inverted euclidean rhythms', () => { + expect(minS('a(-3, 8)')).toEqual(['a: 1/8 - 1/4', 'a: 1/4 - 3/8', 'a: 1/2 - 5/8', 'a: 5/8 - 3/4', 'a: 7/8 - 1']); + }); + it('supports euclidean legato rhythms', () => { + expect(minS('a(3, -8)')).toEqual(['a: 0 - 3/8', 'a: 3/8 - 3/4', 'a: 3/4 - 1']); + }); + it('supports inverted euclidean legato rhythms', () => { + expect(minS('a(-3, -8)')).toEqual([ + 'a: -1/8 - 1/8', + 'a: 1/8 - 1/4', + 'a: 1/4 - 1/2', + 'a: 1/2 - 5/8', + 'a: 5/8 - 7/8', + 'a: 7/8 - 9/8', + ]); + }); it("reproduces Toussaint's example euclidean algorithms", () => { const checkEuclid = function (spec, target) { expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target)); @@ -118,6 +134,40 @@ describe('mini', () => { checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~'); checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~'); }); + it("reproduces Toussaint's example euclidean legato algorithms", () => { + const checkEuclid = function (spec, target) { + expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target)); + }; + checkEuclid([1, -2], 'x _'); + checkEuclid([1, -3], 'x _ _'); + checkEuclid([1, -4], 'x _ _ _'); + checkEuclid([4, -12], 'x _ _ x _ _ x _ _ x _ _'); + checkEuclid([2, -5], 'x _ x _ _'); + // checkEuclid([3, 4], "x _ x x"); // Toussaint is wrong.. + checkEuclid([3, -4], 'x x x _'); // correction + checkEuclid([3, -5], 'x _ x _ x'); + checkEuclid([3, -7], 'x _ x _ x _ _'); + checkEuclid([3, -8], 'x _ _ x _ _ x _'); + checkEuclid([4, -7], 'x _ x _ x _ x'); + checkEuclid([4, -9], 'x _ x _ x _ x _ _'); + checkEuclid([4, -11], 'x _ _ x _ _ x _ _ x _'); + // checkEuclid([5, 6], "x _ x x x x"); // Toussaint is wrong.. + checkEuclid([5, -6], 'x x x x x _'); // correction + checkEuclid([5, -7], 'x _ x x _ x x'); + checkEuclid([5, -8], 'x _ x x _ x x _'); + checkEuclid([5, -9], 'x _ x _ x _ x _ x'); + checkEuclid([5, -11], 'x _ x _ x _ x _ x _ _'); + checkEuclid([5, -12], 'x _ _ x _ x _ _ x _ x _'); + // checkEuclid([5, 16], "x _ _ x _ _ x _ _ x _ _ x _ _ _ _"); // Toussaint is wrong.. + checkEuclid([5, -16], 'x _ _ x _ _ x _ _ x _ _ x _ _ _'); // correction + // checkEuclid([7, 8], "x _ x x x x x x"); // Toussaint is wrong.. + checkEuclid([7, -8], 'x x x x x x x _'); // Correction + checkEuclid([7, -12], 'x _ x x _ x _ x x _ x _'); + checkEuclid([7, -16], 'x _ _ x _ x _ x _ _ x _ x _ x _'); + checkEuclid([9, -16], 'x _ x x _ x _ x _ x x _ x _ x _'); + checkEuclid([11, -24], 'x _ _ x _ x _ x _ x _ x _ _ x _ x _ x _ x _ x _'); + checkEuclid([13, -24], 'x _ x x _ x _ x _ x _ x _ x x _ x _ x _ x _ x _'); + }); it('supports the - alias for ~', () => { expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]')); }); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index a230c3cb9..a043adb83 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -46,7 +46,6 @@ lib[':'] = tail; lib['..'] = range; lib['def'] = () => silence; lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" -//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct function evaluator(node, scope) { const { type } = node; diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index c0a958cfd..7e463bf47 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, isPattern, createParams } from '@strudel/core'; +import { Pattern, getClockTime, createParams } from '@strudel/core'; import Paho from 'paho-mqtt'; const connections = {}; @@ -80,42 +80,39 @@ Pattern.prototype.mqtt = function ( } cx.connect(props); } - return this.withHap((hap) => { - const onTrigger = (hap, currentTime, cps, targetTime) => { - let msg_topic = topic; - if (!cx || !cx.isConnected()) { - return; - } - let message = ''; - if (typeof hap.value === 'object') { - let value = hap.value; + return this.onSchedule((hap, targetTime, cps) => { + let msg_topic = topic; + if (!cx || !cx.isConnected()) { + return; + } + let message = ''; + if (typeof hap.value === 'object') { + let value = hap.value; - // Try to take topic from pattern if it's not set - if (typeof msg_topic === 'undefined' && 'topic' in value) { - msg_topic = value.topic; - if (Array.isArray(msg_topic)) { - msg_topic = msg_topic.join('/'); - } - msg_topic = '/' + msg_topic; + // Try to take topic from pattern if it's not set + if (typeof msg_topic === 'undefined' && 'topic' in value) { + msg_topic = value.topic; + if (Array.isArray(msg_topic)) { + msg_topic = msg_topic.join('/'); } - if (add_meta) { - const duration = hap.duration.div(cps); - value = { ...value, duration: duration.valueOf(), cps: cps }; - } - message = JSON.stringify(value); - } else { - message = hap.value; + msg_topic = '/' + msg_topic; } - message = new Paho.Message(message); - message.destinationName = msg_topic; + if (add_meta) { + const duration = hap.duration.div(cps); + value = { ...value, duration: duration.valueOf(), cps: cps }; + } + message = JSON.stringify(value); + } else { + message = hap.value; + } + message = new Paho.Message(message); + message.destinationName = msg_topic; - const offset = (targetTime - currentTime + latency) * 1000; + const offset = (targetTime - getClockTime() + latency) * 1000; - window.setTimeout(function () { - cx.send(message); - }, offset); - }; - return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); + window.setTimeout(function () { + cx.send(message); + }, offset); }); }; diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 2c0619b4a..7c3be572a 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -55,7 +55,7 @@ export function parseControlsFromHap(hap, cps) { return controls; } -export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { +export async function oscOutput(hap, targetTime, cps = 1) { const ws = await connect(); const timeMs = getClockBridge().getPerformanceTime(targetTime); @@ -90,5 +90,5 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { */ export const osc = register('osc', (pat) => { ensureMinimalOutput(); - return pat.onTrigger(oscTrigger); + return pat.onSchedule(oscOutput); }); diff --git a/packages/osc/superdirtoutput.js b/packages/osc/superdirtoutput.js index a317af8f1..757150659 100644 --- a/packages/osc/superdirtoutput.js +++ b/packages/osc/superdirtoutput.js @@ -1,10 +1,7 @@ -/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; +/* import { tauriOscOutput } from '../desktopbridge/oscbridge.mjs'; import { isTauri } from '../desktopbridge/utils.mjs'; */ -import { oscTrigger } from './osc.mjs'; +import { oscOutput } from './osc.mjs'; -const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; +const output = /* isTauri() ? tauriOscOutput : */ oscOutput; -export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { - const currentTime = performance.now() / 1000; - return trigger(hap, currentTime, cps, targetTime); -}; +export const superdirtOutput = (hap, targetTime, cps) => output(hap, targetTime, cps); diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index cb79a4755..14a77660a 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -37,7 +37,7 @@ if (typeof HTMLElement !== 'undefined') { const drawContext = getDrawContext(); const drawTime = [-2, 2]; this.editor = new StrudelMirror({ - defaultOutput: webaudioOutput, + onSchedule: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, root: container, diff --git a/packages/serial/serial.mjs b/packages/serial/serial.mjs index e007b24b9..b3ee75cc6 100644 --- a/packages/serial/serial.mjs +++ b/packages/serial/serial.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, isPattern } from '@strudel/core'; +import { Pattern, getClockTime } from '@strudel/core'; var writeMessagers = {}; var choosing = false; @@ -64,53 +64,50 @@ function crc16(data) { } Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') { - return this.withHap((hap) => { - if (!(name in writeMessagers)) { - getWriter(name, br); - } - const onTrigger = (hap, currentTime, _cps, targetTime) => { - var message = ''; - var chk = 0; - if (typeof hap.value === 'object') { - if ('action' in hap.value) { - var action = hap.value['action']; + if (!(name in writeMessagers)) { + getWriter(name, br); + } + return this.onSchedule((hap, targetTime) => { + var message = ''; + var chk = 0; + if (typeof hap.value === 'object') { + if ('action' in hap.value) { + var action = hap.value['action']; + if (singlecharids) { + action = action.charAt(0); + } + message += action + '('; + var first = true; + for (var [key, val] of Object.entries(hap.value)) { + if (key === 'action') { + continue; + } + if (first) { + first = false; + } else { + message += ','; + } if (singlecharids) { - action = action.charAt(0); - } - message += action + '('; - var first = true; - for (var [key, val] of Object.entries(hap.value)) { - if (key === 'action') { - continue; - } - if (first) { - first = false; - } else { - message += ','; - } - if (singlecharids) { - key = key.charAt(0); - } - message += key + ':' + val; - } - message += ')'; - if (sendcrc) { - chk = crc16(message); - } - } else { - for (const [key, val] of Object.entries(hap.value)) { - message += `${key}:${val}`; + key = key.charAt(0); } + message += key + ':' + val; + } + message += ')'; + if (sendcrc) { + chk = crc16(message); } } else { - message = hap.value; + for (const [key, val] of Object.entries(hap.value)) { + message += `${key}:${val}`; + } } - const offset = (targetTime - currentTime + latency) * 1000; + } else { + message = hap.value; + } + const offset = (targetTime - getClockTime() + latency) * 1000; - window.setTimeout(function () { - writeMessagers[name](message, chk); - }, offset); - }; - return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); + window.setTimeout(function () { + writeMessagers[name](message, chk); + }, offset); }); }; diff --git a/packages/soundfonts/gm.mjs b/packages/soundfonts/gm.mjs index d8b7687d7..5fc8252d6 100644 --- a/packages/soundfonts/gm.mjs +++ b/packages/soundfonts/gm.mjs @@ -479,7 +479,7 @@ export default { gm_electric_bass_finger: [ // Electric Bass (finger): Bass '0330_JCLive_sf2_file', - '0330_FluidR3_GM_sf2_fible', + '0330_FluidR3_GM_sf2_file', '0330_Aspirin_sf2_file', //0330_Chaos_sf2_file // same as last '0330_GeneralUserGS_sf2_file', @@ -527,7 +527,7 @@ export default { '0370_Aspirin_sf2_file', // 0370_Chaos_sf2_file // same as last '0370_FluidR3_GM_sf2_file', - '0370_GeneralUserGS_sf2_fil e', + '0370_GeneralUserGS_sf2_file', '0370_JCLive_sf2_file', //0370_SBLive_sf2 //0370_SoundBlasterOld_sf2 @@ -1782,6 +1782,5 @@ export default { '1272_GeneralUserGS_sf2_file', '1273_GeneralUserGS_sf2_file', '1274_GeneralUserGS_sf2_file', - '', ], }; diff --git a/packages/soundfonts/sfumato.mjs b/packages/soundfonts/sfumato.mjs index 4d5e7ef45..a8aa299c1 100644 --- a/packages/soundfonts/sfumato.mjs +++ b/packages/soundfonts/sfumato.mjs @@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; Pattern.prototype.soundfont = function (sf, n = 0) { - return this.onTrigger((h, ct, cps, targetTime) => { + return this.onSchedule((h, targetTime) => { const ctx = getAudioContext(); const note = getPlayableNoteValue(h); const preset = sf.presets[n % sf.presets.length]; diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index ed6d0c7b3..5f5c9e16d 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -74,6 +74,7 @@ export const rawdsp = async (code) => { worklet.node.connect(ac.destination); }; -export function rawdspTrigger(hap, currentTime, cps, targetTime) { - window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); +export function rawdspOutput(hap, time, cps) { + const currentTime = getAudioContext().currentTime; + window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps }); } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index d45e1050b..c4b39b469 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -36,8 +36,9 @@ function humanFileSize(bytes, si) { * * Note: `must` be called with await, otherwise you'll get a pending Promise object. * - * @name getDuration,getDur - * @tag samples + * @name getDuration + * @synonym getDur + * @tags samples * @param {string} sampleName * @param {number} (optional) n * diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 98d1fb67b..3209e7098 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -1048,7 +1048,3 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } }); }; - -export const superdoughTrigger = (t, hap, ct, cps) => { - superdough(hap, t - ct, hap.duration / cps, cps); -}; diff --git a/packages/web/web.mjs b/packages/web/web.mjs index eebe6bdc1..e45943ad2 100644 --- a/packages/web/web.mjs +++ b/packages/web/web.mjs @@ -6,7 +6,7 @@ export * from '@strudel/mini'; export * from '@strudel/edo'; export * from '@strudel/tonal'; export * from '@strudel/webaudio'; -import { Pattern, evalScope, setTime } from '@strudel/core'; +import { Pattern, evalScope, setTime, setClockTime } from '@strudel/core'; import { initAudioOnFirstClick, registerSynthSounds, webaudioRepl } from '@strudel/webaudio'; // import { registerSoundfonts } from '@strudel/soundfonts'; import { evaluate as _evaluate, transpiler } from '@strudel/transpiler'; @@ -41,6 +41,7 @@ export function initStrudel(options = {}) { return repl; })(); setTime(() => repl.scheduler.now()); + setClockTime(() => repl.scheduler.getTime()); // TODO: refactor? return initDone; } diff --git a/packages/webaudio/README.md b/packages/webaudio/README.md index d89a6d39c..c3bf11b75 100644 --- a/packages/webaudio/README.md +++ b/packages/webaudio/README.md @@ -19,7 +19,7 @@ initAudioOnFirstClick(); const ctx = getAudioContext(); const { scheduler } = repl({ - defaultOutput: webaudioOutput, + onSchedule: webaudioOutput, getTime: () => ctx.currentTime }); diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index d55957ced..83458c54e 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -20,7 +20,7 @@ const soundMap = new Map(); const loadedSounds = new Map(); Pattern.prototype.supradough = function () { - return this.onTrigger((hap, __, cps, begin) => { + return this.onSchedule((hap, begin, cps) => { hap.value._begin = begin; hap.value._duration = hap.duration / cps; !doughWorklet && initDoughWorklet(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 4f7256fe7..213ac54bd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -9,7 +9,7 @@ import { superdough, getAudioContext, setLogger, - rawdspTrigger, + rawdspOutput, registerWorklet, setAudioContext, initAudio, @@ -32,8 +32,8 @@ const hap2value = (hap) => { }; // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 -// TODO: refactor output callbacks to eliminate deadline -export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { +export const webaudioOutput = (hap, t, cps) => { + const hapDuration = hap.duration / cps; return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; @@ -167,14 +167,14 @@ export function webaudioRepl(options = {}) { setAudioContext(audioContext); options = { getTime: () => audioContext.currentTime, - defaultOutput: webaudioOutput, + onSchedule: webaudioOutput, ...options, }; return repl(options); } Pattern.prototype.rawdsp = function () { - return this.onTrigger(rawdspTrigger, 1); + return this.onSchedule(rawdspOutput, 1); }; function audioBufferToWav(buffer, opt) { diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index a070515b3..b17ad3292 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -8,11 +8,14 @@ import Tune from './tunejs.js'; import { register } from '@strudel/core'; /** + * Deprecated. + * * Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`. * @name tune * @returns Pattern * @memberof Pattern * @param {(string | number[] )} scale + * @deprecated * @example * i("0 1 2 3 4 5").tune("hexany15").mul("220").freq() * @example diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 3f940c89a..0dcc4c9d6 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; +import { register, _mod, parseNumeral, removeUndefineds, Pattern, parray, State } from '@strudel/core'; import Tune from './tunejs.js'; // returns a list of frequency ratios for given edo scale @@ -61,14 +61,8 @@ function xenOffset(xenScale, offset, index = 0) { const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); -// accepts a scale name such as 31edo, and a pattern -// pattern expected to follow format such that a value can be mapped -// to an edostep within the scale. Returns the pattern with -// values mapped to the frequencies associated with the given edosteps -// scaleNameOrRatios: string || number[], steps?: number - /** - * Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. + * Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. See first 4 examples for the different ways you can input a scale into `xen` * * @name xen * @returns Pattern @@ -87,6 +81,15 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); * Math.pow(2, 18/31), * ]).piano() * @example + * // However, with the above you won't get highlighting of the scale indices, + * // instead you could do something like: + * i("[<2 ->, 0 1 <- 2> 3]*2").xen([ + * "1:1", // tonic + * "5:4", // Major 3rd + * "[3:2 4:3]", // Perfect 5th, 4th + * "5:3" // Major 6th + * ]).s("tri") + * @example * // xen also supports all scale names that * // tune does: * i("0 1 2 3 4 5").xen("hexany15") @@ -96,27 +99,100 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") */ -export const xen = register('xen', function (scaleNameOrRatios, pat) { - return pat.withHaps((haps) => { - haps = haps.map((hap) => { - let hVal = hap.value; - const isObject = typeof hVal === 'object'; - if (!isObject) { - throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`); - } - const { i, ...otherValues } = hVal; - const scale = getXenScale(scaleNameOrRatios); - let freq = xenOffset(scale, parseNumeral(hVal.i)); - // 10 is somewhat arbitrary - freq = trimFreq(freq); - hap.value = { ...otherValues, freq }; - return isEdo(scaleNameOrRatios) - ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] }) - : hap; - }); - return removeUndefineds(haps); - }); -}); +export const xen = register( + 'xen', + function (scaleNameOrRatios, pat) { + const JIsc2 = (input) => { + let k = parray(input); + + return new Pattern((state) => { + let res = k.withValue((x) => x.map((y) => eval(y.join('/')))).query(state); + return res; + }); + }; + + const query = (state) => { + let haps = pat.query(state); + let resHaps = []; + + haps.forEach((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + if (!isObject) { + throw new Error( + `Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`, + ); + } + const { i, ...otherValues } = hVal; + let edo = false; + let st = new State(hap.wholeOrPart().intersection(state.span), state.controls); + + // Format: An array of values + // Each value is a pattern, representing an index on the scale + let sc = scaleNameOrRatios.query(st); + // Check to see if input is attempting to be a named scale or a raw scale + let isStr = typeof sc[0].value == 'string'; + let isRawScale = Array.isArray(sc[0].value) && sc[0].value.every((x) => typeof x == 'number'); + if (!isStr && !isRawScale) { + sc = sc[0].value; + } else if (isRawScale) { + sc = getXenScale(sc[0].value); + } + + if (!isStr && !isRawScale) { + let scaleByIndices = []; + for (let j = 0; j < sc.length; j++) { + scaleByIndices[j] = sc[j].query(st); + } + /* + Format: An array of 1 or more Haps, each Hap has an array of values of + one or more scale index definitions (contains whole scale) + Assumption: inner value array should be same length between each + */ + let sc2 = JIsc2(sc).query(st); + + let index = _mod(i, sc.length); + for (let j = 0; j < scaleByIndices[index].length; j++) { + let scale = getXenScale(sc2[j].value); + let freq = xenOffset(scale, parseNumeral(i)); + let newHap = hap; + freq = trimFreq(freq); + newHap.value = { ...otherValues, freq }; + newHap = newHap.setContext(newHap.combineContext(scaleByIndices[index][j])); + resHaps.push(newHap); + } + } else if (isRawScale) { + let freq = xenOffset(sc, parseNumeral(i)); + let newHap = hap; + freq = trimFreq(freq); + newHap.value = { ...otherValues, freq }; + resHaps.push(newHap); + } else if (isStr) { + // scale is named scale(s) + for (let j = 0; j < sc.length; j++) { + let scaleName = sc[j].value; + let scale = getXenScale(scaleName); + let freq = xenOffset(scale, parseNumeral(i)); + let newHap = hap; + freq = trimFreq(freq); + newHap.value = { ...otherValues, freq }; + newHap = newHap.setContext(newHap.combineContext(sc[j])); + if (isEdo(scaleName)) { + edo = scaleName.match(/^([1-9]+[0-9]*)edo$/)[1]; + newHap = newHap.setContext({ ...newHap.context, edoSize: edo }); + } + resHaps.push(newHap); + } + } else { + throw new Error('failed to parse scale'); + } + }); + return resHaps; + }; + return new Pattern(query); + }, + false, +); /** * Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d42e108f..58eba5679 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,6 +299,12 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/osc': + specifier: workspace:* + version: link:../osc + '@strudel/webaudio': + specifier: workspace:* + version: link:../webaudio '@tauri-apps/api': specifier: ^2.2.0 version: 2.2.0 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ccb91377f..dc4c98c45 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1827,6 +1827,27 @@ exports[`runs examples > example "bpsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "brak" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 1/4 → 1/2 | s:hh ]", + "[ 1/2 → 3/4 | s:hh ]", + "[ 3/4 → 1/1 | s:rim ]", + "[ 5/4 → 11/8 | s:bd ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:rim ]", + "[ 2/1 → 9/4 | s:bd ]", + "[ 9/4 → 5/2 | s:hh ]", + "[ 5/2 → 11/4 | s:hh ]", + "[ 11/4 → 3/1 | s:rim ]", + "[ 13/4 → 27/8 | s:bd ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:rim ]", +] +`; + exports[`runs examples > example "brand" example index 0 1`] = ` [ "[ 0/1 → 1/10 | s:hh pan:true ]", @@ -2141,6 +2162,40 @@ exports[`runs examples > example "chooseCycles" example index 1 1`] = ` ] `; +exports[`runs examples > example "chooseInWith" example index 0 1`] = ` +[ + "[ (0/1 → 1/3) ⇝ 1/1 | note:c3 ]", + "[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | note:b3 ]", + "[ 0/1 ⇜ (2/3 → 1/1) | note:d3 ]", + "[ (1/1 → 4/3) ⇝ 2/1 | note:c3 ]", + "[ 1/1 ⇜ (4/3 → 5/3) ⇝ 2/1 | note:b3 ]", + "[ 1/1 ⇜ (5/3 → 2/1) | note:d3 ]", + "[ (2/1 → 7/3) ⇝ 3/1 | note:c3 ]", + "[ 2/1 ⇜ (7/3 → 8/3) ⇝ 3/1 | note:b3 ]", + "[ 2/1 ⇜ (8/3 → 3/1) | note:d3 ]", + "[ (3/1 → 10/3) ⇝ 4/1 | note:c3 ]", + "[ 3/1 ⇜ (10/3 → 11/3) ⇝ 4/1 | note:b3 ]", + "[ 3/1 ⇜ (11/3 → 4/1) | note:d3 ]", +] +`; + +exports[`runs examples > example "chooseInWith" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | note:c3 ]", + "[ 1/3 → 2/3 | note:b3 ]", + "[ 2/3 → 1/1 | note:d3 ]", + "[ 1/1 → 4/3 | note:c3 ]", + "[ 4/3 → 5/3 | note:b3 ]", + "[ 5/3 → 2/1 | note:d3 ]", + "[ 2/1 → 7/3 | note:c3 ]", + "[ 7/3 → 8/3 | note:b3 ]", + "[ 8/3 → 3/1 | note:d3 ]", + "[ 3/1 → 10/3 | note:c3 ]", + "[ 10/3 → 11/3 | note:b3 ]", + "[ 11/3 → 4/1 | note:d3 ]", +] +`; + exports[`runs examples > example "chooseWith" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c2 s:triangle ]", @@ -2166,6 +2221,23 @@ exports[`runs examples > example "chooseWith" example index 0 1`] = ` ] `; +exports[`runs examples > example "chooseWith" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | note:c3 ]", + "[ 1/3 → 2/3 | note:b3 ]", + "[ 2/3 → 1/1 | note:d3 ]", + "[ 1/1 → 4/3 | note:c3 ]", + "[ 4/3 → 5/3 | note:b3 ]", + "[ 5/3 → 2/1 | note:d3 ]", + "[ 2/1 → 7/3 | note:c3 ]", + "[ 7/3 → 8/3 | note:b3 ]", + "[ 8/3 → 3/1 | note:d3 ]", + "[ 3/1 → 10/3 | note:c3 ]", + "[ 10/3 → 11/3 | note:b3 ]", + "[ 11/3 → 4/1 | note:d3 ]", +] +`; + exports[`runs examples > example "chop" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:rhodes begin:0.75 end:1 speed:0.25 unit:c ]", @@ -3006,6 +3078,31 @@ exports[`runs examples > example "degradeBy" example index 2 1`] = ` ] `; +exports[`runs examples > example "degradeByWith" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c ]", + "[ 1/4 → 1/2 | note:a ]", + "[ 1/2 → 3/4 | note:f ]", + "[ 3/4 → 1/1 | note:e ]", + "[ 1/1 → 5/4 | note:c ]", + "[ 5/4 → 3/2 | note:a ]", + "[ 3/2 → 7/4 | note:f ]", + "[ 7/4 → 2/1 | note:e ]", + "[ 2/1 → 9/4 | note:c ]", + "[ 9/4 → 5/2 | note:a ]", + "[ 5/2 → 11/4 | note:f ]", + "[ 11/4 → 3/1 | note:e ]", + "[ 3/1 → 13/4 | note:c ]", + "[ 13/4 → 7/2 | note:a ]", + "[ 7/2 → 15/4 | note:f ]", + "[ 15/4 → 4/1 | note:e ]", +] +`; + +exports[`runs examples > example "degradeByWith" example index 1 1`] = `[]`; + +exports[`runs examples > example "degradeByWith" example index 2 1`] = `[]`; + exports[`runs examples > example "delay" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0 ]", @@ -4264,13 +4361,16 @@ exports[`runs examples > example "euclidLegatoRot" example index 0 1`] = ` "[ -1/5 ⇜ (0/1 → 1/5) | note:c3 ]", "[ 1/5 → 2/5 | note:c3 ]", "[ 2/5 → 4/5 | note:c3 ]", - "[ 4/5 → 6/5 | note:c3 ]", + "[ (4/5 → 1/1) ⇝ 6/5 | note:c3 ]", + "[ 4/5 ⇜ (1/1 → 6/5) | note:c3 ]", "[ 6/5 → 7/5 | note:c3 ]", "[ 7/5 → 9/5 | note:c3 ]", - "[ 9/5 → 11/5 | note:c3 ]", + "[ (9/5 → 2/1) ⇝ 11/5 | note:c3 ]", + "[ 9/5 ⇜ (2/1 → 11/5) | note:c3 ]", "[ 11/5 → 12/5 | note:c3 ]", "[ 12/5 → 14/5 | note:c3 ]", - "[ 14/5 → 16/5 | note:c3 ]", + "[ (14/5 → 3/1) ⇝ 16/5 | note:c3 ]", + "[ 14/5 ⇜ (3/1 → 16/5) | note:c3 ]", "[ 16/5 → 17/5 | note:c3 ]", "[ 17/5 → 19/5 | note:c3 ]", "[ (19/5 → 4/1) ⇝ 21/5 | note:c3 ]", @@ -4378,7 +4478,7 @@ exports[`runs examples > example "extend" example index 0 1`] = ` "[ 3/8 → 1/2 | s:cp ]", "[ 1/2 → 5/8 | s:bd ]", "[ 5/8 → 3/4 | s:bd ]", - "[ 7/8 → 1/1 | s:cp ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:bd ]", "[ 5/4 → 11/8 | s:sd ]", "[ 3/2 → 13/8 | s:bd ]", @@ -4386,7 +4486,7 @@ exports[`runs examples > example "extend" example index 0 1`] = ` "[ 15/8 → 2/1 | s:cp ]", "[ 2/1 → 17/8 | s:bd ]", "[ 17/8 → 9/4 | s:bd ]", - "[ 19/8 → 5/2 | s:cp ]", + "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:bd ]", "[ 11/4 → 23/8 | s:sd ]", "[ 3/1 → 25/8 | s:bd ]", @@ -4394,7 +4494,7 @@ exports[`runs examples > example "extend" example index 0 1`] = ` "[ 27/8 → 7/2 | s:cp ]", "[ 7/2 → 29/8 | s:bd ]", "[ 29/8 → 15/4 | s:bd ]", - "[ 31/8 → 4/1 | s:cp ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -5359,7 +5459,7 @@ exports[`runs examples > example "gain" example index 0 1`] = ` exports[`runs examples > example "gap" example index 0 1`] = `[]`; -exports[`runs examples > example "getDur" example index 0 1`] = ` +exports[`runs examples > example "getDuration" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:sax cps:null ]", "[ 1/1 → 2/1 | s:sax cps:null ]", @@ -5368,6 +5468,43 @@ exports[`runs examples > example "getDur" example index 0 1`] = ` ] `; +exports[`runs examples > example "getFrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 1/12 → 1/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 1/6 → 1/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 1/4 → 1/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 1/2 → 7/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 7/12 → 2/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 2/3 → 3/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 3/4 → 5/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 1/1 → 13/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 13/12 → 7/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 7/6 → 5/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 5/4 → 4/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 3/2 → 19/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 19/12 → 5/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 5/3 → 7/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 7/4 → 11/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 2/1 → 25/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 25/12 → 13/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 13/6 → 9/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 9/4 → 7/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 5/2 → 31/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 31/12 → 8/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 8/3 → 11/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 11/4 → 17/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 3/1 → 37/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 37/12 → 19/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 19/6 → 13/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 13/4 → 10/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 7/2 → 43/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 43/12 → 11/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 11/3 → 15/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 15/4 → 23/6 | freq:{i:11} clip:0.5 room:1 ]", +] +`; + exports[`runs examples > example "grow" example index 0 1`] = ` [ "[ 0/1 → 1/10 | s:tha bank:mridangam ]", @@ -6281,6 +6418,70 @@ exports[`runs examples > example "isaw" example index 1 1`] = ` ] `; +exports[`runs examples > example "isobite" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | note:C3 ]", + "[ 1/3 → 5/12 | note:D3 ]", + "[ 5/12 → 1/2 | note:E3 ]", + "[ 1/2 → 2/3 | note:F3 ]", + "[ 5/6 → 11/12 | note:G3 ]", + "[ 11/12 → 1/1 | note:A3 ]", + "[ 1/1 → 7/6 | note:C3 ]", + "[ 4/3 → 17/12 | note:D3 ]", + "[ 17/12 → 3/2 | note:E3 ]", + "[ 3/2 → 5/3 | note:F3 ]", + "[ 11/6 → 23/12 | note:G3 ]", + "[ 23/12 → 2/1 | note:A3 ]", + "[ 2/1 → 13/6 | note:C3 ]", + "[ 7/3 → 29/12 | note:D3 ]", + "[ 29/12 → 5/2 | note:E3 ]", + "[ 5/2 → 8/3 | note:F3 ]", + "[ 17/6 → 35/12 | note:G3 ]", + "[ 35/12 → 3/1 | note:A3 ]", + "[ 3/1 → 19/6 | note:C3 ]", + "[ 10/3 → 41/12 | note:D3 ]", + "[ 41/12 → 7/2 | note:E3 ]", + "[ 7/2 → 11/3 | note:F3 ]", + "[ 23/6 → 47/12 | note:G3 ]", + "[ 47/12 → 4/1 | note:A3 ]", +] +`; + +exports[`runs examples > example "isobite" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | note:C3 ]", + "[ 2/3 → 1/1 | note:D3 ]", + "[ 1/1 → 4/3 | note:E3 ]", + "[ 5/3 → 2/1 | note:F3 ]", + "[ 2/1 → 7/3 | note:G3 ]", + "[ 8/3 → 3/1 | note:C3 ]", + "[ 3/1 → 10/3 | note:D3 ]", + "[ 11/3 → 4/1 | note:E3 ]", +] +`; + +exports[`runs examples > example "isobite" example index 2 1`] = ` +[ + "[ 0/1 → 1/1 | note:C3 ]", + "[ 1/1 → 3/2 | note:E3 ]", + "[ 3/2 → 2/1 | note:F3 ]", + "[ 2/1 → 3/1 | note:E3 ]", + "[ 3/1 → 7/2 | note:E3 ]", + "[ 7/2 → 4/1 | note:F3 ]", +] +`; + +exports[`runs examples > example "isobite" example index 3 1`] = ` +[ + "[ 0/1 → 1/1 | note:C3 ]", + "[ 1/1 → 3/2 | note:D3 ]", + "[ 3/2 → 2/1 | note:E3 ]", + "[ 2/1 → 3/1 | note:F3 ]", + "[ 3/1 → 7/2 | note:C3 ]", + "[ 7/2 → 4/1 | note:D3 ]", +] +`; + exports[`runs examples > example "iter" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:A3 ]", @@ -7774,15 +7975,6 @@ exports[`runs examples > example "midi" example index 0 1`] = ` ] `; -exports[`runs examples > example "midi" example index 1 1`] = ` -[ - "[ 0/1 → 1/1 | note:c4 midichan:1 ]", - "[ 1/1 → 2/1 | note:c4 midichan:1 ]", - "[ 2/1 → 3/1 | note:c4 midichan:1 ]", - "[ 3/1 → 4/1 | note:c4 midichan:1 ]", -] -`; - exports[`runs examples > example "midibend" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 midibend:0 ]", @@ -8522,6 +8714,19 @@ exports[`runs examples > example "often" example index 0 1`] = ` ] `; +exports[`runs examples > example "onSchedule" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd ]", + "[ 1/2 → 1/1 | s:sd ]", + "[ 1/1 → 3/2 | s:bd ]", + "[ 3/2 → 2/1 | s:sd ]", + "[ 2/1 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 3/1 → 7/2 | s:bd ]", + "[ 7/2 → 4/1 | s:sd ]", +] +`; + exports[`runs examples > example "onTriggerTime" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", @@ -9427,6 +9632,24 @@ exports[`runs examples > example "pickF" example index 2 1`] = ` ] `; +exports[`runs examples > example "pickOut" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:g ]", + "[ 1/4 → 1/2 | note:g ]", + "[ 1/2 → 3/4 | note:a ]", + "[ 3/4 → 1/1 | note:a ]", + "[ (1/1 → 3/2) ⇝ 2/1 | note:e ]", + "[ 1/1 ⇜ (3/2 → 2/1) | note:f ]", + "[ (2/1 → 9/4) ⇝ 5/2 | note:f ]", + "[ 2/1 ⇜ (9/4 → 5/2) | note:g ]", + "[ (5/2 → 11/4) ⇝ 3/1 | note:f ]", + "[ 5/2 ⇜ (11/4 → 3/1) | note:g ]", + "[ 3/1 → 10/3 | note:g ]", + "[ 10/3 → 11/3 | note:c ]", + "[ 11/3 → 4/1 | note:d ]", +] +`; + exports[`runs examples > example "pickmodRestart" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 s:piano ]", @@ -10370,10 +10593,10 @@ exports[`runs examples > example "replicate" example index 0 1`] = ` "[ 5/4 → 11/8 | s:sd ]", "[ 3/2 → 13/8 | s:bd ]", "[ 13/8 → 7/4 | s:bd ]", - "[ 15/8 → 2/1 | s:cp ]", + "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:bd ]", "[ 17/8 → 9/4 | s:bd ]", - "[ 19/8 → 5/2 | s:cp ]", + "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:bd ]", "[ 11/4 → 23/8 | s:sd ]", "[ 3/1 → 25/8 | s:bd ]", @@ -11962,6 +12185,336 @@ exports[`runs examples > example "shuffle" example index 1 1`] = ` ] `; +exports[`runs examples > example "signal" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:30 ]", + "[ 1/16 → 1/8 | note:30.625 ]", + "[ 1/8 → 3/16 | note:31.25 ]", + "[ 3/16 → 1/4 | note:31.875 ]", + "[ 1/4 → 5/16 | note:32.5 ]", + "[ 5/16 → 3/8 | note:33.125 ]", + "[ 3/8 → 7/16 | note:33.75 ]", + "[ 7/16 → 1/2 | note:34.375 ]", + "[ 1/2 → 9/16 | note:35 ]", + "[ 9/16 → 5/8 | note:35.625 ]", + "[ 5/8 → 11/16 | note:36.25 ]", + "[ 11/16 → 3/4 | note:36.875 ]", + "[ 3/4 → 13/16 | note:37.5 ]", + "[ 13/16 → 7/8 | note:38.125 ]", + "[ 7/8 → 15/16 | note:38.75 ]", + "[ 15/16 → 1/1 | note:39.375 ]", + "[ 1/1 → 17/16 | note:30 ]", + "[ 17/16 → 9/8 | note:30.625 ]", + "[ 9/8 → 19/16 | note:31.25 ]", + "[ 19/16 → 5/4 | note:31.875 ]", + "[ 5/4 → 21/16 | note:32.5 ]", + "[ 21/16 → 11/8 | note:33.125 ]", + "[ 11/8 → 23/16 | note:33.75 ]", + "[ 23/16 → 3/2 | note:34.375 ]", + "[ 3/2 → 25/16 | note:35 ]", + "[ 25/16 → 13/8 | note:35.625 ]", + "[ 13/8 → 27/16 | note:36.25 ]", + "[ 27/16 → 7/4 | note:36.875 ]", + "[ 7/4 → 29/16 | note:37.5 ]", + "[ 29/16 → 15/8 | note:38.125 ]", + "[ 15/8 → 31/16 | note:38.75 ]", + "[ 31/16 → 2/1 | note:39.375 ]", + "[ 2/1 → 33/16 | note:30 ]", + "[ 33/16 → 17/8 | note:30.625 ]", + "[ 17/8 → 35/16 | note:31.25 ]", + "[ 35/16 → 9/4 | note:31.875 ]", + "[ 9/4 → 37/16 | note:32.5 ]", + "[ 37/16 → 19/8 | note:33.125 ]", + "[ 19/8 → 39/16 | note:33.75 ]", + "[ 39/16 → 5/2 | note:34.375 ]", + "[ 5/2 → 41/16 | note:35 ]", + "[ 41/16 → 21/8 | note:35.625 ]", + "[ 21/8 → 43/16 | note:36.25 ]", + "[ 43/16 → 11/4 | note:36.875 ]", + "[ 11/4 → 45/16 | note:37.5 ]", + "[ 45/16 → 23/8 | note:38.125 ]", + "[ 23/8 → 47/16 | note:38.75 ]", + "[ 47/16 → 3/1 | note:39.375 ]", + "[ 3/1 → 49/16 | note:30 ]", + "[ 49/16 → 25/8 | note:30.625 ]", + "[ 25/8 → 51/16 | note:31.25 ]", + "[ 51/16 → 13/4 | note:31.875 ]", + "[ 13/4 → 53/16 | note:32.5 ]", + "[ 53/16 → 27/8 | note:33.125 ]", + "[ 27/8 → 55/16 | note:33.75 ]", + "[ 55/16 → 7/2 | note:34.375 ]", + "[ 7/2 → 57/16 | note:35 ]", + "[ 57/16 → 29/8 | note:35.625 ]", + "[ 29/8 → 59/16 | note:36.25 ]", + "[ 59/16 → 15/4 | note:36.875 ]", + "[ 15/4 → 61/16 | note:37.5 ]", + "[ 61/16 → 31/8 | note:38.125 ]", + "[ 31/8 → 63/16 | note:38.75 ]", + "[ 63/16 → 4/1 | note:39.375 ]", +] +`; + +exports[`runs examples > example "signal" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | note:45 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 0/1 → 1/16 | note:45 s:gm_epiano gain:1.7 ]", + "[ 0/1 → 1/16 | note:52 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 0/1 → 1/16 | note:52 s:gm_epiano gain:1.7 ]", + "[ 1/16 → 1/8 | note:45.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/16 → 1/8 | note:45.078125 s:gm_epiano gain:1.7 ]", + "[ 1/16 → 1/8 | note:52.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/16 → 1/8 | note:52.078125 s:gm_epiano gain:1.7 ]", + "[ 1/8 → 3/16 | note:45.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/8 → 3/16 | note:45.3125 s:gm_epiano gain:1.7 ]", + "[ 1/8 → 3/16 | note:52.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/8 → 3/16 | note:52.3125 s:gm_epiano gain:1.7 ]", + "[ 3/16 → 1/4 | note:45.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/16 → 1/4 | note:45.703125 s:gm_epiano gain:1.7 ]", + "[ 3/16 → 1/4 | note:52.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/16 → 1/4 | note:52.703125 s:gm_epiano gain:1.7 ]", + "[ 1/4 → 5/16 | note:46.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/4 → 5/16 | note:46.25 s:gm_epiano gain:1.7 ]", + "[ 1/4 → 5/16 | note:53.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/4 → 5/16 | note:53.25 s:gm_epiano gain:1.7 ]", + "[ 5/16 → 3/8 | note:46.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/16 → 3/8 | note:46.953125 s:gm_epiano gain:1.7 ]", + "[ 5/16 → 3/8 | note:53.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/16 → 3/8 | note:53.953125 s:gm_epiano gain:1.7 ]", + "[ 3/8 → 7/16 | note:47.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/8 → 7/16 | note:47.8125 s:gm_epiano gain:1.7 ]", + "[ 3/8 → 7/16 | note:54.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/8 → 7/16 | note:54.8125 s:gm_epiano gain:1.7 ]", + "[ 7/16 → 1/2 | note:48.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/16 → 1/2 | note:48.828125 s:gm_epiano gain:1.7 ]", + "[ 7/16 → 1/2 | note:55.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/16 → 1/2 | note:55.828125 s:gm_epiano gain:1.7 ]", + "[ 1/2 → 9/16 | note:50 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/2 → 9/16 | note:50 s:gm_epiano gain:1.7 ]", + "[ 1/2 → 9/16 | note:57 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/2 → 9/16 | note:57 s:gm_epiano gain:1.7 ]", + "[ 9/16 → 5/8 | note:51.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 9/16 → 5/8 | note:51.328125 s:gm_epiano gain:1.7 ]", + "[ 9/16 → 5/8 | note:58.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 9/16 → 5/8 | note:58.328125 s:gm_epiano gain:1.7 ]", + "[ 5/8 → 11/16 | note:52.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/8 → 11/16 | note:52.8125 s:gm_epiano gain:1.7 ]", + "[ 5/8 → 11/16 | note:59.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/8 → 11/16 | note:59.8125 s:gm_epiano gain:1.7 ]", + "[ 11/16 → 3/4 | note:54.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 11/16 → 3/4 | note:54.453125 s:gm_epiano gain:1.7 ]", + "[ 11/16 → 3/4 | note:61.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 11/16 → 3/4 | note:61.453125 s:gm_epiano gain:1.7 ]", + "[ 3/4 → 13/16 | note:56.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/4 → 13/16 | note:56.25 s:gm_epiano gain:1.7 ]", + "[ 3/4 → 13/16 | note:63.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/4 → 13/16 | note:63.25 s:gm_epiano gain:1.7 ]", + "[ 13/16 → 7/8 | note:58.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 13/16 → 7/8 | note:58.203125 s:gm_epiano gain:1.7 ]", + "[ 13/16 → 7/8 | note:65.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 13/16 → 7/8 | note:65.203125 s:gm_epiano gain:1.7 ]", + "[ 7/8 → 15/16 | note:60.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/8 → 15/16 | note:60.3125 s:gm_epiano gain:1.7 ]", + "[ 7/8 → 15/16 | note:67.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/8 → 15/16 | note:67.3125 s:gm_epiano gain:1.7 ]", + "[ 15/16 → 1/1 | note:62.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 15/16 → 1/1 | note:62.578125 s:gm_epiano gain:1.7 ]", + "[ 15/16 → 1/1 | note:69.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 15/16 → 1/1 | note:69.578125 s:gm_epiano gain:1.7 ]", + "[ 1/1 → 17/16 | note:45 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/1 → 17/16 | note:45 s:gm_epiano gain:1.7 ]", + "[ 1/1 → 17/16 | note:52 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 1/1 → 17/16 | note:52 s:gm_epiano gain:1.7 ]", + "[ 17/16 → 9/8 | note:47.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 17/16 → 9/8 | note:47.578125 s:gm_epiano gain:1.7 ]", + "[ 17/16 → 9/8 | note:54.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 17/16 → 9/8 | note:54.578125 s:gm_epiano gain:1.7 ]", + "[ 9/8 → 19/16 | note:50.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 9/8 → 19/16 | note:50.3125 s:gm_epiano gain:1.7 ]", + "[ 9/8 → 19/16 | note:57.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 9/8 → 19/16 | note:57.3125 s:gm_epiano gain:1.7 ]", + "[ 19/16 → 5/4 | note:53.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 19/16 → 5/4 | note:53.203125 s:gm_epiano gain:1.7 ]", + "[ 19/16 → 5/4 | note:60.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 19/16 → 5/4 | note:60.203125 s:gm_epiano gain:1.7 ]", + "[ 5/4 → 21/16 | note:56.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/4 → 21/16 | note:56.25 s:gm_epiano gain:1.7 ]", + "[ 5/4 → 21/16 | note:63.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/4 → 21/16 | note:63.25 s:gm_epiano gain:1.7 ]", + "[ 21/16 → 11/8 | note:59.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 21/16 → 11/8 | note:59.453125 s:gm_epiano gain:1.7 ]", + "[ 21/16 → 11/8 | note:66.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 21/16 → 11/8 | note:66.453125 s:gm_epiano gain:1.7 ]", + "[ 11/8 → 23/16 | note:62.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 11/8 → 23/16 | note:62.8125 s:gm_epiano gain:1.7 ]", + "[ 11/8 → 23/16 | note:69.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 11/8 → 23/16 | note:69.8125 s:gm_epiano gain:1.7 ]", + "[ 23/16 → 3/2 | note:46.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 23/16 → 3/2 | note:46.328125 s:gm_epiano gain:1.7 ]", + "[ 23/16 → 3/2 | note:53.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 23/16 → 3/2 | note:53.328125 s:gm_epiano gain:1.7 ]", + "[ 3/2 → 25/16 | note:50 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/2 → 25/16 | note:50 s:gm_epiano gain:1.7 ]", + "[ 3/2 → 25/16 | note:57 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/2 → 25/16 | note:57 s:gm_epiano gain:1.7 ]", + "[ 25/16 → 13/8 | note:53.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 25/16 → 13/8 | note:53.828125 s:gm_epiano gain:1.7 ]", + "[ 25/16 → 13/8 | note:60.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 25/16 → 13/8 | note:60.828125 s:gm_epiano gain:1.7 ]", + "[ 13/8 → 27/16 | note:57.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 13/8 → 27/16 | note:57.8125 s:gm_epiano gain:1.7 ]", + "[ 13/8 → 27/16 | note:64.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 13/8 → 27/16 | note:64.8125 s:gm_epiano gain:1.7 ]", + "[ 27/16 → 7/4 | note:61.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 27/16 → 7/4 | note:61.953125 s:gm_epiano gain:1.7 ]", + "[ 27/16 → 7/4 | note:68.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 27/16 → 7/4 | note:68.953125 s:gm_epiano gain:1.7 ]", + "[ 7/4 → 29/16 | note:46.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/4 → 29/16 | note:46.25 s:gm_epiano gain:1.7 ]", + "[ 7/4 → 29/16 | note:53.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/4 → 29/16 | note:53.25 s:gm_epiano gain:1.7 ]", + "[ 29/16 → 15/8 | note:50.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 29/16 → 15/8 | note:50.703125 s:gm_epiano gain:1.7 ]", + "[ 29/16 → 15/8 | note:57.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 29/16 → 15/8 | note:57.703125 s:gm_epiano gain:1.7 ]", + "[ 15/8 → 31/16 | note:55.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 15/8 → 31/16 | note:55.3125 s:gm_epiano gain:1.7 ]", + "[ 15/8 → 31/16 | note:62.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 15/8 → 31/16 | note:62.3125 s:gm_epiano gain:1.7 ]", + "[ 31/16 → 2/1 | note:60.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 31/16 → 2/1 | note:60.078125 s:gm_epiano gain:1.7 ]", + "[ 31/16 → 2/1 | note:67.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 31/16 → 2/1 | note:67.078125 s:gm_epiano gain:1.7 ]", + "[ 2/1 → 33/16 | note:45 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 2/1 → 33/16 | note:45 s:gm_epiano gain:1.7 ]", + "[ 2/1 → 33/16 | note:52 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 2/1 → 33/16 | note:52 s:gm_epiano gain:1.7 ]", + "[ 33/16 → 17/8 | note:50.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 33/16 → 17/8 | note:50.078125 s:gm_epiano gain:1.7 ]", + "[ 33/16 → 17/8 | note:57.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 33/16 → 17/8 | note:57.078125 s:gm_epiano gain:1.7 ]", + "[ 17/8 → 35/16 | note:55.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 17/8 → 35/16 | note:55.3125 s:gm_epiano gain:1.7 ]", + "[ 17/8 → 35/16 | note:62.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 17/8 → 35/16 | note:62.3125 s:gm_epiano gain:1.7 ]", + "[ 35/16 → 9/4 | note:60.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 35/16 → 9/4 | note:60.703125 s:gm_epiano gain:1.7 ]", + "[ 35/16 → 9/4 | note:67.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 35/16 → 9/4 | note:67.703125 s:gm_epiano gain:1.7 ]", + "[ 9/4 → 37/16 | note:46.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 9/4 → 37/16 | note:46.25 s:gm_epiano gain:1.7 ]", + "[ 9/4 → 37/16 | note:53.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 9/4 → 37/16 | note:53.25 s:gm_epiano gain:1.7 ]", + "[ 37/16 → 19/8 | note:51.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 37/16 → 19/8 | note:51.953125 s:gm_epiano gain:1.7 ]", + "[ 37/16 → 19/8 | note:58.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 37/16 → 19/8 | note:58.953125 s:gm_epiano gain:1.7 ]", + "[ 19/8 → 39/16 | note:57.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 19/8 → 39/16 | note:57.8125 s:gm_epiano gain:1.7 ]", + "[ 19/8 → 39/16 | note:64.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 19/8 → 39/16 | note:64.8125 s:gm_epiano gain:1.7 ]", + "[ 39/16 → 5/2 | note:63.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 39/16 → 5/2 | note:63.828125 s:gm_epiano gain:1.7 ]", + "[ 39/16 → 5/2 | note:70.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 39/16 → 5/2 | note:70.828125 s:gm_epiano gain:1.7 ]", + "[ 5/2 → 41/16 | note:50 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/2 → 41/16 | note:50 s:gm_epiano gain:1.7 ]", + "[ 5/2 → 41/16 | note:57 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 5/2 → 41/16 | note:57 s:gm_epiano gain:1.7 ]", + "[ 41/16 → 21/8 | note:56.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 41/16 → 21/8 | note:56.328125 s:gm_epiano gain:1.7 ]", + "[ 41/16 → 21/8 | note:63.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 41/16 → 21/8 | note:63.328125 s:gm_epiano gain:1.7 ]", + "[ 21/8 → 43/16 | note:62.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 21/8 → 43/16 | note:62.8125 s:gm_epiano gain:1.7 ]", + "[ 21/8 → 43/16 | note:69.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 21/8 → 43/16 | note:69.8125 s:gm_epiano gain:1.7 ]", + "[ 43/16 → 11/4 | note:49.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 43/16 → 11/4 | note:49.453125 s:gm_epiano gain:1.7 ]", + "[ 43/16 → 11/4 | note:56.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 43/16 → 11/4 | note:56.453125 s:gm_epiano gain:1.7 ]", + "[ 11/4 → 45/16 | note:56.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 11/4 → 45/16 | note:56.25 s:gm_epiano gain:1.7 ]", + "[ 11/4 → 45/16 | note:63.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 11/4 → 45/16 | note:63.25 s:gm_epiano gain:1.7 ]", + "[ 45/16 → 23/8 | note:63.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 45/16 → 23/8 | note:63.203125 s:gm_epiano gain:1.7 ]", + "[ 45/16 → 23/8 | note:70.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 45/16 → 23/8 | note:70.203125 s:gm_epiano gain:1.7 ]", + "[ 23/8 → 47/16 | note:50.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 23/8 → 47/16 | note:50.3125 s:gm_epiano gain:1.7 ]", + "[ 23/8 → 47/16 | note:57.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 23/8 → 47/16 | note:57.3125 s:gm_epiano gain:1.7 ]", + "[ 47/16 → 3/1 | note:57.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 47/16 → 3/1 | note:57.578125 s:gm_epiano gain:1.7 ]", + "[ 47/16 → 3/1 | note:64.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 47/16 → 3/1 | note:64.578125 s:gm_epiano gain:1.7 ]", + "[ 3/1 → 49/16 | note:45 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/1 → 49/16 | note:45 s:gm_epiano gain:1.7 ]", + "[ 3/1 → 49/16 | note:52 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 3/1 → 49/16 | note:52 s:gm_epiano gain:1.7 ]", + "[ 49/16 → 25/8 | note:52.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 49/16 → 25/8 | note:52.578125 s:gm_epiano gain:1.7 ]", + "[ 49/16 → 25/8 | note:59.578125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 49/16 → 25/8 | note:59.578125 s:gm_epiano gain:1.7 ]", + "[ 25/8 → 51/16 | note:60.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 25/8 → 51/16 | note:60.3125 s:gm_epiano gain:1.7 ]", + "[ 25/8 → 51/16 | note:67.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 25/8 → 51/16 | note:67.3125 s:gm_epiano gain:1.7 ]", + "[ 51/16 → 13/4 | note:48.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 51/16 → 13/4 | note:48.203125 s:gm_epiano gain:1.7 ]", + "[ 51/16 → 13/4 | note:55.203125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 51/16 → 13/4 | note:55.203125 s:gm_epiano gain:1.7 ]", + "[ 13/4 → 53/16 | note:56.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 13/4 → 53/16 | note:56.25 s:gm_epiano gain:1.7 ]", + "[ 13/4 → 53/16 | note:63.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 13/4 → 53/16 | note:63.25 s:gm_epiano gain:1.7 ]", + "[ 53/16 → 27/8 | note:64.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 53/16 → 27/8 | note:64.453125 s:gm_epiano gain:1.7 ]", + "[ 53/16 → 27/8 | note:71.453125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 53/16 → 27/8 | note:71.453125 s:gm_epiano gain:1.7 ]", + "[ 27/8 → 55/16 | note:52.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 27/8 → 55/16 | note:52.8125 s:gm_epiano gain:1.7 ]", + "[ 27/8 → 55/16 | note:59.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 27/8 → 55/16 | note:59.8125 s:gm_epiano gain:1.7 ]", + "[ 55/16 → 7/2 | note:61.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 55/16 → 7/2 | note:61.328125 s:gm_epiano gain:1.7 ]", + "[ 55/16 → 7/2 | note:68.328125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 55/16 → 7/2 | note:68.328125 s:gm_epiano gain:1.7 ]", + "[ 7/2 → 57/16 | note:50 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/2 → 57/16 | note:50 s:gm_epiano gain:1.7 ]", + "[ 7/2 → 57/16 | note:57 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 7/2 → 57/16 | note:57 s:gm_epiano gain:1.7 ]", + "[ 57/16 → 29/8 | note:58.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 57/16 → 29/8 | note:58.828125 s:gm_epiano gain:1.7 ]", + "[ 57/16 → 29/8 | note:65.828125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 57/16 → 29/8 | note:65.828125 s:gm_epiano gain:1.7 ]", + "[ 29/8 → 59/16 | note:47.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 29/8 → 59/16 | note:47.8125 s:gm_epiano gain:1.7 ]", + "[ 29/8 → 59/16 | note:54.8125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 29/8 → 59/16 | note:54.8125 s:gm_epiano gain:1.7 ]", + "[ 59/16 → 15/4 | note:56.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 59/16 → 15/4 | note:56.953125 s:gm_epiano gain:1.7 ]", + "[ 59/16 → 15/4 | note:63.953125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 59/16 → 15/4 | note:63.953125 s:gm_epiano gain:1.7 ]", + "[ 15/4 → 61/16 | note:46.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 15/4 → 61/16 | note:46.25 s:gm_epiano gain:1.7 ]", + "[ 15/4 → 61/16 | note:53.25 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 15/4 → 61/16 | note:53.25 s:gm_epiano gain:1.7 ]", + "[ 61/16 → 31/8 | note:55.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 61/16 → 31/8 | note:55.703125 s:gm_epiano gain:1.7 ]", + "[ 61/16 → 31/8 | note:62.703125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 61/16 → 31/8 | note:62.703125 s:gm_epiano gain:1.7 ]", + "[ 31/8 → 63/16 | note:45.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 31/8 → 63/16 | note:45.3125 s:gm_epiano gain:1.7 ]", + "[ 31/8 → 63/16 | note:52.3125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 31/8 → 63/16 | note:52.3125 s:gm_epiano gain:1.7 ]", + "[ 63/16 → 4/1 | note:55.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 63/16 → 4/1 | note:55.078125 s:gm_epiano gain:1.7 ]", + "[ 63/16 → 4/1 | note:62.078125 s:gm_electric_guitar_jazz gain:1.7 ]", + "[ 63/16 → 4/1 | note:62.078125 s:gm_epiano gain:1.7 ]", +] +`; + exports[`runs examples > example "silence" example index 0 1`] = `[]`; exports[`runs examples > example "sine" example index 0 1`] = ` @@ -12662,6 +13215,494 @@ exports[`runs examples > example "stack" example index 1 1`] = ` ] `; +exports[`runs examples > example "stackCenter" example index 0 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 1/10 → 3/10 | note:C3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 3/10 → 1/2 | note:D3 ]", + "[ 3/10 → 1/2 | note:F3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 1/2 → 7/10 | note:Eb3 ]", + "[ 1/2 → 7/10 | note:G3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 7/10 → 9/10 | note:F3 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 11/10 → 13/10 | note:C3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 13/10 → 3/2 | note:D3 ]", + "[ 13/10 → 3/2 | note:F3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 3/2 → 17/10 | note:Eb3 ]", + "[ 3/2 → 17/10 | note:G3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 17/10 → 19/10 | note:F3 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 21/10 → 23/10 | note:C3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 23/10 → 5/2 | note:D3 ]", + "[ 23/10 → 5/2 | note:F3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 5/2 → 27/10 | note:Eb3 ]", + "[ 5/2 → 27/10 | note:G3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 27/10 → 29/10 | note:F3 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 31/10 → 33/10 | note:C3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 33/10 → 7/2 | note:D3 ]", + "[ 33/10 → 7/2 | note:F3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 7/2 → 37/10 | note:Eb3 ]", + "[ 7/2 → 37/10 | note:G3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 37/10 → 39/10 | note:F3 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackCenter" example index 1 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 1/10 → 3/10 | note:C3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 3/10 → 1/2 | note:D3 ]", + "[ 3/10 → 1/2 | note:F3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 1/2 → 7/10 | note:Eb3 ]", + "[ 1/2 → 7/10 | note:G3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 7/10 → 9/10 | note:F3 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 11/10 → 13/10 | note:C3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 13/10 → 3/2 | note:D3 ]", + "[ 13/10 → 3/2 | note:F3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 3/2 → 17/10 | note:Eb3 ]", + "[ 3/2 → 17/10 | note:G3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 17/10 → 19/10 | note:F3 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 21/10 → 23/10 | note:C3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 23/10 → 5/2 | note:D3 ]", + "[ 23/10 → 5/2 | note:F3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 5/2 → 27/10 | note:Eb3 ]", + "[ 5/2 → 27/10 | note:G3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 27/10 → 29/10 | note:F3 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 31/10 → 33/10 | note:C3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 33/10 → 7/2 | note:D3 ]", + "[ 33/10 → 7/2 | note:F3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 7/2 → 37/10 | note:Eb3 ]", + "[ 7/2 → 37/10 | note:G3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 37/10 → 39/10 | note:F3 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackLeft" example index 0 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 0/1 → 1/5 | note:C3 ]", + "[ 0/1 → 1/5 | note:F3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:D3 ]", + "[ 1/5 → 2/5 | note:G3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 2/5 → 3/5 | note:Eb3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 3/5 → 4/5 | note:F3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 1/1 → 6/5 | note:C3 ]", + "[ 1/1 → 6/5 | note:F3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:D3 ]", + "[ 6/5 → 7/5 | note:G3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 7/5 → 8/5 | note:Eb3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 8/5 → 9/5 | note:F3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 2/1 → 11/5 | note:C3 ]", + "[ 2/1 → 11/5 | note:F3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:D3 ]", + "[ 11/5 → 12/5 | note:G3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 12/5 → 13/5 | note:Eb3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 13/5 → 14/5 | note:F3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 3/1 → 16/5 | note:C3 ]", + "[ 3/1 → 16/5 | note:F3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:D3 ]", + "[ 16/5 → 17/5 | note:G3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 17/5 → 18/5 | note:Eb3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 18/5 → 19/5 | note:F3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackLeft" example index 1 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 0/1 → 1/5 | note:C3 ]", + "[ 0/1 → 1/5 | note:F3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:D3 ]", + "[ 1/5 → 2/5 | note:G3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 2/5 → 3/5 | note:Eb3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 3/5 → 4/5 | note:F3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 1/1 → 6/5 | note:C3 ]", + "[ 1/1 → 6/5 | note:F3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:D3 ]", + "[ 6/5 → 7/5 | note:G3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 7/5 → 8/5 | note:Eb3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 8/5 → 9/5 | note:F3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 2/1 → 11/5 | note:C3 ]", + "[ 2/1 → 11/5 | note:F3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:D3 ]", + "[ 11/5 → 12/5 | note:G3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 12/5 → 13/5 | note:Eb3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 13/5 → 14/5 | note:F3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 3/1 → 16/5 | note:C3 ]", + "[ 3/1 → 16/5 | note:F3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:D3 ]", + "[ 16/5 → 17/5 | note:G3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 17/5 → 18/5 | note:Eb3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 18/5 → 19/5 | note:F3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackRepeat" example index 0 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 0/1 → 1/5 | note:C3 ]", + "[ 0/1 → 1/5 | note:F3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:D3 ]", + "[ 1/5 → 2/5 | note:G3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 2/5 → 3/5 | note:Eb3 ]", + "[ 2/5 → 3/5 | note:F3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 3/5 → 4/5 | note:F3 ]", + "[ 3/5 → 4/5 | note:G3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 4/5 → 1/1 | note:C3 ]", + "[ 4/5 → 1/1 | note:F3 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 1/1 → 6/5 | note:D3 ]", + "[ 1/1 → 6/5 | note:G3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:Eb3 ]", + "[ 6/5 → 7/5 | note:F3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 7/5 → 8/5 | note:F3 ]", + "[ 7/5 → 8/5 | note:G3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 8/5 → 9/5 | note:C3 ]", + "[ 8/5 → 9/5 | note:F3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 9/5 → 2/1 | note:D3 ]", + "[ 9/5 → 2/1 | note:G3 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 2/1 → 11/5 | note:Eb3 ]", + "[ 2/1 → 11/5 | note:F3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:F3 ]", + "[ 11/5 → 12/5 | note:G3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 12/5 → 13/5 | note:C3 ]", + "[ 12/5 → 13/5 | note:F3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 13/5 → 14/5 | note:D3 ]", + "[ 13/5 → 14/5 | note:G3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 14/5 → 3/1 | note:Eb3 ]", + "[ 14/5 → 3/1 | note:F3 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 3/1 → 16/5 | note:F3 ]", + "[ 3/1 → 16/5 | note:G3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:C3 ]", + "[ 16/5 → 17/5 | note:F3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 17/5 → 18/5 | note:D3 ]", + "[ 17/5 → 18/5 | note:G3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 18/5 → 19/5 | note:Eb3 ]", + "[ 18/5 → 19/5 | note:F3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 19/5 → 4/1 | note:F3 ]", + "[ 19/5 → 4/1 | note:G3 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackRepeat" example index 1 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 0/1 → 1/5 | note:C3 ]", + "[ 0/1 → 1/5 | note:F3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:D3 ]", + "[ 1/5 → 2/5 | note:G3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 2/5 → 3/5 | note:Eb3 ]", + "[ 2/5 → 3/5 | note:F3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 3/5 → 4/5 | note:F3 ]", + "[ 3/5 → 4/5 | note:G3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 4/5 → 1/1 | note:C3 ]", + "[ 4/5 → 1/1 | note:F3 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 1/1 → 6/5 | note:D3 ]", + "[ 1/1 → 6/5 | note:G3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:Eb3 ]", + "[ 6/5 → 7/5 | note:F3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 7/5 → 8/5 | note:F3 ]", + "[ 7/5 → 8/5 | note:G3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 8/5 → 9/5 | note:C3 ]", + "[ 8/5 → 9/5 | note:F3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 9/5 → 2/1 | note:D3 ]", + "[ 9/5 → 2/1 | note:G3 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 2/1 → 11/5 | note:Eb3 ]", + "[ 2/1 → 11/5 | note:F3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:F3 ]", + "[ 11/5 → 12/5 | note:G3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 12/5 → 13/5 | note:C3 ]", + "[ 12/5 → 13/5 | note:F3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 13/5 → 14/5 | note:D3 ]", + "[ 13/5 → 14/5 | note:G3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 14/5 → 3/1 | note:Eb3 ]", + "[ 14/5 → 3/1 | note:F3 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 3/1 → 16/5 | note:F3 ]", + "[ 3/1 → 16/5 | note:G3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:C3 ]", + "[ 16/5 → 17/5 | note:F3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 17/5 → 18/5 | note:D3 ]", + "[ 17/5 → 18/5 | note:G3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 18/5 → 19/5 | note:Eb3 ]", + "[ 18/5 → 19/5 | note:F3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 19/5 → 4/1 | note:F3 ]", + "[ 19/5 → 4/1 | note:G3 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackRight" example index 0 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:C3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 2/5 → 3/5 | note:D3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 3/5 → 4/5 | note:Eb3 ]", + "[ 3/5 → 4/5 | note:F3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 4/5 → 1/1 | note:F3 ]", + "[ 4/5 → 1/1 | note:G3 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:C3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 7/5 → 8/5 | note:D3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 8/5 → 9/5 | note:Eb3 ]", + "[ 8/5 → 9/5 | note:F3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 9/5 → 2/1 | note:F3 ]", + "[ 9/5 → 2/1 | note:G3 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:C3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 12/5 → 13/5 | note:D3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 13/5 → 14/5 | note:Eb3 ]", + "[ 13/5 → 14/5 | note:F3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 14/5 → 3/1 | note:F3 ]", + "[ 14/5 → 3/1 | note:G3 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:C3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 17/5 → 18/5 | note:D3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 18/5 → 19/5 | note:Eb3 ]", + "[ 18/5 → 19/5 | note:F3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 19/5 → 4/1 | note:F3 ]", + "[ 19/5 → 4/1 | note:G3 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "stackRight" example index 1 1`] = ` +[ + "[ 0/1 → 1/15 | note:G3 ]", + "[ 1/15 → 2/15 | note:Ab3 ]", + "[ 2/15 → 1/5 | note:Bb3 ]", + "[ 1/5 → 2/5 | note:C3 ]", + "[ 1/5 → 2/5 | note:C4 ]", + "[ 2/5 → 3/5 | note:D3 ]", + "[ 2/5 → 3/5 | note:D4 ]", + "[ 3/5 → 4/5 | note:Eb3 ]", + "[ 3/5 → 4/5 | note:F3 ]", + "[ 3/5 → 4/5 | note:Eb4 ]", + "[ 4/5 → 1/1 | note:F3 ]", + "[ 4/5 → 1/1 | note:G3 ]", + "[ 4/5 → 1/1 | note:F4 ]", + "[ 1/1 → 16/15 | note:G3 ]", + "[ 16/15 → 17/15 | note:Ab3 ]", + "[ 17/15 → 6/5 | note:Bb3 ]", + "[ 6/5 → 7/5 | note:C3 ]", + "[ 6/5 → 7/5 | note:C4 ]", + "[ 7/5 → 8/5 | note:D3 ]", + "[ 7/5 → 8/5 | note:D4 ]", + "[ 8/5 → 9/5 | note:Eb3 ]", + "[ 8/5 → 9/5 | note:F3 ]", + "[ 8/5 → 9/5 | note:Eb4 ]", + "[ 9/5 → 2/1 | note:F3 ]", + "[ 9/5 → 2/1 | note:G3 ]", + "[ 9/5 → 2/1 | note:F4 ]", + "[ 2/1 → 31/15 | note:G3 ]", + "[ 31/15 → 32/15 | note:Ab3 ]", + "[ 32/15 → 11/5 | note:Bb3 ]", + "[ 11/5 → 12/5 | note:C3 ]", + "[ 11/5 → 12/5 | note:C4 ]", + "[ 12/5 → 13/5 | note:D3 ]", + "[ 12/5 → 13/5 | note:D4 ]", + "[ 13/5 → 14/5 | note:Eb3 ]", + "[ 13/5 → 14/5 | note:F3 ]", + "[ 13/5 → 14/5 | note:Eb4 ]", + "[ 14/5 → 3/1 | note:F3 ]", + "[ 14/5 → 3/1 | note:G3 ]", + "[ 14/5 → 3/1 | note:F4 ]", + "[ 3/1 → 46/15 | note:G3 ]", + "[ 46/15 → 47/15 | note:Ab3 ]", + "[ 47/15 → 16/5 | note:Bb3 ]", + "[ 16/5 → 17/5 | note:C3 ]", + "[ 16/5 → 17/5 | note:C4 ]", + "[ 17/5 → 18/5 | note:D3 ]", + "[ 17/5 → 18/5 | note:D4 ]", + "[ 18/5 → 19/5 | note:Eb3 ]", + "[ 18/5 → 19/5 | note:F3 ]", + "[ 18/5 → 19/5 | note:Eb4 ]", + "[ 19/5 → 4/1 | note:F3 ]", + "[ 19/5 → 4/1 | note:G3 ]", + "[ 19/5 → 4/1 | note:F4 ]", +] +`; + exports[`runs examples > example "stepalt" example index 0 1`] = ` [ "[ 0/1 → 1/5 | s:bd ]", @@ -14739,6 +15780,64 @@ exports[`runs examples > example "withBase" example index 1 1`] = ` ] `; +exports[`runs examples > example "withHap" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c s:gm_piano ]", + "[ 1/4 → 1/2 | note:a s:gm_piano n:2 ]", + "[ 1/2 → 3/4 | note:f s:gm_piano n:2 ]", + "[ 3/4 → 1/1 | note:e s:gm_piano n:2 ]", + "[ 1/1 → 5/4 | note:c s:gm_piano ]", + "[ 5/4 → 3/2 | note:a s:gm_piano n:2 ]", + "[ 3/2 → 7/4 | note:f s:gm_piano n:2 ]", + "[ 7/4 → 2/1 | note:e s:gm_piano n:2 ]", + "[ 2/1 → 9/4 | note:c s:gm_piano ]", + "[ 9/4 → 5/2 | note:a s:gm_piano n:2 ]", + "[ 5/2 → 11/4 | note:f s:gm_piano n:2 ]", + "[ 11/4 → 3/1 | note:e s:gm_piano n:2 ]", + "[ 3/1 → 13/4 | note:c s:gm_piano ]", + "[ 13/4 → 7/2 | note:a s:gm_piano n:2 ]", + "[ 7/2 → 15/4 | note:f s:gm_piano n:2 ]", + "[ 15/4 → 4/1 | note:e s:gm_piano n:2 ]", +] +`; + +exports[`runs examples > example "withHaps" example index 0 1`] = ` +[ + "[ 0/1 → 1/10 | n:1 note:a s:gm_piano ]", + "[ 0/1 → 1/10 | n:1 note:c s:gm_piano ]", + "[ 1/10 → 1/5 | n:1 note:b s:gm_piano ]", + "[ 1/10 → 1/5 | n:1 note:d s:gm_piano ]", + "[ 1/5 → 2/5 | n:1 note:a s:gm_piano ]", + "[ 2/5 → 3/5 | n:1 note:b s:gm_piano ]", + "[ 3/5 → 4/5 | n:1 note:c s:gm_piano ]", + "[ 4/5 → 1/1 | n:1 note:d s:gm_piano ]", + "[ 1/1 → 11/10 | n:1 note:a s:gm_piano ]", + "[ 1/1 → 11/10 | n:1 note:c s:gm_piano ]", + "[ 11/10 → 6/5 | n:1 note:b s:gm_piano ]", + "[ 11/10 → 6/5 | n:1 note:d s:gm_piano ]", + "[ 6/5 → 7/5 | n:1 note:a s:gm_piano ]", + "[ 7/5 → 8/5 | n:1 note:b s:gm_piano ]", + "[ 8/5 → 9/5 | n:1 note:c s:gm_piano ]", + "[ 9/5 → 2/1 | n:1 note:d s:gm_piano ]", + "[ 2/1 → 21/10 | n:1 note:a s:gm_piano ]", + "[ 2/1 → 21/10 | n:1 note:c s:gm_piano ]", + "[ 21/10 → 11/5 | n:1 note:b s:gm_piano ]", + "[ 21/10 → 11/5 | n:1 note:d s:gm_piano ]", + "[ 11/5 → 12/5 | n:1 note:a s:gm_piano ]", + "[ 12/5 → 13/5 | n:1 note:b s:gm_piano ]", + "[ 13/5 → 14/5 | n:1 note:c s:gm_piano ]", + "[ 14/5 → 3/1 | n:1 note:d s:gm_piano ]", + "[ 3/1 → 31/10 | n:1 note:a s:gm_piano ]", + "[ 3/1 → 31/10 | n:1 note:c s:gm_piano ]", + "[ 31/10 → 16/5 | n:1 note:b s:gm_piano ]", + "[ 31/10 → 16/5 | n:1 note:d s:gm_piano ]", + "[ 16/5 → 17/5 | n:1 note:a s:gm_piano ]", + "[ 17/5 → 18/5 | n:1 note:b s:gm_piano ]", + "[ 18/5 → 19/5 | n:1 note:c s:gm_piano ]", + "[ 19/5 → 4/1 | n:1 note:d s:gm_piano ]", +] +`; + exports[`runs examples > example "withValue" example index 0 1`] = ` [ "[ 0/1 → 1/3 | 10 ]", @@ -14758,54 +15857,54 @@ 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 ]", + "[ 0/1 → 1/8 | s:wt_digital note:F1 wt:0 ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:wt_digital note:F1 wt:0 ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:wt_digital note:F1 wt:0.25 ]", + "[ 1/4 → 3/8 | s:wt_digital note:F1 wt:0.25 ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:wt_digital note:F1 wt:0.25 ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:wt_digital note:F1 wt:0.5 ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:wt_digital note:F1 wt:0.5 ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:wt_digital note:F1 wt:0.75 ]", + "[ 5/8 → 3/4 | s:wt_digital note:F1 wt:0.75 ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:wt_digital note:F1 wt:0.75 ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:wt_digital note:F1 wt:1 ]", + "[ 7/8 → 1/1 | s:wt_digital note:F1 wt:1 ]", + "[ 1/1 → 9/8 | s:wt_digital note:F1 wt:0 ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:wt_digital note:F1 wt:0 ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:wt_digital note:F1 wt:0.25 ]", + "[ 5/4 → 11/8 | s:wt_digital note:F1 wt:0.25 ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:wt_digital note:F1 wt:0.25 ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:wt_digital note:F1 wt:0.5 ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:wt_digital note:F1 wt:0.5 ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:wt_digital note:F1 wt:0.75 ]", + "[ 13/8 → 7/4 | s:wt_digital note:F1 wt:0.75 ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:wt_digital note:F1 wt:0.75 ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:wt_digital note:F1 wt:1 ]", + "[ 15/8 → 2/1 | s:wt_digital note:F1 wt:1 ]", + "[ 2/1 → 17/8 | s:wt_digital note:F1 wt:0 ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:wt_digital note:F1 wt:0 ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:wt_digital note:F1 wt:0.25 ]", + "[ 9/4 → 19/8 | s:wt_digital note:F1 wt:0.25 ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:wt_digital note:F1 wt:0.25 ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:wt_digital note:F1 wt:0.5 ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:wt_digital note:F1 wt:0.5 ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:wt_digital note:F1 wt:0.75 ]", + "[ 21/8 → 11/4 | s:wt_digital note:F1 wt:0.75 ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:wt_digital note:F1 wt:0.75 ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:wt_digital note:F1 wt:1 ]", + "[ 23/8 → 3/1 | s:wt_digital note:F1 wt:1 ]", + "[ 3/1 → 25/8 | s:wt_digital note:F1 wt:0 ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:wt_digital note:F1 wt:0 ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:wt_digital note:F1 wt:0.25 ]", + "[ 13/4 → 27/8 | s:wt_digital note:F1 wt:0.25 ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:wt_digital note:F1 wt:0.25 ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:wt_digital note:F1 wt:0.5 ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:wt_digital note:F1 wt:0.5 ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:wt_digital note:F1 wt:0.75 ]", + "[ 29/8 → 15/4 | s:wt_digital note:F1 wt:0.75 ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:wt_digital note:F1 wt:0.75 ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:wt_digital note:F1 wt:1 ]", + "[ 31/8 → 4/1 | s:wt_digital note:F1 wt:1 ]", ] `; @@ -14913,6 +16012,43 @@ exports[`runs examples > example "xen" example index 1 1`] = ` `; exports[`runs examples > example "xen" example index 2 1`] = ` +[ + "[ 0/1 → 1/8 | freq:220 s:tri ]", + "[ 0/1 → 1/2 | freq:330 s:tri ]", + "[ 1/8 → 1/4 | freq:275 s:tri ]", + "[ 3/8 → 1/2 | freq:366.6666667 s:tri ]", + "[ 1/2 → 5/8 | freq:220 s:tri ]", + "[ 5/8 → 3/4 | freq:275 s:tri ]", + "[ 3/4 → 7/8 | freq:293.3333333 s:tri ]", + "[ 7/8 → 1/1 | freq:366.6666667 s:tri ]", + "[ 1/1 → 9/8 | freq:220 s:tri ]", + "[ 1/1 → 3/2 | freq:330 s:tri ]", + "[ 9/8 → 5/4 | freq:275 s:tri ]", + "[ 11/8 → 3/2 | freq:366.6666667 s:tri ]", + "[ 3/2 → 13/8 | freq:220 s:tri ]", + "[ 13/8 → 7/4 | freq:275 s:tri ]", + "[ 7/4 → 15/8 | freq:293.3333333 s:tri ]", + "[ 15/8 → 2/1 | freq:366.6666667 s:tri ]", + "[ 2/1 → 17/8 | freq:220 s:tri ]", + "[ 2/1 → 5/2 | freq:330 s:tri ]", + "[ 17/8 → 9/4 | freq:275 s:tri ]", + "[ 19/8 → 5/2 | freq:366.6666667 s:tri ]", + "[ 5/2 → 21/8 | freq:220 s:tri ]", + "[ 21/8 → 11/4 | freq:275 s:tri ]", + "[ 11/4 → 23/8 | freq:293.3333333 s:tri ]", + "[ 23/8 → 3/1 | freq:366.6666667 s:tri ]", + "[ 3/1 → 25/8 | freq:220 s:tri ]", + "[ 3/1 → 7/2 | freq:330 s:tri ]", + "[ 25/8 → 13/4 | freq:275 s:tri ]", + "[ 27/8 → 7/2 | freq:366.6666667 s:tri ]", + "[ 7/2 → 29/8 | freq:220 s:tri ]", + "[ 29/8 → 15/4 | freq:275 s:tri ]", + "[ 15/4 → 31/8 | freq:293.3333333 s:tri ]", + "[ 31/8 → 4/1 | freq:366.6666667 s:tri ]", +] +`; + +exports[`runs examples > example "xen" example index 3 1`] = ` [ "[ 0/1 → 1/6 | freq:220 ]", "[ 1/6 → 1/3 | freq:275 ]", @@ -14941,7 +16077,7 @@ exports[`runs examples > example "xen" example index 2 1`] = ` ] `; -exports[`runs examples > example "xen" example index 3 1`] = ` +exports[`runs examples > example "xen" example index 4 1`] = ` [ "[ 0/1 → 1/8 | freq:220 ]", "[ 1/8 → 1/4 | freq:252.7136381 ]", diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 1fb178801..d33592bb4 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -7322,12 +7322,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` [ "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.598990320228 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.836992966942 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 0/1 → 3/2 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.921303892508 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.842607785016 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 3/2 → 9/4 | note:F2 s:bass clip:1 gain:0.8 ]", - "[ 9/4 → 3/1 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.700696257874 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.650794394314 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", @@ -7339,12 +7339,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.875811336562 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 6/1 → 15/2 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.75977108255 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.774316444062 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.836244757287 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 15/2 → 33/4 | note:D2 s:bass clip:1 gain:0.8 ]", - "[ 33/4 → 9/1 | note:D2 s:bass clip:1 gain:0.8 ]", + "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.59140187595 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", @@ -7356,12 +7356,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.734787144698 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 12/1 → 27/2 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.979763559997 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.666239210404 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.95169514697 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 27/2 → 57/4 | note:A2 s:bass clip:1 gain:0.8 ]", - "[ 57/4 → 15/1 | note:A2 s:bass clip:1 gain:0.8 ]", + "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.918253320269 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.571157105267 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", @@ -7373,12 +7373,12 @@ exports[`renders tunes > tune: randomBells 1`] = ` "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.945647025481 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 18/1 → 39/2 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.608644555323 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.506259439513 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.671621939167 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", - "[ 39/2 → 81/4 | note:G2 s:bass clip:1 gain:0.8 ]", - "[ 81/4 → 21/1 | note:G2 s:bass clip:1 gain:0.8 ]", + "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]", + "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.704345900565 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.887838857248 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 09ebb7692..69c5c0b2a 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -22,7 +22,7 @@ export function MiniRepl({ tunes, hideHeader = false, canvasHeight = 100, - onTrigger, + onSchedule, punchcard, punchcardLabels = true, claviature, @@ -48,7 +48,7 @@ export function MiniRepl({ const editor = new StrudelMirror({ id, - defaultOutput: webaudioOutput, + onSchedule: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, autodraw, @@ -58,8 +58,8 @@ export function MiniRepl({ drawTime, drawContext, editPattern: (pat, id) => { - if (onTrigger) { - pat = pat.onTrigger(onTrigger, false); + if (onSchedule) { + pat = pat.onSchedule(onSchedule, false); } if (claviature) { pat = pat.onPaint((ctx, time, haps, drawTime) => { diff --git a/website/src/layouts/MainLayout.astro b/website/src/layouts/MainLayout.astro index c6f6653ce..f56d7f58d 100644 --- a/website/src/layouts/MainLayout.astro +++ b/website/src/layouts/MainLayout.astro @@ -36,7 +36,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
-
+
diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 517eb658f..9f8a3370c 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -33,7 +33,7 @@ The signal chain in Strudel is as follows: - This is where the main (volume) ADSR happens - A lowpass filter (`lpf`) - A highpass filter (`hpf`) - - A bandpass filter (`bandpass`) + - A bandpass filter (`bpf`) - A vowel filter (`vowel`) - Sample rate reduction (`coarse`) - Bit crushing (`crush`) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index 3f9dc730b..d136408f0 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -346,3 +346,11 @@ s increase by one semitone, i.e. sharp, works for steps of scales, note names $: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names _ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage. ``` + +## What is a 'hap'? + +A 'hap' is a name for a thing that happens, e.g. `note("c3 -")` playing a note and then stopping after a while. Haps have a start and an end. Every separate sound that is produced by strudel is a hap on its own. + +We would have called it 'event' if this particular name wasn't used by JavaScript to describe things that happen on html pages, like clicking on a button or hovering over a line of text. + +Since strudel is written in JavaScript, this confusion of having the same name for different things is avoided by calling events 'haps'. diff --git a/website/src/pages/learn/signals.mdx b/website/src/pages/learn/signals.mdx index 527e42bef..501135b29 100644 --- a/website/src/pages/learn/signals.mdx +++ b/website/src/pages/learn/signals.mdx @@ -55,6 +55,14 @@ There is also `saw2`, `sine2`, `cosine2`, `tri2`, `square2` and `rand2` which ha +## time + + + +## creating your own signal + + + ## mouseX diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 144a40f1a..3e6a5b12f 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -12,6 +12,12 @@ import { JsDoc } from '../../docs/JsDoc'; These functions allow the use of scales other than your typical chromatic 12 based ones. +### xen(scaleOrRatios) + +{/* TODO add explanation of EDO to documentation */} + + + ### tune(scale) {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} @@ -110,9 +116,3 @@ You can also give tune a list of frequencies to use as the scale: 523.2511306012 ]).mul(220).freq();`} /> - -### xen(scaleOrRatios) - -{/* TODO add explanation of EDO to documentation */} - - diff --git a/website/src/pages/technical-manual/internals.mdx b/website/src/pages/technical-manual/internals.mdx index 2ec74641a..f3fc1122a 100644 --- a/website/src/pages/technical-manual/internals.mdx +++ b/website/src/pages/technical-manual/internals.mdx @@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder. # Other -## onTrigger +## onSchedule - + ## log diff --git a/website/src/pages/technical-manual/patterns.mdx b/website/src/pages/technical-manual/patterns.mdx index 49ede526c..da97b2157 100644 --- a/website/src/pages/technical-manual/patterns.mdx +++ b/website/src/pages/technical-manual/patterns.mdx @@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc'; # Patterns -Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time. +Patterns are the essence of Uzu languages such as Strudel and Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time. From now on, this process of generating events from a time span will be called **querying**. Example: diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index d8a22cf55..4135848ed 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP 1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance 2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span. -3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output. +3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output. @@ -134,10 +134,7 @@ let minLatency = 0.1; // min time before a hap should trigger setInterval(() => { const haps = pattern.queryArc(time, time + interval); time += interval; // increment time - haps.forEach((hap) => { - const deadline = hap.whole.begin - time + minLatency; - onTrigger(hap, deadline, duration); - }); + haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency)); }, interval * 1000); // query each "interval" seconds ``` @@ -181,20 +178,19 @@ This example creates the custom control parameters `x` and `y` which are then us ### Outputs -Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this: +Now that we know how the value of an hap is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onSchedule` function which is used to implement the output. A very simple version of the web audio output could look like this: ```js -function onTrigger(hap, deadline, duration) { +function onSchedule(hap, time, cps) { const { note } = hap.value; - const time = getAudioContext().currentTime + deadline; const o = getAudioContext().createOscillator(); o.frequency.value = getFreq(note); o.start(time); - o.stop(time + event.duration); + o.stop(time + hap.duration / cps); o.connect(getAudioContext().destination); } ``` -The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects. +The above example will create an `OscillatorNode` for each hap, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects. I want to help, how do I contribute to the [Docs](/technical-manual/docs)? diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index 2876388a8..7657312ea 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -628,7 +628,7 @@ export const debugAudiograph = async (argOptions = {}) => { /*global all*/ all((pat) => - pat.onTrigger(async (hap, duration, cps, t) => { + pat.onSchedule(async (hap, t, cps) => { hap_count++; const key = Object.entries(hap.value) .map((param) => param.join('/')) @@ -643,7 +643,7 @@ export const debugAudiograph = async (argOptions = {}) => { return; } - await webaudioOutput(hap, t, hap.duration / cps, cps, t); + await webaudioOutput(hap, t, cps); if (hapsBatch && hap_count % hapsBatch === 0) drawDiagram(); }), diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 852c63f88..62e14456d 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -68,7 +68,7 @@ const initialCode = `// LOADING`; export function useReplContext() { const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; - const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; + const onSchedule = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; const init = useCallback(() => { setActivePattern(getViewingPatternData().id); @@ -76,7 +76,7 @@ export function useReplContext() { const drawContext = getDrawContext(); const editor = new StrudelMirror({ sync: isSyncEnabled, - defaultOutput, + onSchedule, getTime, setInterval, clearInterval, diff --git a/website/src/settings.mjs b/website/src/settings.mjs index d2f57afc9..cd883f81f 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -138,7 +138,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) => register(key, (value, pat) => - pat.onTrigger(() => { + pat.onSchedule(() => { value = Array.isArray(value) ? value.join(' ') : value; if (value !== settingsMap.get()[key]) { settingsMap.setKey(key, value);