Merge branch 'tidalcycles:main' into gainmod

This commit is contained in:
Jade (Rose) Rowland 2024-05-09 23:55:13 -04:00 committed by GitHub
commit 1509ff1793
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 180 additions and 45 deletions

22
bench/tunes.bench.mjs Normal file
View file

@ -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);
});
});
});

View file

@ -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",

View file

@ -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);

View file

@ -9,6 +9,7 @@
},
"scripts": {
"test": "vitest run",
"bench": "vitest bench",
"build": "vite build",
"prepublishOnly": "pnpm build"
},

View file

@ -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);
});
/**

View file

@ -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);
});
});

View file

@ -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

View file

@ -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] <a b [c d?]>').fast(64).firstCycle();
},
{ time: 1000 },
);
calculateTactus(false);
bench(
'-tactus',
() => {
mini('a b c*3 [c d e, f g] <a b [c d?]>').fast(64).firstCycle();
},
{ time: 1000 },
);
calculateTactus(true);
});

View file

@ -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' }}) };

View file

@ -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 }}) }

View file

@ -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);

View file

@ -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"

View file

@ -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 });
}

View file

@ -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 }];

View file

@ -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')