Polish, rename, and document stepwise functions (#1262)

* polish, rename, and document stepwise functions: `pace`, `take`, `drop`, `expand`, `contract`, `repeat`, `zip`, `grow`, `shrink`, and `tour`
This commit is contained in:
Alex McLean 2025-02-02 20:26:44 +00:00 committed by GitHub
parent 0338c5b222
commit ce9d23049a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1543 additions and 804 deletions

View File

@ -1,22 +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';
import { calculateSteps } from '../packages/core/index.mjs';
const tuneKeys = Object.keys(tunes);
describe('renders tunes', () => {
tuneKeys.forEach((key) => {
describe(key, () => {
calculateTactus(true);
bench(`+tactus`, async () => {
calculateSteps(true);
bench(`+steps`, async () => {
await queryCode(tunes[key], testCycles[key] || 1);
});
calculateTactus(false);
bench(`-tactus`, async () => {
calculateSteps(false);
bench(`-steps`, async () => {
await queryCode(tunes[key], testCycles[key] || 1);
});
calculateTactus(true);
calculateSteps(true);
});
});
});

View File

@ -4,7 +4,7 @@ import { calculateTactus, sequence, stack } from '../index.mjs';
const pat64 = sequence(...Array(64).keys());
describe('tactus', () => {
describe('steps', () => {
calculateTactus(true);
bench(
'+tactus',

View File

@ -1,6 +1,6 @@
/*
pattern.mjs - Core pattern representation for strudel
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pattern.mjs>
Copyright (C) 2025 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pattern.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@ -27,10 +27,10 @@ import { logger } from './logger.mjs';
let stringParser;
let __tactus = true;
let __steps = true;
export const calculateTactus = function (x) {
__tactus = x ? true : false;
export const calculateSteps = function (x) {
__steps = x ? true : false;
};
// parser is expected to turn a string into a pattern
@ -46,34 +46,34 @@ export class Pattern {
* @param {function} query - The function that maps a `State` to an array of `Hap`.
* @noAutocomplete
*/
constructor(query, tactus = undefined) {
constructor(query, steps = undefined) {
this.query = query;
this._Pattern = true; // this property is used to detectinstance of another Pattern
this.tactus = tactus; // in terms of number of steps per cycle
this._steps = steps; // in terms of number of steps per cycle
}
get tactus() {
return this.__tactus;
get _steps() {
return this.__steps;
}
set tactus(tactus) {
this.__tactus = tactus === undefined ? undefined : Fraction(tactus);
set _steps(steps) {
this.__steps = steps === undefined ? undefined : Fraction(steps);
}
setTactus(tactus) {
this.tactus = tactus;
setSteps(steps) {
this._steps = steps;
return this;
}
withTactus(f) {
if (!__tactus) {
withSteps(f) {
if (!__steps) {
return this;
}
return new Pattern(this.query, this.tactus === undefined ? undefined : f(this.tactus));
return new Pattern(this.query, this._steps === undefined ? undefined : f(this._steps));
}
get hasTactus() {
return this.tactus !== undefined;
get hasSteps() {
return this._steps !== undefined;
}
//////////////////////////////////////////////////////////////////////
@ -90,7 +90,7 @@ export class Pattern {
*/
withValue(func) {
const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
result.tactus = this.tactus;
result._steps = this._steps;
return result;
}
@ -166,8 +166,8 @@ export class Pattern {
return span_a.intersection_e(span_b);
};
const result = pat_func.appWhole(whole_func, pat_val);
if (__tactus) {
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
if (__steps) {
result._steps = lcm(pat_val._steps, pat_func._steps);
}
return result;
}
@ -203,7 +203,7 @@ export class Pattern {
return haps;
};
const result = new Pattern(query);
result.tactus = this.tactus;
result._steps = this._steps;
return result;
}
@ -236,7 +236,7 @@ export class Pattern {
return haps;
};
const result = new Pattern(query);
result.tactus = pat_val.tactus;
result._steps = pat_val._steps;
return result;
}
@ -280,7 +280,7 @@ export class Pattern {
}
outerBind(func) {
return this.bindWhole((a) => a, func).setTactus(this.tactus);
return this.bindWhole((a) => a, func).setSteps(this._steps);
}
outerJoin() {
@ -388,7 +388,7 @@ export class Pattern {
polyJoin = function () {
const pp = this;
return pp.fmap((p) => p.s_extend(pp.tactus.div(p.tactus))).outerJoin();
return pp.fmap((p) => p.repeat(pp._steps.div(p._steps))).outerJoin();
};
polyBind(func) {
@ -499,7 +499,7 @@ export class Pattern {
*/
withHaps(func) {
const result = new Pattern((state) => func(this.query(state), state));
result.tactus = this.tactus;
result._steps = this._steps;
return result;
}
@ -589,7 +589,7 @@ export class Pattern {
* @noAutocomplete
*/
filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setTactus(this.tactus);
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
}
/**
@ -1159,7 +1159,7 @@ function _composeOp(a, b, func) {
export const polyrhythm = stack;
export const pr = stack;
export const pm = s_polymeter;
export const pm = polymeter;
// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
@ -1182,13 +1182,13 @@ export const pm = s_polymeter;
// Elemental patterns
/**
* Does absolutely nothing, but with a given metrical 'tactus'
* Does absolutely nothing, but with a given metrical 'steps'
* @name gap
* @param {number} tactus
* @param {number} steps
* @example
* gap(3) // "~@3"
*/
export const gap = (tactus) => new Pattern(() => [], tactus);
export const gap = (steps) => new Pattern(() => [], steps);
/**
* Does absolutely nothing..
@ -1198,7 +1198,7 @@ export const gap = (tactus) => new Pattern(() => [], tactus);
*/
export const silence = gap(1);
/* Like silence, but with a 'tactus' (relative duration) of 0 */
/* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0);
/** A discrete value that repeats once per cycle.
@ -1263,8 +1263,8 @@ 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);
if (__tactus) {
result.tactus = lcm(...pats.map((pat) => pat.tactus));
if (__steps) {
result._steps = lcm(...pats.map((pat) => pat._steps));
}
return result;
}
@ -1277,54 +1277,54 @@ function _stackWith(func, pats) {
if (pats.length === 1) {
return pats[0];
}
const [left, ...right] = pats.map((pat) => pat.tactus);
const tactus = __tactus ? left.maximum(...right) : undefined;
return stack(...func(tactus, pats));
const [left, ...right] = pats.map((pat) => pat._steps);
const steps = __steps ? left.maximum(...right) : undefined;
return stack(...func(steps, pats));
}
export function stackLeft(...pats) {
return _stackWith(
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(pat, gap(tactus.sub(pat.tactus))))),
(steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(pat, gap(steps.sub(pat._steps))))),
pats,
);
}
export function stackRight(...pats) {
return _stackWith(
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(gap(tactus.sub(pat.tactus)), pat))),
(steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(gap(steps.sub(pat._steps)), pat))),
pats,
);
}
export function stackCentre(...pats) {
return _stackWith(
(tactus, pats) =>
(steps, pats) =>
pats.map((pat) => {
if (pat.tactus.eq(tactus)) {
if (pat._steps.eq(steps)) {
return pat;
}
const g = gap(tactus.sub(pat.tactus).div(2));
return s_cat(g, pat, g);
const g = gap(steps.sub(pat._steps).div(2));
return stepcat(g, pat, g);
}),
pats,
);
}
export function stackBy(by, ...pats) {
const [left, ...right] = pats.map((pat) => pat.tactus);
const tactus = left.maximum(...right);
const [left, ...right] = pats.map((pat) => pat._steps);
const steps = left.maximum(...right);
const lookup = {
centre: stackCentre,
left: stackLeft,
right: stackRight,
expand: stack,
repeat: (...args) => s_polymeterSteps(tactus, ...args),
repeat: (...args) => polymeterSteps(steps, ...args),
};
return by
.inhabit(lookup)
.fmap((func) => func(...pats))
.innerJoin()
.setTactus(tactus);
.setSteps(steps);
}
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
@ -1358,8 +1358,8 @@ 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 = __tactus ? lcm(...pats.map((x) => x.tactus)) : undefined;
return new Pattern(query).splitQueries().setTactus(tactus);
const steps = __steps ? lcm(...pats.map((x) => x._steps)) : undefined;
return new Pattern(query).splitQueries().setSteps(steps);
}
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles.
@ -1409,7 +1409,7 @@ export function cat(...pats) {
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
return s_cat(...sections).slow(total);
return stepcat(...sections).slow(total);
}
/**
@ -1446,10 +1446,10 @@ export function fastcat(...pats) {
let result = slowcat(...pats);
if (pats.length > 1) {
result = result._fast(pats.length);
result.tactus = pats.length;
result._steps = pats.length;
}
if (pats.length == 1 && pats[0].__tactus_source) {
pats.tactus = pats[0].tactus;
if (pats.length == 1 && pats[0].__steps_source) {
pats._steps = pats[0]._steps;
}
return result;
}
@ -1536,11 +1536,11 @@ export const func = curry((a, b) => reify(b).func(a));
* @noAutocomplete
*
*/
export function register(name, func, patternify = true, preserveTactus = false, join = (x) => x.innerJoin()) {
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
result[name_item] = register(name_item, func, patternify);
result[name_item] = register(name_item, func, patternify, preserveSteps, join);
}
return result;
}
@ -1576,8 +1576,8 @@ export function register(name, func, patternify = true, preserveTactus = false,
result = join(right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)));
}
}
if (preserveTactus) {
result.tactus = pat.tactus;
if (preserveSteps) {
result._steps = pat._steps;
}
return result;
};
@ -1585,8 +1585,8 @@ export function register(name, func, patternify = true, preserveTactus = false,
pfunc = function (...args) {
args = args.map(reify);
const result = func(...args);
if (preserveTactus) {
result.tactus = args[args.length - 1].tactus;
if (preserveSteps) {
result._steps = args[args.length - 1]._steps;
}
return result;
};
@ -1609,8 +1609,8 @@ export function register(name, func, patternify = true, preserveTactus = false,
// version, prefixed by '_'
Pattern.prototype['_' + name] = function (...args) {
const result = func(...args, this);
if (preserveTactus) {
result.setTactus(this.tactus);
if (preserveSteps) {
result.setSteps(this._steps);
}
return result;
};
@ -1622,8 +1622,8 @@ export function register(name, func, patternify = true, preserveTactus = false,
}
// Like register, but defaults to stepJoin
function stepRegister(name, func, patternify = true, preserveTactus = false, join = (x) => x.stepJoin()) {
return register(name, func, patternify, preserveTactus, join);
function stepRegister(name, func, patternify = true, preserveSteps = false, join = (x) => x.stepJoin()) {
return register(name, func, patternify, preserveSteps, join);
}
//////////////////////////////////////////////////////////////////////
@ -1833,8 +1833,8 @@ 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();
if (__tactus) {
result.tactus = Fraction(factor).mulmaybe(pat.tactus);
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
@ -1858,7 +1858,7 @@ export const { fast, density } = register(
}
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor)).setTactus(pat.tactus);
return fastQuery.withHapTime((t) => t.div(factor)).setSteps(pat._steps);
},
true,
true,
@ -2030,12 +2030,12 @@ export const zoom = register('zoom', function (s, e, pat) {
return nothing;
}
const d = e.sub(s);
const tactus = __tactus ? pat.tactus.mulmaybe(d) : undefined;
const steps = __steps ? pat._steps.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)))
.splitQueries()
.setTactus(tactus);
.setSteps(steps);
});
export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a, pat) {
@ -2097,7 +2097,7 @@ export const linger = register(
* note(saw.range(40,52).segment(24))
*/
export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate)).setTactus(rate);
return pat.struct(pure(true)._fast(rate)).setSteps(rate);
});
/**
@ -2273,7 +2273,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(__tactus ? lcm(left.tactus, right.tactus) : undefined);
return stack(left, right).setSteps(__steps ? lcm(left._steps, right._steps) : undefined);
});
/**
@ -2533,16 +2533,15 @@ export const within = register('within', (a, b, fn, pat) =>
);
//////////////////////////////////////////////////////////////////////
// Tactus-related functions, i.e. ones that do stepwise
// transformations
// Stepwise functions
Pattern.prototype.stepJoin = function () {
const pp = this;
const first_t = s_cat(..._retime(_slices(pp.queryArc(0, 1)))).tactus;
const first_t = stepcat(..._retime(_slices(pp.queryArc(0, 1))))._steps;
const q = function (state) {
const shifted = pp.early(state.span.begin.sam());
const haps = shifted.query(state.setSpan(new TimeSpan(Fraction(0), Fraction(1))));
const pat = s_cat(..._retime(_slices(haps)));
const pat = stepcat(..._retime(_slices(haps)));
return pat.query(state);
};
return new Pattern(q, first_t);
@ -2553,17 +2552,17 @@ Pattern.prototype.stepBind = function (func) {
};
export function _retime(timedHaps) {
const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0));
const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce(
const occupied_perc = timedHaps.filter((t, pat) => pat.hasSteps).reduce((a, b) => a.add(b), Fraction(0));
const occupied_steps = removeUndefineds(timedHaps.map((t, pat) => pat._steps)).reduce(
(a, b) => a.add(b),
Fraction(0),
);
const total_tactus = occupied_perc.eq(0) ? undefined : occupied_tactus.div(occupied_perc);
const total_steps = occupied_perc.eq(0) ? undefined : occupied_steps.div(occupied_perc);
function adjust(dur, pat) {
if (pat.tactus === undefined) {
return [dur.mulmaybe(total_tactus), pat];
if (pat._steps === undefined) {
return [dur.mulmaybe(total_steps), pat];
}
return [pat.tactus, pat];
return [pat._steps, pat];
}
return timedHaps.map((x) => adjust(...x));
}
@ -2593,20 +2592,22 @@ export function _match(span, hap_p) {
}
/**
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given number of steps per cycle (aka tactus).
* *Experimental*
*
* Speeds a pattern up or down, to fit to the given number of steps per cycle.
* @example
* s("bd sd cp").steps(4)
* // The same as s("{bd sd cp}%4")
* sound("bd sd cp").pace(4)
* // The same as sound("{bd sd cp}%4") or sound("<bd sd cp>*4")
*/
export const steps = register('steps', function (targetTactus, pat) {
if (pat.tactus === undefined) {
export const pace = register('pace', function (targetSteps, pat) {
if (pat._steps === undefined) {
return pat;
}
if (pat.tactus.eq(Fraction(0))) {
if (pat._steps.eq(Fraction(0))) {
// avoid divide by zero..
return nothing;
}
return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
return pat._fast(Fraction(targetSteps).div(pat._steps)).setSteps(targetSteps);
});
export function _polymeterListSteps(steps, ...args) {
@ -2632,17 +2633,18 @@ export function _polymeterListSteps(steps, ...args) {
}
/**
* Aligns one or more given patterns to the given number of steps per cycle.
* This relies on patterns having coherent number of steps per cycle,
* *Experimental*
*
* @name s_polymeterSteps
* Aligns the steps of the patterns, to match the given number of steps per cycle, creating polymeters.
*
* @name polymeterSteps
* @param {number} steps how many items are placed in one cycle
* @param {any[]} patterns one or more patterns
* @example
* // the same as "{c d, e f g}%4"
* s_polymeterSteps(4, "c d", "e f g").note()
* polymeterSteps(4, "c d", "e f g").note()
*/
export function s_polymeterSteps(steps, ...args) {
export function polymeterSteps(steps, ...args) {
if (args.length == 0) {
return silence;
}
@ -2651,59 +2653,61 @@ export function s_polymeterSteps(steps, ...args) {
return _polymeterListSteps(steps, ...args);
}
return s_polymeter(...args).steps(steps);
return polymeter(...args).pace(steps);
}
/**
* *EXPERIMENTAL* - Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used.
* *Experimental*
*
* Aligns the steps of the patterns, to match the steps per cycle of the first pattern, creating polymeters. See `polymeterSteps` to set the target steps explicitly.
* @synonyms pm
* @example
* // The same as note("{c eb g, c2 g2}")
* s_polymeter("c eb g", "c2 g2").note()
* polymeter("c eb g", "c2 g2").note()
*
*/
export function s_polymeter(...args) {
export function polymeter(...args) {
if (Array.isArray(args[0])) {
// Support old behaviour
return _polymeterListSteps(0, ...args);
}
// TODO currently ignoring arguments without tactus...
args = args.filter((arg) => arg.hasTactus);
// TODO currently ignoring arguments without steps...
args = args.filter((arg) => arg.hasSteps);
if (args.length == 0) {
return silence;
}
const tactus = args[0].tactus;
if (tactus.eq(Fraction(0))) {
const steps = args[0]._steps;
if (steps.eq(Fraction(0))) {
return nothing;
}
const [head, ...tail] = args;
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
result.tactus = tactus;
const result = stack(head, ...tail.map((pat) => pat._slow(pat._steps.div(steps))));
result._steps = steps;
return result;
}
/** Sequences patterns like `seq`, but each pattern has a length, relative to the whole.
* This length can either be provided as a [length, pattern] pair, or inferred from
* the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`.
* @name s_cat
/** 'Concatenates' patterns like `fastcat`, but proportional to a number of steps per cycle.
* The steps can either be inferred from the pattern, or provided as a [length, pattern] pair.
* Has the alias `timecat`.
* @name stepcat
* @synonyms timeCat, timecat
* @return {Pattern}
* @example
* s_cat([3,"e3"],[1, "g3"]).note()
* stepcat([3,"e3"],[1, "g3"]).note()
* // the same as "e3@3 g3".note()
* @example
* s_cat("bd sd cp","hh hh").sound()
* stepcat("bd sd cp","hh hh").sound()
* // the same as "bd sd cp hh hh".sound()
*/
export function s_cat(...timepats) {
export function stepcat(...timepats) {
if (timepats.length === 0) {
return nothing;
}
const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]);
timepats = timepats.map(findtactus);
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]);
timepats = timepats.map(findsteps);
if (timepats.find((x) => x[0] === undefined)) {
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
if (times.length === 0) {
@ -2721,7 +2725,7 @@ export function s_cat(...timepats) {
}
if (timepats.length == 1) {
const result = reify(timepats[0][1]);
return result.withTactus((_) => timepats[0][0]);
return result.withSteps((_) => timepats[0][0]);
}
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
@ -2736,23 +2740,22 @@ export function s_cat(...timepats) {
begin = end;
}
const result = stack(...pats);
result.tactus = total;
result._steps = total;
return result;
}
/** Aliases for `s_cat` */
export const timecat = s_cat;
export const timeCat = s_cat;
/**
* *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'.
* Similar to `s_cat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
* *Experimental*
*
* Concatenates patterns stepwise, according to an inferred 'steps per cycle'.
* Similar to `stepcat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
*
* @return {Pattern}
* @example
* s_alt(["bd cp", "mt"], "bd").sound()
* stepalt(["bd cp", "mt"], "bd").sound()
* // The same as "bd cp bd mt bd".sound()
*/
export function s_alt(...groups) {
export function stepalt(...groups) {
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
@ -2761,21 +2764,34 @@ export function s_alt(...groups) {
for (let cycle = 0; cycle < cycles; ++cycle) {
result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length])));
}
result = result.filter((x) => x.hasTactus && x.tactus > 0);
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
result = s_cat(...result);
result.tactus = tactus;
result = result.filter((x) => x.hasSteps && x._steps > 0);
const steps = result.reduce((a, b) => a.add(b._steps), Fraction(0));
result = stepcat(...result);
result._steps = steps;
return result;
}
/**
* *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'.
* *Experimental*
*
* Takes the given number of steps from a pattern (dropping the rest).
* A positive number will take steps from the start of a pattern, and a negative number from the end.
* @return {Pattern}
* @example
* "bd cp ht mt".take("2").sound()
* // The same as "bd cp".sound()
* @example
* "bd cp ht mt".take("1 2 3").sound()
* // The same as "bd bd cp bd cp ht".sound()
* @example
* "bd cp ht mt".take("-1 -2 -3").sound()
* // The same as "mt ht mt cp ht mt".sound()
*/
export const s_add = stepRegister('s_add', function (i, pat) {
if (!pat.hasTactus) {
export const take = stepRegister('take', function (i, pat) {
if (!pat.hasSteps) {
return nothing;
}
if (pat.tactus.lte(0)) {
if (pat._steps.lte(0)) {
return nothing;
}
i = Fraction(i);
@ -2786,7 +2802,7 @@ export const s_add = stepRegister('s_add', function (i, pat) {
if (flip) {
i = i.abs();
}
const frac = i.div(pat.tactus);
const frac = i.div(pat._steps);
if (frac.lte(0)) {
return nothing;
}
@ -2800,77 +2816,117 @@ export const s_add = stepRegister('s_add', function (i, pat) {
});
/**
* *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'.
* *Experimental*
*
* Drops the given number of steps from a pattern.
* A positive number will drop steps from the start of a pattern, and a negative number from the end.
* @return {Pattern}
* @example
* "bd cp ht mt".drop("1").sound()
* // The same as "cp ht mt".sound()
* @example
* "bd cp ht mt".drop("-1").sound()
* // The same as "bd cp ht".sound()
* @example
* "bd cp ht mt".drop("1 2 3").sound()
* // The same as "cp ht mt ht mt mt".sound()
* @example
* "bd cp ht mt".drop("-1 -2 -3").sound()
* // The same as "bd cp ht bd cp bd".sound()
*/
export const s_sub = stepRegister('s_sub', function (i, pat) {
if (!pat.hasTactus) {
export const drop = stepRegister('drop', function (i, pat) {
if (!pat.hasSteps) {
return nothing;
}
i = Fraction(i);
if (i.lt(0)) {
return pat.s_add(Fraction(0).sub(pat.tactus.add(i)));
return pat.take(pat._steps.add(i));
}
return pat.s_add(pat.tactus.sub(i));
return pat.take(Fraction(0).sub(pat._steps.sub(i)));
});
export const s_extend = stepRegister('s_extend', function (factor, pat) {
return pat.fast(factor).s_expand(factor);
export const repeat = stepRegister('repeat', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
export const s_expand = stepRegister('s_expand', function (factor, pat) {
return pat.withTactus((t) => t.mul(Fraction(factor)));
export const expand = stepRegister('expand', function (factor, pat) {
return pat.withSteps((t) => t.mul(Fraction(factor)));
});
export const s_contract = stepRegister('s_contract', function (factor, pat) {
return pat.withTactus((t) => t.div(Fraction(factor)));
export const contract = stepRegister('contract', function (factor, pat) {
return pat.withSteps((t) => t.div(Fraction(factor)));
});
/**
* *EXPERIMENTAL*
*/
Pattern.prototype.s_taperlist = function (amount, times) {
Pattern.prototype.shrinklist = function (amount) {
const pat = this;
if (!pat.hasTactus) {
if (!pat.hasSteps) {
return [pat];
}
times = times - 1;
let [amountv, times] = Array.isArray(amount) ? amount : [amount, pat._steps];
amountv = Fraction(amountv);
if (times === 0) {
if (times === 0 || amountv === 0) {
return [pat];
}
const list = [];
const reverse = amount > 0;
amount = Fraction(Math.abs(amount));
const start = pat.tactus.sub(amount.mul(Fraction(times))).max(Fraction(0));
for (let i = 0; i < times; ++i) {
list.push(pat.zoom(0, start.add(amount.mul(Fraction(i))).div(pat.tactus)));
const fromstart = amountv > 0;
const ranges = [];
if (fromstart) {
const seg = Fraction(1).div(pat._steps).mul(amountv);
for (let i = 0; i < times; ++i) {
const s = seg.mul(i);
if (s.gt(1)) {
break;
}
ranges.push([s, 1]);
}
} else {
amountv = Fraction(0).sub(amountv);
const seg = Fraction(1).div(pat._steps).mul(amountv);
for (let i = 0; i < times; ++i) {
const e = Fraction(1).sub(seg.mul(i));
if (e.lt(0)) {
break;
}
ranges.push([Fraction(0), e]);
}
}
list.push(pat);
if (reverse) {
list.reverse();
}
return list;
return ranges.map((x) => pat.zoom(...x));
};
export const s_taperlist = (amount, times, pat) => pat.s_taperlist(amount, times);
export const shrinklist = (amount, pat) => pat.shrinklist(amount);
/**
* *EXPERIMENTAL*
* *Experimental*
*
* Progressively shrinks the pattern by 'n' steps until there's nothing left, or if a second value is given (using mininotation list syntax with `:`),
* that number of times.
* A positive number will progressively drop steps from the start of a pattern, and a negative number from the end.
* @return {Pattern}
* @example
* "bd cp ht mt".shrink("1").sound()
* // The same as "bd cp ht mt".drop("0 1 2 3").sound()
* @example
* "bd cp ht mt".shrink("-1").sound()
* // The same as "bd cp ht mt".drop("0 -1 -2 -3").sound()
* @example
* "bd cp ht mt".grow("1 -1").sound()
*/
export const s_taper = register(
's_taper',
function (amount, times, pat) {
if (!pat.hasTactus) {
export const shrink = register(
'shrink',
function (amount, pat) {
if (!pat.hasSteps) {
return nothing;
}
const list = pat.s_taperlist(amount, times);
const result = s_cat(...list);
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
const list = pat.shrinklist(amount);
const result = stepcat(...list);
// TODO is this calculation needed?
result._steps = list.reduce((a, b) => a.add(b._steps), Fraction(0));
return result;
},
true,
@ -2879,10 +2935,58 @@ export const s_taper = register(
);
/**
* *EXPERIMENTAL*
* *Experimental*
*
* Progressively grows the pattern by 'n' steps until the full pattern is played, or if a second value is given (using mininotation list syntax with `:`),
* that number of times.
* A positive number will progressively grow steps from the start of a pattern, and a negative number from the end.
* @return {Pattern}
* @example
* "bd cp ht mt".grow("1").sound()
* // The same as "bd cp ht mt".take("1 2 3 4")
* @example
* "bd cp ht mt".grow("-1").sound()
* // The same as "bd cp ht mt".take("-1 -2 -3 -4")
*/
Pattern.prototype.s_tour = function (...many) {
return s_cat(
export const grow = register(
'grow',
function (amount, pat) {
if (!pat.hasSteps) {
return nothing;
}
const list = pat.shrinklist(Fraction(0).sub(amount));
list.reverse();
const result = stepcat(...list);
// TODO is this calculation needed?
result._steps = list.reduce((a, b) => a.add(b._steps), Fraction(0));
return result;
},
true,
false,
(x) => x.stepJoin(),
);
/**
* *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
* number of steps per cycle is therefore usually recommended.
*
* @return {Pattern}
* @example
* "[c g]".tour("e f", "e f g", "g f e c").note()
.sound("folkharp")
.pace(8)
*/
export const tour = function (pat, ...many) {
return pat.tour(...many);
};
Pattern.prototype.tour = function (...many) {
return stepcat(
...[].concat(
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
this,
@ -2891,16 +2995,56 @@ Pattern.prototype.s_tour = function (...many) {
);
};
export const s_tour = function (pat, ...many) {
return pat.s_tour(...many);
/**
* *Experimental*
*
* '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.
*
* @returns {Pattern}
* @example
* zip("e f", "e f g", "g [f e] a f4 c").note()
.sound("folkharp")
.pace(8)
*/
export const zip = function (...pats) {
pats = pats.filter((pat) => pat.hasSteps);
const zipped = slowcat(...pats.map((pat) => pat._slow(pat._steps)));
const steps = lcm(...pats.map((x) => x._steps));
return zipped._fast(steps).setSteps(steps);
};
const s_zip = function (...pats) {
pats = pats.filter((pat) => pat.hasTactus);
const zipped = slowcat(...pats.map((pat) => pat._slow(pat.tactus)));
// Should maybe use lcm or gcd for tactus?
return zipped._fast(pats[0].tactus).setTactus(pats[0].tactus);
};
/** Aliases for `stepcat` */
export const timecat = stepcat;
export const timeCat = stepcat;
// Deprecated stepwise aliases
export const s_cat = stepcat;
export const s_alt = stepalt;
export const s_polymeterSteps = polymeterSteps;
Pattern.prototype.s_polymeterSteps = Pattern.prototype.polymeterSteps;
export const s_polymeter = polymeter;
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
export const s_taper = shrink;
Pattern.prototype.s_taper = Pattern.prototype.shrink;
export const s_taperlist = shrinklist;
Pattern.prototype.s_taperlist = Pattern.prototype.shrinklist;
export const s_add = take;
Pattern.prototype.s_add = Pattern.prototype.take;
export const s_sub = drop;
Pattern.prototype.s_sub = Pattern.prototype.drop;
export const s_expand = expand;
Pattern.prototype.s_expand = Pattern.prototype.expand;
export const s_extend = repeat;
Pattern.prototype.s_extend = Pattern.prototype.repeat;
export const s_contract = contract;
Pattern.prototype.s_contract = Pattern.prototype.contract;
export const s_tour = tour;
Pattern.prototype.s_tour = Pattern.prototype.tour;
export const s_zip = zip;
Pattern.prototype.s_zip = Pattern.prototype.zip;
export const steps = pace;
Pattern.prototype.steps = Pattern.prototype.pace;
//////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of
@ -2934,7 +3078,7 @@ export const chop = register('chop', function (n, pat) {
const func = function (o) {
return sequence(slice_objects.map((slice_o) => merge(o, slice_o)));
};
return pat.squeezeBind(func).setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
return pat.squeezeBind(func).setSteps(__steps ? Fraction(n).mulmaybe(pat._steps) : undefined);
});
/**
@ -2952,7 +3096,7 @@ export const striate = register('striate', function (n, pat) {
return pat
.set(slicePat)
._fast(n)
.setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
.setSteps(__steps ? Fraction(n).mulmaybe(pat._steps) : undefined);
});
/**
@ -3001,7 +3145,7 @@ export const slice = register(
}),
),
)
.setTactus(ipat.tactus);
.setSteps(ipat._steps);
},
false, // turns off auto-patternification
);
@ -3032,14 +3176,14 @@ export const splice = register(
...v,
})),
);
}).setTactus(ipat.tactus);
}).setSteps(ipat._steps);
},
false, // turns off auto-patternification
);
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
const tactus = pat.tactus ? pat.tactus.div(factor) : undefined;
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), tactus);
const steps = pat._steps ? pat._steps.div(factor) : undefined;
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps);
});
/**

View File

@ -30,14 +30,14 @@ describe('controls', () => {
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
});
it('preserves tactus of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
it('preserves step count of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4')))._steps).toEqual(Fraction(3));
});
it('preserves tactus of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
it('preserves step count of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4'))))._steps).toEqual(Fraction(4));
});
it('combines tactus of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
it('combines step count of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4'))))._steps).toEqual(Fraction(12));
});
it('finds control name by alias', () => {
expect(getControlName('lpf')).toEqual('cutoff');

View File

@ -21,8 +21,8 @@ import {
cat,
sequence,
palindrome,
s_polymeter,
s_polymeterSteps,
polymeter,
polymeterSteps,
polyrhythm,
silence,
fast,
@ -51,8 +51,8 @@ import {
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
stepcat,
sometimes,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@ -609,18 +609,18 @@ describe('Pattern', () => {
);
});
});
describe('s_polymeter()', () => {
describe('polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => {
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
);
});
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
});
@ -1140,130 +1140,135 @@ describe('Pattern', () => {
);
});
});
describe('tactus', () => {
describe('_steps', () => {
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(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));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
expect(sequence(0, 1, 2, 3).linger(4)._steps).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).iter(4)._steps).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4)._steps).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).hurry(4)._steps).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).rev()._steps).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10)._steps).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert()._steps).toStrictEqual(Fraction(3));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4)._steps).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4)._steps).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3))._steps).toStrictEqual(
Fraction(4),
);
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3))._steps).toStrictEqual(
Fraction(4),
);
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4)._steps).toStrictEqual(Fraction(12));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(2)).tactus,
.setSteps(3)
.appBoth(pure(1).setSteps(2))._steps,
).toStrictEqual(Fraction(6));
expect(
pure((x) => x + 1)
.setTactus(undefined)
.appBoth(pure(1).setTactus(2)).tactus,
.setSteps(undefined)
.appBoth(pure(1).setSteps(2))._steps,
).toStrictEqual(Fraction(2));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(undefined)).tactus,
.setSteps(3)
.appBoth(pure(1).setSteps(undefined))._steps,
).toStrictEqual(Fraction(3));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(6));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setSteps(undefined))._steps).toStrictEqual(Fraction(3));
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4))._steps).toStrictEqual(Fraction(4));
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)).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(2));
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
expect(stackCentre(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));
expect(fastcat(0, 1).setSteps(undefined).fast(3)._steps).toStrictEqual(undefined);
});
});
describe('s_cat', () => {
describe('stepcat', () => {
it('can cat', () => {
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
expect(sameFirst(stepcat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
expect(sameFirst(stepcat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
});
it('calculates undefined tactuses as the average', () => {
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
it('calculates undefined steps as the average', () => {
expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
});
});
describe('s_taper', () => {
it('can taper', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
describe('shrink', () => {
it('can shrink', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).shrink(1), sequence(0, 1, 2, 3, 4, 1, 2, 3, 4, 2, 3, 4, 3, 4, 4)));
});
it('can taper backwards', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
it('can shrink backwards', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).shrink(-1), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
});
});
describe('s_add and s_sub', () => {
it('can add from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
describe('grow', () => {
it('can grow', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).grow(1), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
});
it('can sub to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
it('can grow backwards', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).grow(-1), sequence(4, 3, 4, 2, 3, 4, 1, 2, 3, 4, 0, 1, 2, 3, 4)));
});
it('can add from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
});
describe('take and drop', () => {
it('can take from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).take(2), sequence(0, 1)));
});
it('can sub to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
it('can drop from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).drop(2), sequence(2, 3, 4)));
});
it('can subtract nothing', () => {
expect(sameFirst(pure('a').s_sub(0), pure('a')));
it('can take from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).take(-2), sequence(3, 4)));
});
it('can subtract nothing, repeatedly', () => {
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
it('can drop from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).drop(-2), sequence(0, 1, 2)));
});
it('can drop nothing', () => {
expect(sameFirst(pure('a').drop(0), pure('a')));
});
it('can drop nothing, repeatedly', () => {
expect(sameFirst(pure('a').drop(0, 0), fastcat('a', 'a')));
for (var i = 0; i < 100; ++i) {
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
expect(sameFirst(pure('a').drop(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
}
});
});
describe('s_expand', () => {
describe('expand', () => {
it('can expand four things in half', () => {
expect(
sameFirst(
sequence(0, 1, 2, 3).s_expand(1, 0.5),
s_cat(sequence(0, 1, 2, 3), sequence(0, 1, 2, 3).s_expand(0.5)),
),
sameFirst(sequence(0, 1, 2, 3).expand(1, 0.5), stepcat(sequence(0, 1, 2, 3), sequence(0, 1, 2, 3).expand(0.5))),
);
});
it('can expand five things in half', () => {
expect(
sameFirst(
sequence(0, 1, 2, 3, 4).s_expand(1, 0.5),
s_cat(sequence(0, 1, 2, 3, 4), sequence(0, 1, 2, 3, 4).s_expand(0.5)),
sequence(0, 1, 2, 3, 4).expand(1, 0.5),
stepcat(sequence(0, 1, 2, 3, 4), sequence(0, 1, 2, 3, 4).expand(0.5)),
),
);
});
});
describe('stepJoin', () => {
it('can join a pattern with a tactus of 2', () => {
it('can join a pattern with steps of 2', () => {
expect(
sameFirst(
sequence(pure(pure('a')), pure(pure('b').setTactus(2))).stepJoin(),
s_cat(pure('a'), pure('b').setTactus(2)),
sequence(pure(pure('a')), pure(pure('b').setSteps(2))).stepJoin(),
stepcat(pure('a'), pure('b').setSteps(2)),
),
);
});
it('can join a pattern with a tactus of 0.5', () => {
it('can join a pattern with steps of 0.5', () => {
expect(
sameFirst(
sequence(pure(pure('a')), pure(pure('b').setTactus(0.5))).stepJoin(),
s_cat(pure('a'), pure('b').setTactus(0.5)),
sequence(pure(pure('a')), pure(pure('b').setSteps(0.5))).stepJoin(),
stepcat(pure('a'), pure('b').setSteps(0.5)),
),
);
});
});
describe('loopAt', () => {
it('maintains tactus', () => {
expect(s('bev').chop(8).loopAt(2).tactus).toStrictEqual(Fraction(4));
it('maintains steps', () => {
expect(s('bev').chop(8).loopAt(2)._steps).toStrictEqual(Fraction(4));
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location();
}
var PatternStub = function(source, alignment, seed, tactus)
var PatternStub = function(source, alignment, seed, _steps)
{
this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus };
this.arguments_ = { alignment: alignment, _steps: _steps };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
@ -172,8 +172,8 @@ slice_with_ops = s:slice ops:slice_op*
}
// a sequence is a combination of one or more successive slices (as an array)
sequence = tactus:'^'? s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat', undefined, !!tactus); }
sequence = _steps:'^'? s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat', undefined, !!_steps); }
// a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+

View File

@ -14,7 +14,7 @@ const applyOptions = (parent, enter) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
const tactus_source = pat.__tactus_source;
const steps_source = pat.__steps_source;
if (ops) {
for (const op of ops) {
switch (op.type_) {
@ -69,7 +69,7 @@ const applyOptions = (parent, enter) => (pat, i) => {
}
}
}
pat.__tactus_source = pat.__tactus_source || tactus_source;
pat.__steps_source = pat.__steps_source || steps_source;
return pat;
};
@ -82,20 +82,20 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
// 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);
const with_steps = children.filter((child) => child.__steps_source);
let pat;
switch (alignment) {
case 'stack': {
pat = strudel.stack(...children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
if (with_steps.length) {
pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps)));
}
break;
}
case 'polymeter_slowcat': {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
if (with_steps.length) {
pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps)));
}
break;
}
@ -111,8 +111,8 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
}
case 'rand': {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
if (with_steps.length) {
pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps)));
}
break;
}
@ -131,21 +131,21 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
pat._steps = weightSum;
if (with_steps.length) {
pat._steps = pat._steps.mul(lcm(...with_steps.map((x) => Fraction(x._steps))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
pat._steps = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
if (ast.arguments_._steps) {
pat.__steps_source = true;
}
}
}
if (with_tactus.length) {
pat.__tactus_source = true;
if (with_steps.length) {
pat.__steps_source = true;
}
return pat;
}

View File

@ -208,16 +208,16 @@ describe('mini', () => {
it('_ and @ are almost interchangeable', () => {
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
});
it('supports ^ tactus marking', () => {
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
expect(mini('[^b c]!3').tactus).toEqual(Fraction(6));
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
expect(mini('[^a b c d e]').tactus).toEqual(Fraction(5));
it('supports ^ step marking', () => {
expect(mini('a [^b c]')._steps).toEqual(Fraction(4));
expect(mini('[^b c]!3')._steps).toEqual(Fraction(6));
expect(mini('[a b c] [d [e f]]')._steps).toEqual(Fraction(2));
expect(mini('^[a b c] [d [e f]]')._steps).toEqual(Fraction(2));
expect(mini('[a b c] [d [^e f]]')._steps).toEqual(Fraction(8));
expect(mini('[a b c] [^d [e f]]')._steps).toEqual(Fraction(4));
expect(mini('[^a b c] [^d [e f]]')._steps).toEqual(Fraction(12));
expect(mini('[^a b c] [d [^e f]]')._steps).toEqual(Fraction(24));
expect(mini('[^a b c d e]')._steps).toEqual(Fraction(5));
});
});

View File

@ -249,5 +249,5 @@ export const scale = register(
);
},
true,
true, // preserve tactus
true, // preserve step count
);

View File

@ -2521,6 +2521,98 @@ exports[`runs examples > example "drive" example index 0 1`] = `
]
`;
exports[`runs examples > example "drop" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:cp ]",
"[ 1/3 → 2/3 | s:ht ]",
"[ 2/3 → 1/1 | s:mt ]",
"[ 1/1 → 4/3 | s:cp ]",
"[ 4/3 → 5/3 | s:ht ]",
"[ 5/3 → 2/1 | s:mt ]",
"[ 2/1 → 7/3 | s:cp ]",
"[ 7/3 → 8/3 | s:ht ]",
"[ 8/3 → 3/1 | s:mt ]",
"[ 3/1 → 10/3 | s:cp ]",
"[ 10/3 → 11/3 | s:ht ]",
"[ 11/3 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "drop" example index 1 1`] = `
[
"[ 0/1 → 1/3 | s:bd ]",
"[ 1/3 → 2/3 | s:cp ]",
"[ 2/3 → 1/1 | s:ht ]",
"[ 1/1 → 4/3 | s:bd ]",
"[ 4/3 → 5/3 | s:cp ]",
"[ 5/3 → 2/1 | s:ht ]",
"[ 2/1 → 7/3 | s:bd ]",
"[ 7/3 → 8/3 | s:cp ]",
"[ 8/3 → 3/1 | s:ht ]",
"[ 3/1 → 10/3 | s:bd ]",
"[ 10/3 → 11/3 | s:cp ]",
"[ 11/3 → 4/1 | s:ht ]",
]
`;
exports[`runs examples > example "drop" example index 2 1`] = `
[
"[ 0/1 → 1/6 | s:cp ]",
"[ 1/6 → 1/3 | s:ht ]",
"[ 1/3 → 1/2 | s:mt ]",
"[ 1/2 → 2/3 | s:ht ]",
"[ 2/3 → 5/6 | s:mt ]",
"[ 5/6 → 1/1 | s:mt ]",
"[ 1/1 → 7/6 | s:cp ]",
"[ 7/6 → 4/3 | s:ht ]",
"[ 4/3 → 3/2 | s:mt ]",
"[ 3/2 → 5/3 | s:ht ]",
"[ 5/3 → 11/6 | s:mt ]",
"[ 11/6 → 2/1 | s:mt ]",
"[ 2/1 → 13/6 | s:cp ]",
"[ 13/6 → 7/3 | s:ht ]",
"[ 7/3 → 5/2 | s:mt ]",
"[ 5/2 → 8/3 | s:ht ]",
"[ 8/3 → 17/6 | s:mt ]",
"[ 17/6 → 3/1 | s:mt ]",
"[ 3/1 → 19/6 | s:cp ]",
"[ 19/6 → 10/3 | s:ht ]",
"[ 10/3 → 7/2 | s:mt ]",
"[ 7/2 → 11/3 | s:ht ]",
"[ 11/3 → 23/6 | s:mt ]",
"[ 23/6 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "drop" example index 3 1`] = `
[
"[ 0/1 → 1/6 | s:bd ]",
"[ 1/6 → 1/3 | s:cp ]",
"[ 1/3 → 1/2 | s:ht ]",
"[ 1/2 → 2/3 | s:bd ]",
"[ 2/3 → 5/6 | s:cp ]",
"[ 5/6 → 1/1 | s:bd ]",
"[ 1/1 → 7/6 | s:bd ]",
"[ 7/6 → 4/3 | s:cp ]",
"[ 4/3 → 3/2 | s:ht ]",
"[ 3/2 → 5/3 | s:bd ]",
"[ 5/3 → 11/6 | s:cp ]",
"[ 11/6 → 2/1 | s:bd ]",
"[ 2/1 → 13/6 | s:bd ]",
"[ 13/6 → 7/3 | s:cp ]",
"[ 7/3 → 5/2 | s:ht ]",
"[ 5/2 → 8/3 | s:bd ]",
"[ 8/3 → 17/6 | s:cp ]",
"[ 17/6 → 3/1 | s:bd ]",
"[ 3/1 → 19/6 | s:bd ]",
"[ 19/6 → 10/3 | s:cp ]",
"[ 10/3 → 7/2 | s:ht ]",
"[ 7/2 → 11/3 | s:bd ]",
"[ 11/3 → 23/6 | s:cp ]",
"[ 23/6 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "dry" example index 0 1`] = `
[
"[ 0/1 → 1/8 | n:0 s:superpiano room:0.7 dry:0 ]",
@ -3442,6 +3534,96 @@ exports[`runs examples > example "gain" example index 0 1`] = `
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
exports[`runs examples > example "grow" example index 0 1`] = `
[
"[ 0/1 → 1/10 | s:bd ]",
"[ 1/10 → 1/5 | s:bd ]",
"[ 1/5 → 3/10 | s:cp ]",
"[ 3/10 → 2/5 | s:bd ]",
"[ 2/5 → 1/2 | s:cp ]",
"[ 1/2 → 3/5 | s:ht ]",
"[ 3/5 → 7/10 | s:bd ]",
"[ 7/10 → 4/5 | s:cp ]",
"[ 4/5 → 9/10 | s:ht ]",
"[ 9/10 → 1/1 | s:mt ]",
"[ 1/1 → 11/10 | s:bd ]",
"[ 11/10 → 6/5 | s:bd ]",
"[ 6/5 → 13/10 | s:cp ]",
"[ 13/10 → 7/5 | s:bd ]",
"[ 7/5 → 3/2 | s:cp ]",
"[ 3/2 → 8/5 | s:ht ]",
"[ 8/5 → 17/10 | s:bd ]",
"[ 17/10 → 9/5 | s:cp ]",
"[ 9/5 → 19/10 | s:ht ]",
"[ 19/10 → 2/1 | s:mt ]",
"[ 2/1 → 21/10 | s:bd ]",
"[ 21/10 → 11/5 | s:bd ]",
"[ 11/5 → 23/10 | s:cp ]",
"[ 23/10 → 12/5 | s:bd ]",
"[ 12/5 → 5/2 | s:cp ]",
"[ 5/2 → 13/5 | s:ht ]",
"[ 13/5 → 27/10 | s:bd ]",
"[ 27/10 → 14/5 | s:cp ]",
"[ 14/5 → 29/10 | s:ht ]",
"[ 29/10 → 3/1 | s:mt ]",
"[ 3/1 → 31/10 | s:bd ]",
"[ 31/10 → 16/5 | s:bd ]",
"[ 16/5 → 33/10 | s:cp ]",
"[ 33/10 → 17/5 | s:bd ]",
"[ 17/5 → 7/2 | s:cp ]",
"[ 7/2 → 18/5 | s:ht ]",
"[ 18/5 → 37/10 | s:bd ]",
"[ 37/10 → 19/5 | s:cp ]",
"[ 19/5 → 39/10 | s:ht ]",
"[ 39/10 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "grow" example index 1 1`] = `
[
"[ 0/1 → 1/10 | s:mt ]",
"[ 1/10 → 1/5 | s:ht ]",
"[ 1/5 → 3/10 | s:mt ]",
"[ 3/10 → 2/5 | s:cp ]",
"[ 2/5 → 1/2 | s:ht ]",
"[ 1/2 → 3/5 | s:mt ]",
"[ 3/5 → 7/10 | s:bd ]",
"[ 7/10 → 4/5 | s:cp ]",
"[ 4/5 → 9/10 | s:ht ]",
"[ 9/10 → 1/1 | s:mt ]",
"[ 1/1 → 11/10 | s:mt ]",
"[ 11/10 → 6/5 | s:ht ]",
"[ 6/5 → 13/10 | s:mt ]",
"[ 13/10 → 7/5 | s:cp ]",
"[ 7/5 → 3/2 | s:ht ]",
"[ 3/2 → 8/5 | s:mt ]",
"[ 8/5 → 17/10 | s:bd ]",
"[ 17/10 → 9/5 | s:cp ]",
"[ 9/5 → 19/10 | s:ht ]",
"[ 19/10 → 2/1 | s:mt ]",
"[ 2/1 → 21/10 | s:mt ]",
"[ 21/10 → 11/5 | s:ht ]",
"[ 11/5 → 23/10 | s:mt ]",
"[ 23/10 → 12/5 | s:cp ]",
"[ 12/5 → 5/2 | s:ht ]",
"[ 5/2 → 13/5 | s:mt ]",
"[ 13/5 → 27/10 | s:bd ]",
"[ 27/10 → 14/5 | s:cp ]",
"[ 14/5 → 29/10 | s:ht ]",
"[ 29/10 → 3/1 | s:mt ]",
"[ 3/1 → 31/10 | s:mt ]",
"[ 31/10 → 16/5 | s:ht ]",
"[ 16/5 → 33/10 | s:mt ]",
"[ 33/10 → 17/5 | s:cp ]",
"[ 17/5 → 7/2 | s:ht ]",
"[ 7/2 → 18/5 | s:mt ]",
"[ 18/5 → 37/10 | s:bd ]",
"[ 37/10 → 19/5 | s:cp ]",
"[ 19/5 → 39/10 | s:ht ]",
"[ 39/10 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "hpattack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
@ -5057,6 +5239,27 @@ exports[`runs examples > example "outside" example index 0 1`] = `
]
`;
exports[`runs examples > example "pace" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:sd ]",
"[ 1/2 → 3/4 | s:cp ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:sd ]",
"[ 5/4 → 3/2 | s:cp ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:sd ]",
"[ 2/1 → 9/4 | s:cp ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:sd ]",
"[ 11/4 → 3/1 | s:cp ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:sd ]",
"[ 7/2 → 15/4 | s:cp ]",
"[ 15/4 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "palindrome" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
@ -5651,6 +5854,72 @@ exports[`runs examples > example "ply" example index 0 1`] = `
]
`;
exports[`runs examples > example "polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c ]",
"[ 0/1 → 1/3 | note:c2 ]",
"[ 1/3 → 2/3 | note:eb ]",
"[ 1/3 → 2/3 | note:g2 ]",
"[ 2/3 → 1/1 | note:g ]",
"[ 2/3 → 1/1 | note:c2 ]",
"[ 1/1 → 4/3 | note:c ]",
"[ 1/1 → 4/3 | note:g2 ]",
"[ 4/3 → 5/3 | note:eb ]",
"[ 4/3 → 5/3 | note:c2 ]",
"[ 5/3 → 2/1 | note:g ]",
"[ 5/3 → 2/1 | note:g2 ]",
"[ 2/1 → 7/3 | note:c ]",
"[ 2/1 → 7/3 | note:c2 ]",
"[ 7/3 → 8/3 | note:eb ]",
"[ 7/3 → 8/3 | note:g2 ]",
"[ 8/3 → 3/1 | note:g ]",
"[ 8/3 → 3/1 | note:c2 ]",
"[ 3/1 → 10/3 | note:c ]",
"[ 3/1 → 10/3 | note:g2 ]",
"[ 10/3 → 11/3 | note:eb ]",
"[ 10/3 → 11/3 | note:c2 ]",
"[ 11/3 → 4/1 | note:g ]",
"[ 11/3 → 4/1 | note:g2 ]",
]
`;
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
"[ 0/1 → 1/4 | note:e ]",
"[ 1/4 → 1/2 | note:d ]",
"[ 1/4 → 1/2 | note:f ]",
"[ 1/2 → 3/4 | note:c ]",
"[ 1/2 → 3/4 | note:g ]",
"[ 3/4 → 1/1 | note:d ]",
"[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | note:c ]",
"[ 1/1 → 5/4 | note:f ]",
"[ 5/4 → 3/2 | note:d ]",
"[ 5/4 → 3/2 | note:g ]",
"[ 3/2 → 7/4 | note:c ]",
"[ 3/2 → 7/4 | note:e ]",
"[ 7/4 → 2/1 | note:d ]",
"[ 7/4 → 2/1 | note:f ]",
"[ 2/1 → 9/4 | note:c ]",
"[ 2/1 → 9/4 | note:g ]",
"[ 9/4 → 5/2 | note:d ]",
"[ 9/4 → 5/2 | note:e ]",
"[ 5/2 → 11/4 | note:c ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:d ]",
"[ 11/4 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | note:c ]",
"[ 3/1 → 13/4 | note:e ]",
"[ 13/4 → 7/2 | note:d ]",
"[ 13/4 → 7/2 | note:f ]",
"[ 7/2 → 15/4 | note:c ]",
"[ 7/2 → 15/4 | note:g ]",
"[ 15/4 → 4/1 | note:d ]",
"[ 15/4 → 4/1 | note:e ]",
]
`;
exports[`runs examples > example "postgain" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
@ -6522,135 +6791,6 @@ exports[`runs examples > example "s" example index 1 1`] = `
]
`;
exports[`runs examples > example "s_alt" example index 0 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:cp ]",
"[ 2/5 → 3/5 | s:bd ]",
"[ 3/5 → 4/5 | s:mt ]",
"[ 4/5 → 1/1 | s:bd ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:cp ]",
"[ 7/5 → 8/5 | s:bd ]",
"[ 8/5 → 9/5 | s:mt ]",
"[ 9/5 → 2/1 | s:bd ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:cp ]",
"[ 12/5 → 13/5 | s:bd ]",
"[ 13/5 → 14/5 | s:mt ]",
"[ 14/5 → 3/1 | s:bd ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:cp ]",
"[ 17/5 → 18/5 | s:bd ]",
"[ 18/5 → 19/5 | s:mt ]",
"[ 19/5 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "s_cat" example index 0 1`] = `
[
"[ 0/1 → 3/4 | note:e3 ]",
"[ 3/4 → 1/1 | note:g3 ]",
"[ 1/1 → 7/4 | note:e3 ]",
"[ 7/4 → 2/1 | note:g3 ]",
"[ 2/1 → 11/4 | note:e3 ]",
"[ 11/4 → 3/1 | note:g3 ]",
"[ 3/1 → 15/4 | note:e3 ]",
"[ 15/4 → 4/1 | note:g3 ]",
]
`;
exports[`runs examples > example "s_cat" example index 1 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:sd ]",
"[ 2/5 → 3/5 | s:cp ]",
"[ 3/5 → 4/5 | s:hh ]",
"[ 4/5 → 1/1 | s:hh ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:sd ]",
"[ 7/5 → 8/5 | s:cp ]",
"[ 8/5 → 9/5 | s:hh ]",
"[ 9/5 → 2/1 | s:hh ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:sd ]",
"[ 12/5 → 13/5 | s:cp ]",
"[ 13/5 → 14/5 | s:hh ]",
"[ 14/5 → 3/1 | s:hh ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:sd ]",
"[ 17/5 → 18/5 | s:cp ]",
"[ 18/5 → 19/5 | s:hh ]",
"[ 19/5 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "s_polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c ]",
"[ 0/1 → 1/3 | note:c2 ]",
"[ 1/3 → 2/3 | note:eb ]",
"[ 1/3 → 2/3 | note:g2 ]",
"[ 2/3 → 1/1 | note:g ]",
"[ 2/3 → 1/1 | note:c2 ]",
"[ 1/1 → 4/3 | note:c ]",
"[ 1/1 → 4/3 | note:g2 ]",
"[ 4/3 → 5/3 | note:eb ]",
"[ 4/3 → 5/3 | note:c2 ]",
"[ 5/3 → 2/1 | note:g ]",
"[ 5/3 → 2/1 | note:g2 ]",
"[ 2/1 → 7/3 | note:c ]",
"[ 2/1 → 7/3 | note:c2 ]",
"[ 7/3 → 8/3 | note:eb ]",
"[ 7/3 → 8/3 | note:g2 ]",
"[ 8/3 → 3/1 | note:g ]",
"[ 8/3 → 3/1 | note:c2 ]",
"[ 3/1 → 10/3 | note:c ]",
"[ 3/1 → 10/3 | note:g2 ]",
"[ 10/3 → 11/3 | note:eb ]",
"[ 10/3 → 11/3 | note:c2 ]",
"[ 11/3 → 4/1 | note:g ]",
"[ 11/3 → 4/1 | note:g2 ]",
]
`;
exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
"[ 0/1 → 1/4 | note:e ]",
"[ 1/4 → 1/2 | note:d ]",
"[ 1/4 → 1/2 | note:f ]",
"[ 1/2 → 3/4 | note:c ]",
"[ 1/2 → 3/4 | note:g ]",
"[ 3/4 → 1/1 | note:d ]",
"[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | note:c ]",
"[ 1/1 → 5/4 | note:f ]",
"[ 5/4 → 3/2 | note:d ]",
"[ 5/4 → 3/2 | note:g ]",
"[ 3/2 → 7/4 | note:c ]",
"[ 3/2 → 7/4 | note:e ]",
"[ 7/4 → 2/1 | note:d ]",
"[ 7/4 → 2/1 | note:f ]",
"[ 2/1 → 9/4 | note:c ]",
"[ 2/1 → 9/4 | note:g ]",
"[ 9/4 → 5/2 | note:d ]",
"[ 9/4 → 5/2 | note:e ]",
"[ 5/2 → 11/4 | note:c ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:d ]",
"[ 11/4 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | note:c ]",
"[ 3/1 → 13/4 | note:e ]",
"[ 13/4 → 7/2 | note:d ]",
"[ 13/4 → 7/2 | note:f ]",
"[ 7/2 → 15/4 | note:c ]",
"[ 7/2 → 15/4 | note:g ]",
"[ 15/4 → 4/1 | note:d ]",
"[ 15/4 → 4/1 | note:e ]",
]
`;
exports[`runs examples > example "samples" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@ -7238,6 +7378,181 @@ exports[`runs examples > example "shape" example index 0 1`] = `
]
`;
exports[`runs examples > example "shrink" example index 0 1`] = `
[
"[ 0/1 → 1/10 | s:bd ]",
"[ 1/10 → 1/5 | s:cp ]",
"[ 1/5 → 3/10 | s:ht ]",
"[ 3/10 → 2/5 | s:mt ]",
"[ 2/5 → 1/2 | s:cp ]",
"[ 1/2 → 3/5 | s:ht ]",
"[ 3/5 → 7/10 | s:mt ]",
"[ 7/10 → 4/5 | s:ht ]",
"[ 4/5 → 9/10 | s:mt ]",
"[ 9/10 → 1/1 | s:mt ]",
"[ 1/1 → 11/10 | s:bd ]",
"[ 11/10 → 6/5 | s:cp ]",
"[ 6/5 → 13/10 | s:ht ]",
"[ 13/10 → 7/5 | s:mt ]",
"[ 7/5 → 3/2 | s:cp ]",
"[ 3/2 → 8/5 | s:ht ]",
"[ 8/5 → 17/10 | s:mt ]",
"[ 17/10 → 9/5 | s:ht ]",
"[ 9/5 → 19/10 | s:mt ]",
"[ 19/10 → 2/1 | s:mt ]",
"[ 2/1 → 21/10 | s:bd ]",
"[ 21/10 → 11/5 | s:cp ]",
"[ 11/5 → 23/10 | s:ht ]",
"[ 23/10 → 12/5 | s:mt ]",
"[ 12/5 → 5/2 | s:cp ]",
"[ 5/2 → 13/5 | s:ht ]",
"[ 13/5 → 27/10 | s:mt ]",
"[ 27/10 → 14/5 | s:ht ]",
"[ 14/5 → 29/10 | s:mt ]",
"[ 29/10 → 3/1 | s:mt ]",
"[ 3/1 → 31/10 | s:bd ]",
"[ 31/10 → 16/5 | s:cp ]",
"[ 16/5 → 33/10 | s:ht ]",
"[ 33/10 → 17/5 | s:mt ]",
"[ 17/5 → 7/2 | s:cp ]",
"[ 7/2 → 18/5 | s:ht ]",
"[ 18/5 → 37/10 | s:mt ]",
"[ 37/10 → 19/5 | s:ht ]",
"[ 19/5 → 39/10 | s:mt ]",
"[ 39/10 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "shrink" example index 1 1`] = `
[
"[ 0/1 → 1/10 | s:bd ]",
"[ 1/10 → 1/5 | s:cp ]",
"[ 1/5 → 3/10 | s:ht ]",
"[ 3/10 → 2/5 | s:mt ]",
"[ 2/5 → 1/2 | s:bd ]",
"[ 1/2 → 3/5 | s:cp ]",
"[ 3/5 → 7/10 | s:ht ]",
"[ 7/10 → 4/5 | s:bd ]",
"[ 4/5 → 9/10 | s:cp ]",
"[ 9/10 → 1/1 | s:bd ]",
"[ 1/1 → 11/10 | s:bd ]",
"[ 11/10 → 6/5 | s:cp ]",
"[ 6/5 → 13/10 | s:ht ]",
"[ 13/10 → 7/5 | s:mt ]",
"[ 7/5 → 3/2 | s:bd ]",
"[ 3/2 → 8/5 | s:cp ]",
"[ 8/5 → 17/10 | s:ht ]",
"[ 17/10 → 9/5 | s:bd ]",
"[ 9/5 → 19/10 | s:cp ]",
"[ 19/10 → 2/1 | s:bd ]",
"[ 2/1 → 21/10 | s:bd ]",
"[ 21/10 → 11/5 | s:cp ]",
"[ 11/5 → 23/10 | s:ht ]",
"[ 23/10 → 12/5 | s:mt ]",
"[ 12/5 → 5/2 | s:bd ]",
"[ 5/2 → 13/5 | s:cp ]",
"[ 13/5 → 27/10 | s:ht ]",
"[ 27/10 → 14/5 | s:bd ]",
"[ 14/5 → 29/10 | s:cp ]",
"[ 29/10 → 3/1 | s:bd ]",
"[ 3/1 → 31/10 | s:bd ]",
"[ 31/10 → 16/5 | s:cp ]",
"[ 16/5 → 33/10 | s:ht ]",
"[ 33/10 → 17/5 | s:mt ]",
"[ 17/5 → 7/2 | s:bd ]",
"[ 7/2 → 18/5 | s:cp ]",
"[ 18/5 → 37/10 | s:ht ]",
"[ 37/10 → 19/5 | s:bd ]",
"[ 19/5 → 39/10 | s:cp ]",
"[ 39/10 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "shrink" example index 2 1`] = `
[
"[ 0/1 → 1/20 | s:bd ]",
"[ 1/20 → 1/10 | s:bd ]",
"[ 1/10 → 3/20 | s:cp ]",
"[ 3/20 → 1/5 | s:bd ]",
"[ 1/5 → 1/4 | s:cp ]",
"[ 1/4 → 3/10 | s:ht ]",
"[ 3/10 → 7/20 | s:bd ]",
"[ 7/20 → 2/5 | s:cp ]",
"[ 2/5 → 9/20 | s:ht ]",
"[ 9/20 → 1/2 | s:mt ]",
"[ 1/2 → 11/20 | s:mt ]",
"[ 11/20 → 3/5 | s:ht ]",
"[ 3/5 → 13/20 | s:mt ]",
"[ 13/20 → 7/10 | s:cp ]",
"[ 7/10 → 3/4 | s:ht ]",
"[ 3/4 → 4/5 | s:mt ]",
"[ 4/5 → 17/20 | s:bd ]",
"[ 17/20 → 9/10 | s:cp ]",
"[ 9/10 → 19/20 | s:ht ]",
"[ 19/20 → 1/1 | s:mt ]",
"[ 1/1 → 21/20 | s:bd ]",
"[ 21/20 → 11/10 | s:bd ]",
"[ 11/10 → 23/20 | s:cp ]",
"[ 23/20 → 6/5 | s:bd ]",
"[ 6/5 → 5/4 | s:cp ]",
"[ 5/4 → 13/10 | s:ht ]",
"[ 13/10 → 27/20 | s:bd ]",
"[ 27/20 → 7/5 | s:cp ]",
"[ 7/5 → 29/20 | s:ht ]",
"[ 29/20 → 3/2 | s:mt ]",
"[ 3/2 → 31/20 | s:mt ]",
"[ 31/20 → 8/5 | s:ht ]",
"[ 8/5 → 33/20 | s:mt ]",
"[ 33/20 → 17/10 | s:cp ]",
"[ 17/10 → 7/4 | s:ht ]",
"[ 7/4 → 9/5 | s:mt ]",
"[ 9/5 → 37/20 | s:bd ]",
"[ 37/20 → 19/10 | s:cp ]",
"[ 19/10 → 39/20 | s:ht ]",
"[ 39/20 → 2/1 | s:mt ]",
"[ 2/1 → 41/20 | s:bd ]",
"[ 41/20 → 21/10 | s:bd ]",
"[ 21/10 → 43/20 | s:cp ]",
"[ 43/20 → 11/5 | s:bd ]",
"[ 11/5 → 9/4 | s:cp ]",
"[ 9/4 → 23/10 | s:ht ]",
"[ 23/10 → 47/20 | s:bd ]",
"[ 47/20 → 12/5 | s:cp ]",
"[ 12/5 → 49/20 | s:ht ]",
"[ 49/20 → 5/2 | s:mt ]",
"[ 5/2 → 51/20 | s:mt ]",
"[ 51/20 → 13/5 | s:ht ]",
"[ 13/5 → 53/20 | s:mt ]",
"[ 53/20 → 27/10 | s:cp ]",
"[ 27/10 → 11/4 | s:ht ]",
"[ 11/4 → 14/5 | s:mt ]",
"[ 14/5 → 57/20 | s:bd ]",
"[ 57/20 → 29/10 | s:cp ]",
"[ 29/10 → 59/20 | s:ht ]",
"[ 59/20 → 3/1 | s:mt ]",
"[ 3/1 → 61/20 | s:bd ]",
"[ 61/20 → 31/10 | s:bd ]",
"[ 31/10 → 63/20 | s:cp ]",
"[ 63/20 → 16/5 | s:bd ]",
"[ 16/5 → 13/4 | s:cp ]",
"[ 13/4 → 33/10 | s:ht ]",
"[ 33/10 → 67/20 | s:bd ]",
"[ 67/20 → 17/5 | s:cp ]",
"[ 17/5 → 69/20 | s:ht ]",
"[ 69/20 → 7/2 | s:mt ]",
"[ 7/2 → 71/20 | s:mt ]",
"[ 71/20 → 18/5 | s:ht ]",
"[ 18/5 → 73/20 | s:mt ]",
"[ 73/20 → 37/10 | s:cp ]",
"[ 37/10 → 15/4 | s:ht ]",
"[ 15/4 → 19/5 | s:mt ]",
"[ 19/5 → 77/20 | s:bd ]",
"[ 77/20 → 39/10 | s:cp ]",
"[ 39/10 → 79/20 | s:ht ]",
"[ 79/20 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "shuffle
Slices a pattern into the given number of parts, then plays those parts in random order.
Each part will be played exactly once per cycle." example index 0 1`] = `
@ -7988,24 +8303,66 @@ exports[`runs examples > example "stack" example index 1 1`] = `
]
`;
exports[`runs examples > example "steps" example index 0 1`] = `
exports[`runs examples > example "stepalt" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:sd ]",
"[ 1/2 → 3/4 | s:cp ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:sd ]",
"[ 5/4 → 3/2 | s:cp ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:sd ]",
"[ 2/1 → 9/4 | s:cp ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:sd ]",
"[ 11/4 → 3/1 | s:cp ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:sd ]",
"[ 7/2 → 15/4 | s:cp ]",
"[ 15/4 → 4/1 | s:bd ]",
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:cp ]",
"[ 2/5 → 3/5 | s:bd ]",
"[ 3/5 → 4/5 | s:mt ]",
"[ 4/5 → 1/1 | s:bd ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:cp ]",
"[ 7/5 → 8/5 | s:bd ]",
"[ 8/5 → 9/5 | s:mt ]",
"[ 9/5 → 2/1 | s:bd ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:cp ]",
"[ 12/5 → 13/5 | s:bd ]",
"[ 13/5 → 14/5 | s:mt ]",
"[ 14/5 → 3/1 | s:bd ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:cp ]",
"[ 17/5 → 18/5 | s:bd ]",
"[ 18/5 → 19/5 | s:mt ]",
"[ 19/5 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "stepcat" example index 0 1`] = `
[
"[ 0/1 → 3/4 | note:e3 ]",
"[ 3/4 → 1/1 | note:g3 ]",
"[ 1/1 → 7/4 | note:e3 ]",
"[ 7/4 → 2/1 | note:g3 ]",
"[ 2/1 → 11/4 | note:e3 ]",
"[ 11/4 → 3/1 | note:g3 ]",
"[ 3/1 → 15/4 | note:e3 ]",
"[ 15/4 → 4/1 | note:g3 ]",
]
`;
exports[`runs examples > example "stepcat" example index 1 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:sd ]",
"[ 2/5 → 3/5 | s:cp ]",
"[ 3/5 → 4/5 | s:hh ]",
"[ 4/5 → 1/1 | s:hh ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:sd ]",
"[ 7/5 → 8/5 | s:cp ]",
"[ 8/5 → 9/5 | s:hh ]",
"[ 9/5 → 2/1 | s:hh ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:sd ]",
"[ 12/5 → 13/5 | s:cp ]",
"[ 13/5 → 14/5 | s:hh ]",
"[ 14/5 → 3/1 | s:hh ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:sd ]",
"[ 17/5 → 18/5 | s:cp ]",
"[ 18/5 → 19/5 | s:hh ]",
"[ 19/5 → 4/1 | s:hh ]",
]
`;
@ -8318,6 +8675,118 @@ exports[`runs examples > example "swingBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "take" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
"[ 1/2 → 1/1 | s:cp ]",
"[ 1/1 → 3/2 | s:bd ]",
"[ 3/2 → 2/1 | s:cp ]",
"[ 2/1 → 5/2 | s:bd ]",
"[ 5/2 → 3/1 | s:cp ]",
"[ 3/1 → 7/2 | s:bd ]",
"[ 7/2 → 4/1 | s:cp ]",
]
`;
exports[`runs examples > example "take" example index 1 1`] = `
[
"[ 0/1 → 1/6 | s:bd ]",
"[ 1/6 → 1/3 | s:bd ]",
"[ 1/3 → 1/2 | s:cp ]",
"[ 1/2 → 2/3 | s:bd ]",
"[ 2/3 → 5/6 | s:cp ]",
"[ 5/6 → 1/1 | s:ht ]",
"[ 1/1 → 7/6 | s:bd ]",
"[ 7/6 → 4/3 | s:bd ]",
"[ 4/3 → 3/2 | s:cp ]",
"[ 3/2 → 5/3 | s:bd ]",
"[ 5/3 → 11/6 | s:cp ]",
"[ 11/6 → 2/1 | s:ht ]",
"[ 2/1 → 13/6 | s:bd ]",
"[ 13/6 → 7/3 | s:bd ]",
"[ 7/3 → 5/2 | s:cp ]",
"[ 5/2 → 8/3 | s:bd ]",
"[ 8/3 → 17/6 | s:cp ]",
"[ 17/6 → 3/1 | s:ht ]",
"[ 3/1 → 19/6 | s:bd ]",
"[ 19/6 → 10/3 | s:bd ]",
"[ 10/3 → 7/2 | s:cp ]",
"[ 7/2 → 11/3 | s:bd ]",
"[ 11/3 → 23/6 | s:cp ]",
"[ 23/6 → 4/1 | s:ht ]",
]
`;
exports[`runs examples > example "take" example index 2 1`] = `
[
"[ 0/1 → 1/6 | s:mt ]",
"[ 1/6 → 1/3 | s:ht ]",
"[ 1/3 → 1/2 | s:mt ]",
"[ 1/2 → 2/3 | s:cp ]",
"[ 2/3 → 5/6 | s:ht ]",
"[ 5/6 → 1/1 | s:mt ]",
"[ 1/1 → 7/6 | s:mt ]",
"[ 7/6 → 4/3 | s:ht ]",
"[ 4/3 → 3/2 | s:mt ]",
"[ 3/2 → 5/3 | s:cp ]",
"[ 5/3 → 11/6 | s:ht ]",
"[ 11/6 → 2/1 | s:mt ]",
"[ 2/1 → 13/6 | s:mt ]",
"[ 13/6 → 7/3 | s:ht ]",
"[ 7/3 → 5/2 | s:mt ]",
"[ 5/2 → 8/3 | s:cp ]",
"[ 8/3 → 17/6 | s:ht ]",
"[ 17/6 → 3/1 | s:mt ]",
"[ 3/1 → 19/6 | s:mt ]",
"[ 19/6 → 10/3 | s:ht ]",
"[ 10/3 → 7/2 | s:mt ]",
"[ 7/2 → 11/3 | s:cp ]",
"[ 11/3 → 23/6 | s:ht ]",
"[ 23/6 → 4/1 | s:mt ]",
]
`;
exports[`runs examples > example "tour" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:e s:folkharp ]",
"[ 1/8 → 1/4 | note:f s:folkharp ]",
"[ 1/4 → 3/8 | note:e s:folkharp ]",
"[ 3/8 → 1/2 | note:f s:folkharp ]",
"[ 1/2 → 5/8 | note:g s:folkharp ]",
"[ 5/8 → 3/4 | note:g s:folkharp ]",
"[ 3/4 → 7/8 | note:f s:folkharp ]",
"[ 7/8 → 1/1 | note:e s:folkharp ]",
"[ 1/1 → 9/8 | note:c s:folkharp ]",
"[ 9/8 → 19/16 | note:c s:folkharp ]",
"[ 19/16 → 5/4 | note:g s:folkharp ]",
"[ 5/4 → 11/8 | note:e s:folkharp ]",
"[ 11/8 → 3/2 | note:f s:folkharp ]",
"[ 3/2 → 13/8 | note:e s:folkharp ]",
"[ 13/8 → 7/4 | note:f s:folkharp ]",
"[ 7/4 → 15/8 | note:g s:folkharp ]",
"[ 15/8 → 31/16 | note:c s:folkharp ]",
"[ 31/16 → 2/1 | note:g s:folkharp ]",
"[ 2/1 → 17/8 | note:g s:folkharp ]",
"[ 17/8 → 9/4 | note:f s:folkharp ]",
"[ 9/4 → 19/8 | note:e s:folkharp ]",
"[ 19/8 → 5/2 | note:c s:folkharp ]",
"[ 5/2 → 21/8 | note:e s:folkharp ]",
"[ 21/8 → 11/4 | note:f s:folkharp ]",
"[ 11/4 → 45/16 | note:c s:folkharp ]",
"[ 45/16 → 23/8 | note:g s:folkharp ]",
"[ 23/8 → 3/1 | note:e s:folkharp ]",
"[ 3/1 → 25/8 | note:f s:folkharp ]",
"[ 25/8 → 13/4 | note:g s:folkharp ]",
"[ 13/4 → 27/8 | note:g s:folkharp ]",
"[ 27/8 → 7/2 | note:f s:folkharp ]",
"[ 7/2 → 29/8 | note:e s:folkharp ]",
"[ 29/8 → 15/4 | note:c s:folkharp ]",
"[ 15/4 → 61/16 | note:c s:folkharp ]",
"[ 61/16 → 31/8 | note:g s:folkharp ]",
"[ 31/8 → 4/1 | note:e s:folkharp ]",
]
`;
exports[`runs examples > example "transpose" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C2 ]",
@ -9003,6 +9472,45 @@ exports[`runs examples > example "xfade" example index 0 1`] = `
]
`;
exports[`runs examples > example "zip" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:e s:folkharp ]",
"[ 1/8 → 1/4 | note:e s:folkharp ]",
"[ 1/4 → 3/8 | note:g s:folkharp ]",
"[ 3/8 → 1/2 | note:f s:folkharp ]",
"[ 1/2 → 5/8 | note:f s:folkharp ]",
"[ 5/8 → 11/16 | note:f s:folkharp ]",
"[ 11/16 → 3/4 | note:e s:folkharp ]",
"[ 3/4 → 7/8 | note:e s:folkharp ]",
"[ 7/8 → 1/1 | note:g s:folkharp ]",
"[ 1/1 → 9/8 | note:a s:folkharp ]",
"[ 9/8 → 5/4 | note:f s:folkharp ]",
"[ 5/4 → 11/8 | note:e s:folkharp ]",
"[ 11/8 → 3/2 | note:f4 s:folkharp ]",
"[ 3/2 → 13/8 | note:e s:folkharp ]",
"[ 13/8 → 7/4 | note:f s:folkharp ]",
"[ 7/4 → 15/8 | note:c s:folkharp ]",
"[ 15/8 → 2/1 | note:f s:folkharp ]",
"[ 2/1 → 17/8 | note:g s:folkharp ]",
"[ 17/8 → 9/4 | note:g s:folkharp ]",
"[ 9/4 → 19/8 | note:e s:folkharp ]",
"[ 19/8 → 5/2 | note:e s:folkharp ]",
"[ 5/2 → 41/16 | note:f s:folkharp ]",
"[ 41/16 → 21/8 | note:e s:folkharp ]",
"[ 21/8 → 11/4 | note:f s:folkharp ]",
"[ 11/4 → 23/8 | note:f s:folkharp ]",
"[ 23/8 → 3/1 | note:a s:folkharp ]",
"[ 3/1 → 25/8 | note:e s:folkharp ]",
"[ 25/8 → 13/4 | note:g s:folkharp ]",
"[ 13/4 → 27/8 | note:f4 s:folkharp ]",
"[ 27/8 → 7/2 | note:f s:folkharp ]",
"[ 7/2 → 29/8 | note:e s:folkharp ]",
"[ 29/8 → 15/4 | note:c s:folkharp ]",
"[ 15/4 → 31/8 | note:e s:folkharp ]",
"[ 31/8 → 4/1 | note:f s:folkharp ]",
]
`;
exports[`runs examples > example "zoom" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:hh ]",

View File

@ -96,6 +96,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
{ text: 'Accumulation', link: 'learn/accumulation' },
{ text: 'Tonal Functions', link: 'learn/tonal' },
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
],
Understand: [
{ text: 'Coding syntax', link: 'learn/code' },

View File

@ -11,15 +11,15 @@ import { JsDoc } from '../../docs/JsDoc';
The following functions will return a pattern.
These are the equivalents used by the Mini Notation:
| function | mini |
| -------------------------------- | ---------------- |
| `cat(x, y)` | `"<x y>"` |
| `seq(x, y)` | `"x y"` |
| `stack(x, y)` | `"x,y"` |
| `s_cat([3,x],[2,y])` | `"x@3 y@2"` |
| `s_polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` |
| `s_polymeterSteps(2, x, y, z)` | `"{x y z}%2"` |
| `silence` | `"~"` |
| function | mini |
| ------------------------------ | ---------------- |
| `cat(x, y)` | `"<x y>"` |
| `seq(x, y)` | `"x y"` |
| `stack(x, y)` | `"x,y"` |
| `stepcat([3,x],[2,y])` | `"x@3 y@2"` |
| `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` |
| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` |
| `silence` | `"~"` |
## cat
@ -33,21 +33,21 @@ These are the equivalents used by the Mini Notation:
<JsDoc client:idle name="stack" h={0} />
## s_cat
## stepcat
<JsDoc client:idle name="s_cat" h={0} />
<JsDoc client:idle name="stepcat" h={0} />
## arrange
<JsDoc client:idle name="arrange" h={0} />
## s_polymeter
## polymeter
<JsDoc client:idle name="s_polymeter" h={0} />
<JsDoc client:idle name="polymeter" h={0} />
## s_polymeterSteps
## polymeterSteps
<JsDoc client:idle name="s_polymeterSteps" h={0} />
<JsDoc client:idle name="polymeterSteps" h={0} />
## silence

View File

@ -0,0 +1,139 @@
---
title: Stepwise patterning
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Stepwise patterning (experimental)
This is a developing area of strudel, and behaviour might change or be renamed in future versions. Feedback and ideas are welcome!
## Introduction
Usually in strudel, the only reference point for most pattern transformations is the _cycle_. Now it is possible to also work with _steps_, via a growing range of functions.
For example usually when you `fastcat` two patterns together, the cycles will be squashed into half a cycle each:
<MiniRepl client:idle tune={`fastcat("bd hh hh", "bd hh hh cp hh").sound()`} />
With the new stepwise `stepcat` function, the steps of the two patterns will be evenly distributed across the cycle:
<MiniRepl client:idle tune={`stepcat("bd hh hh", "bd hh hh cp hh").sound()`} />
By default, steps are counted according to the 'top level' in mini-notation. For example `"a [b c] d e"` has five events in it per cycle, but is counted as four steps, where `[b c]` is counted as a single step.
However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattenr are twice as long, and so counted as two steps each.
## Pacing the steps
Some stepwise functions don't appear to do very much on their own, for example these two examples of the `expand` function sound exactly the same despite being expanded by different amounts:
<MiniRepl client:idle tune={`"c a f e".expand(2).note().sound("folkharp")`} />
<MiniRepl client:idle tune={`"c a f e".expand(4).note().sound("folkharp")`} />
The number of steps per cycle is being changed behind the scenes, but on its own, that doesn't do anything. You will hear a difference however, once you use another stepwise function with it, for example `stepcat`:
<MiniRepl
client:idle
tune={`stepcat("c a f e".expand(2), "g d").note()
.sound("folkharp")`}
/>
<MiniRepl
client:idle
tune={`stepcat("c a f e".expand(4), "g d").note()
.sound("folkharp")`}
/>
You should be able to hear that `expand` increases the duration of the steps of the first subpattern, proportionally to the second one.
You can also change the speed of a pattern to match a given number of steps per cycle, with the `pace` function:
<MiniRepl
client:idle
tune={`stepcat("c a f e".expand(2), "g d").note()
.sound("folkharp")
.pace(8)`}
/>
<MiniRepl
client:idle
tune={`stepcat("c a f e".expand(4), "g d").note()
.sound("folkharp")
.pace(8)`}
/>
The first example has ten steps, and the second example has 18 steps, but are then both played a rate of 8 steps per cycle.
The argument to `expand` can also be patterned, and will be treated in a stepwise fashion. This means that the patterns from the changing values in the argument will be `stepcat`ted together:
<MiniRepl client:idle tune={`note("c a f e").sound("folkharp").expand("3 2 1 1 2 3")`} />
This results in a dense pattern, because the different expanded versions are squashed into a single cycle. `pace` is again handy here for slowing down the pattern to a particular number of steps per cycle:
<MiniRepl client:idle tune={`note("c a f e").sound("folkharp").expand("3 2 1 1 2 3").pace(8)`} />
## Deprecated aliases
Earlier versions of many of these functions had `s_` prefixes, and the `pace` function was previously known as `steps`. These still exist as aliases, but may have changed behaviour and will soon be removed. Please update your patterns!
## Stepwise functions
### pace
<JsDoc client:idle name="pace" h={0} />
### stepcat
<JsDoc client:idle name="stepcat" h={0} />
### stepalt
<JsDoc client:idle name="stepalt" h={0} />
### expand
<JsDoc client:idle name="expand" h={0} />
### contract
<JsDoc client:idle name="contract" h={0} />
### repeat
<JsDoc client:idle name="repeat" h={0} />
### take
<JsDoc client:idle name="take" h={0} />
### drop
<JsDoc client:idle name="drop" h={0} />
### polymeter
<JsDoc client:idle name="polymeter" h={0} />
### polymeterSteps
<JsDoc client:idle name="polymeterSteps" h={0} />
### shrink
<JsDoc client:idle name="shrink" h={0} />
### grow
<JsDoc client:idle name="grow" h={0} />
### tour
<JsDoc client:idle name="tour" h={0} />
### zip
<JsDoc client:idle name="zip" h={0} />