mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
More experimental step functions: stepwax, stepwane, steptaper, steptour and taperlist (#1042)
* group tactus-oriented functions * add stepwax, stepwane, steptaper and steptour * taperlist
This commit is contained in:
parent
558e9e36ff
commit
ebeefc0ac4
@ -1328,7 +1328,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))));
|
||||
};
|
||||
return new Pattern(query).splitQueries();
|
||||
const tactus = lcm(...pats.map((x) => x.tactus));
|
||||
return new Pattern(query).splitQueries().setTactus(tactus);
|
||||
}
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles.
|
||||
@ -1359,42 +1360,6 @@ export function cat(...pats) {
|
||||
return slowcat(...pats);
|
||||
}
|
||||
|
||||
/** 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.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* timecat([3,"e3"],[1, "g3"]).note()
|
||||
* // the same as "e3@3 g3".note()
|
||||
* @example
|
||||
* timecat("bd sd cp","hh hh").sound()
|
||||
* // the same as "bd sd cp hh hh".sound()
|
||||
*/
|
||||
export function timecat(...timepats) {
|
||||
const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]);
|
||||
timepats = timepats.map(findtactus);
|
||||
if (timepats.length == 1) {
|
||||
const result = reify(timepats[0][1]);
|
||||
result.tactus = timepats[0][0];
|
||||
return result;
|
||||
}
|
||||
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
let begin = Fraction(0);
|
||||
const pats = [];
|
||||
for (const [time, pat] of timepats) {
|
||||
const end = begin.add(time);
|
||||
pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
|
||||
begin = end;
|
||||
}
|
||||
const result = stack(...pats);
|
||||
result.tactus = total;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Deprecated alias for `timecat` */
|
||||
export const timeCat = timecat;
|
||||
|
||||
/**
|
||||
* Allows to arrange multiple patterns together over multiple cycles.
|
||||
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||
@ -1426,30 +1391,6 @@ export function sequence(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates patterns stepwise, according to their 'tactus'.
|
||||
* Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stepcat(["bd cp", "mt"], "bd").sound()
|
||||
*/
|
||||
export function stepcat(...groups) {
|
||||
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
|
||||
|
||||
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
|
||||
|
||||
let result = [];
|
||||
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.tactus > 0);
|
||||
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||
result = timecat(...result);
|
||||
result.tactus = tactus;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Like **cat**, but the items are crammed into one cycle.
|
||||
* @synonyms fastcat, sequence
|
||||
* @example
|
||||
@ -1474,86 +1415,6 @@ function _sequenceCount(x) {
|
||||
return [reify(x), 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Speeds a pattern up or down, to fit to the given metrical 'tactus'.
|
||||
* @example
|
||||
* s("bd sd cp").toTactus(4)
|
||||
* // The same as s("{bd sd cp}%4")
|
||||
*/
|
||||
export const toTactus = register('toTactus', function (targetTactus, pat) {
|
||||
return pat.fast(Fraction(targetTactus).div(pat.tactus));
|
||||
});
|
||||
|
||||
export function _polymeterListSteps(steps, ...args) {
|
||||
const seqs = args.map((a) => _sequenceCount(a));
|
||||
if (seqs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
if (steps == 0) {
|
||||
steps = seqs[0][1];
|
||||
}
|
||||
const pats = [];
|
||||
for (const seq of seqs) {
|
||||
if (seq[1] == 0) {
|
||||
continue;
|
||||
}
|
||||
if (steps == seq[1]) {
|
||||
pats.push(seq[0]);
|
||||
} else {
|
||||
pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1]))));
|
||||
}
|
||||
}
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
*
|
||||
* @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"
|
||||
* polymeterSteps(4, "c d", "e f g")
|
||||
*/
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
if (Array.isArray(args[0])) {
|
||||
// Support old behaviour
|
||||
return _polymeterListSteps(steps, ...args);
|
||||
}
|
||||
|
||||
return polymeter(...args).toTactus(steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used.
|
||||
* @synonyms pm
|
||||
* @example
|
||||
* // The same as "{c eb g, c2 g2}"
|
||||
* polymeter("c eb g", "c2 g2")
|
||||
*
|
||||
*/
|
||||
export function polymeter(...args) {
|
||||
if (Array.isArray(args[0])) {
|
||||
// Support old behaviour
|
||||
return _polymeterListSteps(0, ...args);
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
const tactus = args[0].tactus;
|
||||
const [head, ...tail] = args;
|
||||
|
||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
|
||||
result.tactus = tactus;
|
||||
return result;
|
||||
}
|
||||
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
@ -2069,11 +1930,16 @@ export const late = register(
|
||||
export const zoom = register('zoom', function (s, e, pat) {
|
||||
e = Fraction(e);
|
||||
s = Fraction(s);
|
||||
if (s.gte(e)) {
|
||||
return nothing;
|
||||
}
|
||||
const d = e.sub(s);
|
||||
const tactus = pat.tactus.mul(d);
|
||||
return pat
|
||||
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||
.splitQueries();
|
||||
.splitQueries()
|
||||
.setTactus(tactus);
|
||||
});
|
||||
|
||||
export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a, pat) {
|
||||
@ -2487,6 +2353,250 @@ Pattern.prototype.tag = function (tag) {
|
||||
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Tactus-related functions, i.e. ones that do stepwise
|
||||
// transformations
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given metrical 'tactus'.
|
||||
* @example
|
||||
* s("bd sd cp").toTactus(4)
|
||||
* // The same as s("{bd sd cp}%4")
|
||||
*/
|
||||
export const toTactus = register('toTactus', function (targetTactus, pat) {
|
||||
if (pat.tactus.eq(0)) {
|
||||
// avoid divide by zero..
|
||||
return nothing;
|
||||
}
|
||||
return pat.fast(Fraction(targetTactus).div(pat.tactus));
|
||||
});
|
||||
|
||||
export function _polymeterListSteps(steps, ...args) {
|
||||
const seqs = args.map((a) => _sequenceCount(a));
|
||||
if (seqs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
if (steps == 0) {
|
||||
steps = seqs[0][1];
|
||||
}
|
||||
const pats = [];
|
||||
for (const seq of seqs) {
|
||||
if (seq[1] == 0) {
|
||||
continue;
|
||||
}
|
||||
if (steps == seq[1]) {
|
||||
pats.push(seq[0]);
|
||||
} else {
|
||||
pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1]))));
|
||||
}
|
||||
}
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
*
|
||||
* @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"
|
||||
* polymeterSteps(4, "c d", "e f g")
|
||||
*/
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
if (Array.isArray(args[0])) {
|
||||
// Support old behaviour
|
||||
return _polymeterListSteps(steps, ...args);
|
||||
}
|
||||
|
||||
return polymeter(...args).toTactus(steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used.
|
||||
* @synonyms pm
|
||||
* @example
|
||||
* // The same as "{c eb g, c2 g2}"
|
||||
* polymeter("c eb g", "c2 g2")
|
||||
*
|
||||
*/
|
||||
export function polymeter(...args) {
|
||||
if (Array.isArray(args[0])) {
|
||||
// Support old behaviour
|
||||
return _polymeterListSteps(0, ...args);
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
const tactus = args[0].tactus;
|
||||
const [head, ...tail] = args;
|
||||
|
||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
|
||||
result.tactus = tactus;
|
||||
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.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* timecat([3,"e3"],[1, "g3"]).note()
|
||||
* // the same as "e3@3 g3".note()
|
||||
* @example
|
||||
* timecat("bd sd cp","hh hh").sound()
|
||||
* // the same as "bd sd cp hh hh".sound()
|
||||
*/
|
||||
export function timecat(...timepats) {
|
||||
const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]);
|
||||
timepats = timepats.map(findtactus);
|
||||
if (timepats.length == 1) {
|
||||
const result = reify(timepats[0][1]);
|
||||
result.tactus = timepats[0][0];
|
||||
return result;
|
||||
}
|
||||
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
let begin = Fraction(0);
|
||||
const pats = [];
|
||||
for (const [time, pat] of timepats) {
|
||||
if (Fraction(time).eq(0)) {
|
||||
continue;
|
||||
}
|
||||
const end = begin.add(time);
|
||||
pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
|
||||
begin = end;
|
||||
}
|
||||
const result = stack(...pats);
|
||||
result.tactus = total;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Deprecated alias for `timecat` */
|
||||
export const timeCat = timecat;
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'.
|
||||
* Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stepcat(["bd cp", "mt"], "bd").sound()
|
||||
*/
|
||||
export function stepcat(...groups) {
|
||||
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
|
||||
|
||||
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
|
||||
|
||||
let result = [];
|
||||
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.tactus > 0);
|
||||
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||
result = timecat(...result);
|
||||
result.tactus = tactus;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'.
|
||||
*/
|
||||
export const stepwax = register('stepwax', function (i, pat) {
|
||||
if (pat.tactus.lte(0)) {
|
||||
return nothing;
|
||||
}
|
||||
i = Fraction(i);
|
||||
if (i.eq(0)) {
|
||||
return nothing;
|
||||
}
|
||||
const flip = i < 0;
|
||||
if (flip) {
|
||||
i = i.abs();
|
||||
}
|
||||
const frac = i.div(pat.tactus);
|
||||
if (frac.lte(0)) {
|
||||
return nothing;
|
||||
}
|
||||
if (frac.gte(1)) {
|
||||
return pat;
|
||||
}
|
||||
if (flip) {
|
||||
return pat.zoom(Fraction(1).sub(frac), 1);
|
||||
}
|
||||
return pat.zoom(0, frac);
|
||||
});
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'.
|
||||
*/
|
||||
export const stepwane = register('stepwane', function (i, pat) {
|
||||
i = Fraction(i);
|
||||
if (i.lt(0)) {
|
||||
return pat.stepwax(Fraction(0).sub(pat.tactus.add(i)));
|
||||
}
|
||||
return pat.stepwax(pat.tactus.sub(i));
|
||||
});
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL*
|
||||
*/
|
||||
Pattern.prototype.taperlist = function (amount, times) {
|
||||
const pat = this;
|
||||
times = times - 1;
|
||||
|
||||
if (times === 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)));
|
||||
}
|
||||
list.push(pat);
|
||||
if (reverse) {
|
||||
list.reverse();
|
||||
}
|
||||
return list;
|
||||
};
|
||||
export const taperlist = (amount, times, pat) => pat.taperlist(amount, times);
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL*
|
||||
*/
|
||||
export const steptaper = register('steptaper', function (amount, times, pat) {
|
||||
const list = pat.taperlist(amount, times);
|
||||
const result = timecat(...list);
|
||||
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL*
|
||||
*/
|
||||
Pattern.prototype.steptour = function (...many) {
|
||||
return stepcat(
|
||||
...[].concat(
|
||||
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
|
||||
this,
|
||||
...many,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const steptour = function (pat, ...many) {
|
||||
return pat.steptour(...many);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Control-related functions, i.e. ones that manipulate patterns of
|
||||
// objects
|
||||
|
||||
@ -1138,4 +1138,28 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('steptaper', () => {
|
||||
it('can taper', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).steptaper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
|
||||
});
|
||||
it('can taper backwards', () => {
|
||||
expect(
|
||||
sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('wax and wane, left', () => {
|
||||
it('can wax from the left', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
|
||||
});
|
||||
it('can wane to the left', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
|
||||
});
|
||||
it('can wax from the right', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
|
||||
});
|
||||
it('can wane to the right', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4)));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user