diff --git a/bench/tunes.bench.mjs b/bench/tunes.bench.mjs new file mode 100644 index 000000000..cd381873d --- /dev/null +++ b/bench/tunes.bench.mjs @@ -0,0 +1,22 @@ +import { queryCode, testCycles } from '../test/runtime.mjs'; +import * as tunes from '../website/src/repl/tunes.mjs'; +import { describe, bench } from 'vitest'; +import { calculateTactus } from '../packages/core/index.mjs'; + +const tuneKeys = Object.keys(tunes); + +describe('renders tunes', () => { + tuneKeys.forEach((key) => { + describe(key, () => { + calculateTactus(true); + bench(`+tactus`, async () => { + await queryCode(tunes[key], testCycles[key] || 1); + }); + calculateTactus(false); + bench(`-tactus`, async () => { + await queryCode(tunes[key], testCycles[key] || 1); + }); + calculateTactus(true); + }); + }); +}); diff --git a/package.json b/package.json index 979009130..8aefa1023 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "npm run pretest && vitest run --version", "test-ui": "npm run pretest && vitest --ui", "test-coverage": "npm run pretest && vitest --coverage", + "bench": "npm run pretest && vitest bench", "snapshot": "npm run pretest && vitest run -u --silent", "repl": "npm run prestart && cd website && npm run dev", "start": "npm run prestart && cd website && npm run dev", diff --git a/packages/core/bench/pattern.bench.mjs b/packages/core/bench/pattern.bench.mjs new file mode 100644 index 000000000..12644800b --- /dev/null +++ b/packages/core/bench/pattern.bench.mjs @@ -0,0 +1,46 @@ +import { describe, bench } from 'vitest'; + +import { calculateTactus, sequence, stack } from '../index.mjs'; + +const pat64 = sequence(...Array(64).keys()); + +describe('tactus', () => { + calculateTactus(true); + bench( + '+tactus', + () => { + pat64.iter(64).fast(64).firstCycle(); + }, + { time: 1000 }, + ); + + calculateTactus(false); + bench( + '-tactus', + () => { + pat64.iter(64).fast(64).firstCycle(); + }, + { time: 1000 }, + ); +}); + +describe('stack', () => { + calculateTactus(true); + bench( + '+tactus', + () => { + stack(pat64, pat64, pat64, pat64, pat64, pat64, pat64, pat64).fast(64).firstCycle(); + }, + { time: 1000 }, + ); + + calculateTactus(false); + bench( + '-tactus', + () => { + stack(pat64, pat64, pat64, pat64, pat64, pat64, pat64, pat64).fast(64).firstCycle(); + }, + { time: 1000 }, + ); +}); +calculateTactus(true); diff --git a/packages/core/package.json b/packages/core/package.json index 7f59ed416..6c95d0492 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -9,6 +9,7 @@ }, "scripts": { "test": "vitest run", + "bench": "vitest bench", "build": "vite build", "prepublishOnly": "pnpm build" }, diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 7291c12fd..03ce2e2d9 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -27,6 +27,12 @@ import { logger } from './logger.mjs'; let stringParser; +let __tactus = true; + +export const calculateTactus = function (x) { + __tactus = x ? true : false; +}; + // parser is expected to turn a string into a pattern // if set, the reify function will parse all strings with it // intended to use with mini to automatically interpret all strings as mini notation @@ -60,6 +66,9 @@ export class Pattern { } withTactus(f) { + if (!__tactus) { + return this; + } return new Pattern(this.query, this.tactus === undefined ? undefined : f(this.tactus)); } @@ -149,7 +158,9 @@ export class Pattern { return span_a.intersection_e(span_b); }; const result = pat_func.appWhole(whole_func, pat_val); - result.tactus = lcm(pat_val.tactus, pat_func.tactus); + if (__tactus) { + result.tactus = lcm(pat_val.tactus, pat_func.tactus); + } return result; } @@ -1254,7 +1265,9 @@ export function stack(...pats) { pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); const query = (state) => flatten(pats.map((pat) => pat.query(state))); const result = new Pattern(query); - result.tactus = lcm(...pats.map((pat) => pat.tactus)); + if (__tactus) { + result.tactus = lcm(...pats.map((pat) => pat.tactus)); + } return result; } @@ -1267,7 +1280,7 @@ function _stackWith(func, pats) { return pats[0]; } const [left, ...right] = pats.map((pat) => pat.tactus); - const tactus = left.maximum(...right); + const tactus = __tactus ? left.maximum(...right) : undefined; return stack(...func(tactus, pats)); } @@ -1347,7 +1360,7 @@ export function slowcat(...pats) { const offset = span.begin.floor().sub(span.begin.div(pats.length).floor()); return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset)))); }; - const tactus = lcm(...pats.map((x) => x.tactus)); + const tactus = __tactus ? lcm(...pats.map((x) => x.tactus)) : undefined; return new Pattern(query).splitQueries().setTactus(tactus); } @@ -1766,7 +1779,9 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun */ export const ply = register('ply', function (factor, pat) { const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); - result.tactus = Fraction(factor).mulmaybe(pat.tactus); + if (__tactus) { + result.tactus = Fraction(factor).mulmaybe(pat.tactus); + } return result; }); @@ -1781,16 +1796,19 @@ export const ply = register('ply', function (factor, pat) { * @example * s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2") */ -export const { fast, density } = register(['fast', 'density'], function (factor, pat) { - if (factor === 0) { - return silence; - } - factor = Fraction(factor); - const fastQuery = pat.withQueryTime((t) => t.mul(factor)); - const result = fastQuery.withHapTime((t) => t.div(factor)); - result.tactus = factor.mulmaybe(pat.tactus); - return result; -}); +export const { fast, density } = register( + ['fast', 'density'], + function (factor, pat) { + if (factor === 0) { + return silence; + } + factor = Fraction(factor); + const fastQuery = pat.withQueryTime((t) => t.mul(factor)); + return fastQuery.withHapTime((t) => t.div(factor)).setTactus(pat.tactus); + }, + true, + true, +); /** * Both speeds up the pattern (like 'fast') and the sample playback (like 'speed'). @@ -1958,7 +1976,7 @@ export const zoom = register('zoom', function (s, e, pat) { return nothing; } const d = e.sub(s); - const tactus = pat.tactus.mulmaybe(d); + const tactus = __tactus ? pat.tactus.mulmaybe(d) : undefined; return pat .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) @@ -2173,7 +2191,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }))); - return stack(left, right).setTactus(lcm(left.tactus, right.tactus)); + return stack(left, right).setTactus(__tactus ? lcm(left.tactus, right.tactus) : undefined); }); /** @@ -2292,7 +2310,13 @@ export const { iterBack, iterback } = register( export const { repeatCycles } = register( 'repeatCycles', function (n, pat) { - return slowcat(...Array(n).fill(pat)); + return new Pattern(function (state) { + const cycle = state.span.begin.sam(); + const source_cycle = cycle.div(n).sam(); + const delta = cycle.sub(source_cycle); + state = state.withSpan((span) => span.withTime((spant) => spant.sub(delta))); + return pat.query(state).map((hap) => hap.withSpan((span) => span.withTime((spant) => spant.add(delta)))); + }).splitQueries(); }, true, true, @@ -2773,7 +2797,7 @@ export const chop = register('chop', function (n, pat) { const func = function (o) { return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); }; - return pat.squeezeBind(func).setTactus(Fraction(n).mulmaybe(pat.tactus)); + return pat.squeezeBind(func).setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined); }); /** @@ -2788,7 +2812,10 @@ export const striate = register('striate', function (n, pat) { const slices = Array.from({ length: n }, (x, i) => i); const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); const slicePat = slowcat(...slice_objects); - return pat.set(slicePat)._fast(n); + return pat + .set(slicePat) + ._fast(n) + .setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined); }); /** diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 4bed34cb8..845ed00ab 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -51,6 +51,7 @@ import { stackRight, stackCentre, s_cat, + calculateTactus, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1127,8 +1128,8 @@ describe('Pattern', () => { it('Is correctly preserved/calculated through transformations', () => { expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4)); expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4)); - expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(16)); - expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16)); + expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4)); + expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4)); expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4)); expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10)); expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3)); @@ -1164,7 +1165,7 @@ describe('Pattern', () => { expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3)); expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6)); expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined); - expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(6)); + expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2)); expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined); }); }); diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index a00be74fb..1eeaf6fa5 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((time, hap) => { + return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -38,9 +38,11 @@ export const csound = register('csound', (instrument, pat) => { .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 = time_deprecate - getAudioContext().currentTime const params = [ `"${instrument}"`, // p1: instrument name - time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats + timeOffset, // p2: starting time in arbitrary unit called beats hap.duration + 0, // p3: duration in beats // instrument specific params: freq, //.toFixed(precision), // p4: frequency diff --git a/packages/mini/bench/mini.bench.mjs b/packages/mini/bench/mini.bench.mjs new file mode 100644 index 000000000..782ac86ba --- /dev/null +++ b/packages/mini/bench/mini.bench.mjs @@ -0,0 +1,25 @@ +import { describe, bench } from 'vitest'; + +import { calculateTactus } from '../../core/index.mjs'; +import { mini } from '../index.mjs'; + +describe('mini', () => { + calculateTactus(true); + bench( + '+tactus', + () => { + mini('a b c*3 [c d e, f g] ').fast(64).firstCycle(); + }, + { time: 1000 }, + ); + + calculateTactus(false); + bench( + '-tactus', + () => { + mini('a b c*3 [c d e, f g] ').fast(64).firstCycle(); + }, + { time: 1000 }, + ); + calculateTactus(true); +}); diff --git a/packages/mini/krill-parser.js b/packages/mini/krill-parser.js index 7762242d5..1cdd34867 100644 --- a/packages/mini/krill-parser.js +++ b/packages/mini/krill-parser.js @@ -295,7 +295,15 @@ function peg$parse(input, options) { var peg$f6 = function(a) { return a }; var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; }; var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }; - var peg$f9 = function(a) { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }; + var peg$f9 = function(a) { return x => {const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1; + x.options_['reps'] = reps; + console.log("reps: ", reps) + x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate"); + x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }}); + x.options_['weight'] = reps; + console.log("options: ", x.options_); + } + }; var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }; var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }; var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }; diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index 35d7bdc42..c1349ca52 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -135,7 +135,14 @@ op_weight = ws ("@" / "_") a:number? { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 } op_replicate = ws "!" a:number? - { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 } + { return x => {// A bit fiddly, to support both x!4 and x!!! as equivalent.. + const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1; + x.options_['reps'] = reps; + x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate"); + x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }}); + x.options_['weight'] = reps; + } + } op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")" { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) } diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 8c8932736..8d2276fb2 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -27,6 +27,12 @@ const applyOptions = (parent, enter) => (pat, i) => { pat = strudel.reify(pat)[type](enter(amount)); break; } + case 'replicate': { + const { amount } = op.arguments_; + pat = strudel.reify(pat); + pat = pat._repeatCycles(amount)._fast(amount); + break; + } case 'bjorklund': { if (op.arguments_.rotation) { pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation)); @@ -67,26 +73,13 @@ const applyOptions = (parent, enter) => (pat, i) => { return pat; }; -function resolveReplications(ast) { - ast.source_ = strudel.flatten( - ast.source_.map((child) => { - const { reps } = child.options_ || {}; - if (!reps) { - return [child]; - } - delete child.options_.reps; - return Array(reps).fill(child); - }), - ); -} - // expects ast from mini2ast + quoted mini string + optional callback when a node is entered export function patternifyAST(ast, code, onEnter, offset = 0) { onEnter?.(ast); const enter = (node) => patternifyAST(node, code, onEnter, offset); switch (ast.type_) { case 'pattern': { - resolveReplications(ast); + // resolveReplications(ast); const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter)); const alignment = ast.arguments_.alignment; const with_tactus = children.filter((child) => child.__tactus_source); diff --git a/packages/mini/package.json b/packages/mini/package.json index cb4f459c6..7bd730111 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -9,6 +9,7 @@ }, "scripts": { "test": "vitest run", + "bench": "vitest bench", "build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs", "build": "vite build", "prepublishOnly": "npm run build" diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index deff485a3..ed5c1e7e0 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -74,6 +74,6 @@ export const dough = async (code) => { worklet.node.connect(ac.destination); }; -export function doughTrigger(t, hap, currentTime, duration, cps) { - window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps }); +export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) { + window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 3fa89ca63..99eca92b5 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -130,6 +130,7 @@ class AMProcessor extends AudioWorkletProcessor { } registerProcessor('am-processor', AMProcessor); +const blockSize = 128; class CoarseProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [{ name: 'coarse', defaultValue: 1 }]; diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 6dba7dabe..3147ae38c 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -26,13 +26,13 @@ export async function initCode() { // load code from url hash (either short hash from database or decode long hash) try { const initialUrl = window.location.href; - const hash = initialUrl.split('?')[1]?.split('#')?.[0]; + const hash = initialUrl.split('?')[1]?.split('#')?.[0]?.split('&')[0]; const codeParam = window.location.href.split('#')[1] || ''; - // looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length) if (codeParam) { // looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length) return hash2code(codeParam); } else if (hash) { + // looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length) return supabase .from('code_v1') .select('code')