Skip to content

Commit

Permalink
Feature: tactus marking (#1021)
Browse files Browse the repository at this point in the history
* rename `beat` option to `stackBy` to `repeat`
* fix parse error reporting
* rename `weight` to `tactus` (it might in the end be pulse, step, or tap)
* tactus marking with ^
* and add some tests
  • Loading branch information
yaxu authored Mar 27, 2024
1 parent 2e8ae38 commit 2fd2bdb
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 135 deletions.
121 changes: 59 additions & 62 deletions packages/core/pattern.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ export class Pattern {
* @param {function} query - The function that maps a `State` to an array of `Hap`.
* @noAutocomplete
*/
constructor(query, weight = undefined) {
constructor(query, tactus = undefined) {
this.query = query;
this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
this.__weight = weight; // in terms of number of beats per cycle
this.__tactus = tactus; // in terms of number of beats per cycle
}

get weight() {
return this.__weight ?? Fraction(1);
get tactus() {
return this.__tactus ?? Fraction(1);
}

set weight(weight) {
this.__weight = Fraction(weight);
set tactus(tactus) {
this.__tactus = Fraction(tactus);
}

setWeight(weight) {
this.weight = weight;
setTactus(tactus) {
this.tactus = tactus;
return this;
}

Expand All @@ -62,7 +62,7 @@ export class Pattern {
*/
withValue(func) {
const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
result.weight = this.weight;
result.tactus = this.tactus;
return result;
}

Expand Down Expand Up @@ -130,7 +130,7 @@ export class Pattern {
return span_a.intersection_e(span_b);
};
const result = pat_func.appWhole(whole_func, pat_val);
result.weight = lcm(pat_val.weight, pat_func.weight);
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
return result;
}

Expand Down Expand Up @@ -165,7 +165,7 @@ export class Pattern {
return haps;
};
const result = new Pattern(query);
result.weight = this.weight;
result.tactus = this.tactus;
return result;
}

Expand Down Expand Up @@ -198,7 +198,7 @@ export class Pattern {
return haps;
};
const result = new Pattern(query);
result.weight = pat_val.weight;
result.tactus = pat_val.tactus;
return result;
}

Expand Down Expand Up @@ -452,7 +452,7 @@ export class Pattern {
*/
withHaps(func) {
const result = new Pattern((state) => func(this.query(state), state));
result.weight = this.weight;
result.tactus = this.tactus;
return result;
}

Expand Down Expand Up @@ -1160,13 +1160,13 @@ Pattern.prototype.factories = {
// Elemental patterns

/**
* Does absolutely nothing, with a given metrical 'weight'
* Does absolutely nothing, but with a given metrical 'tactus'
* @name gap
* @param {number} weight
* @param {number} tactus
* @example
* gap(3) // "~@3"
*/
export const gap = (weight) => new Pattern(() => [], Fraction(weight));
export const gap = (tactus) => new Pattern(() => [], Fraction(tactus));

/**
* Does absolutely nothing..
Expand All @@ -1176,7 +1176,7 @@ export const gap = (weight) => new Pattern(() => [], Fraction(weight));
*/
export const silence = gap(1);

/* Like silence, but with a 'weight' (relative duration) of 0 */
/* Like silence, but with a 'tactus' (relative duration) of 0 */
export const nothing = gap(0);

/** A discrete value that repeats once per cycle.
Expand Down Expand Up @@ -1235,7 +1235,7 @@ export function stack(...pats) {
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
const result = new Pattern(query);
result.weight = lcm(...pats.map((pat) => pat.weight));
result.tactus = lcm(...pats.map((pat) => pat.tactus));
return result;
}

Expand All @@ -1247,54 +1247,54 @@ function _stackWith(func, pats) {
if (pats.length === 1) {
return pats[0];
}
const [left, ...right] = pats.map((pat) => pat.weight);
const weight = left.maximum(...right);
return stack(...func(weight, pats));
const [left, ...right] = pats.map((pat) => pat.tactus);
const tactus = left.maximum(...right);
return stack(...func(tactus, pats));
}

export function stackLeft(...pats) {
return _stackWith(
(weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(pat, gap(weight.sub(pat.weight))))),
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timeCat(pat, gap(tactus.sub(pat.tactus))))),
pats,
);
}

export function stackRight(...pats) {
return _stackWith(
(weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(gap(weight.sub(pat.weight)), pat))),
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timeCat(gap(tactus.sub(pat.tactus)), pat))),
pats,
);
}

export function stackCentre(...pats) {
return _stackWith(
(weight, pats) =>
(tactus, pats) =>
pats.map((pat) => {
if (pat.weight.eq(weight)) {
if (pat.tactus.eq(tactus)) {
return pat;
}
const g = gap(weight.sub(pat.weight).div(2));
const g = gap(tactus.sub(pat.tactus).div(2));
return timeCat(g, pat, g);
}),
pats,
);
}

export function stackBy(by, ...pats) {
const [left, ...right] = pats.map((pat) => pat.weight);
const weight = left.maximum(...right);
const [left, ...right] = pats.map((pat) => pat.tactus);
const tactus = left.maximum(...right);
const lookup = {
centre: stackCentre,
left: stackLeft,
right: stackRight,
expand: stack,
beat: (...args) => polymeterSteps(weight, ...args),
repeat: (...args) => polymeterSteps(tactus, ...args),
};
return by
.inhabit(lookup)
.fmap((func) => func(...pats))
.innerJoin()
.setWeight(weight);
.setTactus(tactus);
}

/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
Expand Down Expand Up @@ -1361,8 +1361,7 @@ export function cat(...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
* mininotation as the number of toplevel steps. The latter only works if the mininotation
* hasn't first been modified by another function.
* the pattern's 'tactus', generally inferred by the mininotation.
* @return {Pattern}
* @example
* timeCat([3,"e3"],[1, "g3"]).note()
Expand All @@ -1372,13 +1371,11 @@ export function cat(...pats) {
* // the same as "bd sd cp hh hh".sound()
*/
export function timeCat(...timepats) {
// Weights may either be provided explicitly in [weight, pattern] pairs, or
// where possible, inferred from the pattern.
const findWeight = (x) => (Array.isArray(x) ? x : [x.weight, x]);
timepats = timepats.map(findWeight);
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.weight = timepats[0][0];
result.tactus = timepats[0][0];
return result;
}

Expand All @@ -1391,7 +1388,7 @@ export function timeCat(...timepats) {
begin = end;
}
const result = stack(...pats);
result.weight = total;
result.tactus = total;
return result;
}

Expand All @@ -1416,7 +1413,7 @@ export function fastcat(...pats) {
let result = slowcat(...pats);
if (pats.length > 1) {
result = result._fast(pats.length);
result.weight = pats.length;
result.tactus = pats.length;
}
return result;
}
Expand All @@ -1437,10 +1434,10 @@ export function beatCat(...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.weight > 0);
const weight = result.reduce((a, b) => a.add(b.weight), Fraction(0));
result = result.filter((x) => x.tactus > 0);
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
result = timeCat(...result);
result.weight = weight;
result.tactus = tactus;
return result;
}

Expand Down Expand Up @@ -1474,13 +1471,13 @@ function _sequenceCount(x) {
}

/**
* Speeds a pattern up or down, to fit to the given metrical 'weight'.
* Speeds a pattern up or down, to fit to the given metrical 'tactus'.
* @example
* s("bd sd cp").reweight(4)
* s("bd sd cp").toTactus(4)
* // The same as s("{bd sd cp}%4")
*/
export const reweight = register('reweight', function (targetWeight, pat) {
return pat.fast(Fraction(targetWeight).div(pat.weight));
export const toTactus = register('toTactus', function (targetTactus, pat) {
return pat.fast(Fraction(targetTactus).div(pat.tactus));
});

export function _polymeterListSteps(steps, ...args) {
Expand Down Expand Up @@ -1525,7 +1522,7 @@ export function polymeterSteps(steps, ...args) {
return _polymeterListSteps(steps, ...args);
}

return polymeter(...args).reweight(steps);
return polymeter(...args).toTactus(steps);
}

/**
Expand All @@ -1545,11 +1542,11 @@ export function polymeter(...args) {
if (args.length == 0) {
return silence;
}
const weight = args[0].weight;
const tactus = args[0].tactus;
const [head, ...tail] = args;

const result = stack(head, ...tail.map((pat) => pat._slow(pat.weight.div(weight))));
result.weight = weight;
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
result.tactus = tactus;
return result;
}

Expand Down Expand Up @@ -1592,7 +1589,7 @@ export const func = curry((a, b) => reify(b).func(a));
* @noAutocomplete
*
*/
export function register(name, func, patternify = true, preserveWeight = false) {
export function register(name, func, patternify = true, preserveTactus = false) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
Expand Down Expand Up @@ -1632,17 +1629,17 @@ export function register(name, func, patternify = true, preserveWeight = false)
result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
}
}
if (preserveWeight) {
result.weight = pat.weight;
if (preserveTactus) {
result.tactus = pat.tactus;
}
return result;
};
} else {
pfunc = function (...args) {
args = args.map(reify);
const result = func(...args);
if (preserveWeight) {
result.weight = args[args.length - 1].weight;
if (preserveTactus) {
result.tactus = args[args.length - 1].tactus;
}
return result;
};
Expand Down Expand Up @@ -1880,7 +1877,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
*/
export const ply = register('ply', function (factor, pat) {
const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
result.weight = pat.weight.mul(factor);
result.tactus = pat.tactus.mul(factor);
return result;
});

Expand All @@ -1902,7 +1899,7 @@ export const { fast, density } = register(['fast', 'density'], function (factor,
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
const result = fastQuery.withHapTime((t) => t.div(factor));
result.weight = factor.mul(pat.weight);
result.tactus = factor.mul(pat.tactus);
return result;
});

Expand Down Expand Up @@ -2106,7 +2103,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)).setWeight(rate);
return pat.struct(pure(true)._fast(rate)).setTactus(rate);
});

/**
Expand Down Expand Up @@ -2264,7 +2261,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).setWeight(lcm(left.weight, right.weight));
return stack(left, right).setTactus(lcm(left.tactus, right.tactus));
});

/**
Expand Down Expand Up @@ -2510,7 +2507,7 @@ export const chop = register('chop', function (n, pat) {
const func = function (o) {
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
};
return pat.squeezeBind(func).setWeight(pat.weight.mul(n));
return pat.squeezeBind(func).setTactus(pat.tactus.mul(n));
});

/**
Expand Down Expand Up @@ -2574,7 +2571,7 @@ export const slice = register(
}),
),
)
.setWeight(ipat.weight);
.setTactus(ipat.tactus);
},
false, // turns off auto-patternification
);
Expand Down Expand Up @@ -2605,7 +2602,7 @@ export const splice = register(
...v,
})),
);
}).setWeight(ipat.weight);
}).setTactus(ipat.tactus);
},
false, // turns off auto-patternification
);
Expand Down
12 changes: 6 additions & 6 deletions packages/core/test/controls.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ 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 weight of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).weight).toEqual(Fraction(3));
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 weight of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(4));
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('combines weight of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(12));
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));
});
});
Loading

0 comments on commit 2fd2bdb

Please sign in to comment.