diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 4e230240..c07045ff 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import { Pattern, sequence } from './pattern.mjs';
+import { Pattern, sequence, registerControl } from './pattern.mjs';
const controls = {};
const generic_params = [
@@ -828,26 +828,26 @@ const generic_params = [
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
-const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
-
-const _setter = (func, name) =>
- function (...pats) {
+const makeControl = function (name) {
+ const func = (...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
+ const setter = function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
}
return this.set(func(...pats));
};
+ Pattern.prototype[name] = setter;
+ registerControl(name, func);
+ return func;
+};
generic_params.forEach(([type, name, description]) => {
- controls[name] = (...pats) => _name(name, ...pats);
- Pattern.prototype[name] = _setter(controls[name], name);
+ controls[name] = makeControl(name);
});
// create custom param
controls.createParam = (name) => {
- const func = (...pats) => _name(name, ...pats);
- Pattern.prototype[name] = _setter(func, name);
- return (...pats) => _name(name, ...pats);
+ return makeControl(name);
};
controls.createParams = (...names) =>
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index b9c9e24c..1184e1a1 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -21,6 +21,141 @@ let stringParser;
// intended to use with mini to automatically interpret all strings as mini notation
export const setStringParser = (parser) => (stringParser = parser);
+const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero'];
+
+const methodRegistry = [];
+const getterRegistry = [];
+const controlRegistry = [];
+const controlSubscribers = [];
+const composifiedRegistry = [];
+
+//////////////////////////////////////////////////////////////////////
+// Magic for supporting higher order composition of method chains
+
+// Dresses the given (unary) function with methods for composition chaining, so e.g.
+// `fast(2).iter(4)` composes to pattern functions into a new one.
+function composify(func) {
+ if (!func.__composified) {
+ for (const [name, method] of methodRegistry) {
+ func[name] = method;
+ }
+ for (const [name, getter] of getterRegistry) {
+ Object.defineProperty(func, name, getter);
+ }
+ func.__composified = true;
+ composifiedRegistry.push(func);
+ } else {
+ console.log('Warning: attempt at composifying a function more than once');
+ }
+ return func;
+}
+
+export function registerMethod(name, addAlignments = false, addControls = false) {
+ if (addAlignments || addControls) {
+ // This method needs to make its 'this' object available to chained alignments and/or
+ // control parameters, so it has to be implemented as a getter
+ const getter = {
+ get: function () {
+ const func = this;
+ const wrapped = function (...args) {
+ const composed = (pat) => func(pat)[name](...args);
+ return composify(composed);
+ };
+
+ if (addAlignments) {
+ for (const alignment of alignments) {
+ wrapped[alignment] = function (...args) {
+ const composed = (pat) => func(pat)[name][alignment](...args);
+ return composify(composed);
+ };
+ for (const [controlname, controlfunc] of controlRegistry) {
+ wrapped[alignment][controlname] = function (...args) {
+ const composed = (pat) => func(pat)[name][alignment](controlfunc(...args));
+ return composify(composed);
+ };
+ }
+ }
+ }
+ if (addControls) {
+ for (const [controlname, controlfunc] of controlRegistry) {
+ wrapped[controlname] = function (...args) {
+ const composed = (pat) => func(pat)[name](controlfunc(...args));
+ return composify(composed);
+ };
+ }
+ }
+ return wrapped;
+ },
+ };
+
+ getterRegistry.push([name, getter]);
+
+ // Add to functions already 'composified'
+ for (const composified of composifiedRegistry) {
+ Object.defineProperty(composified, name, getter);
+ }
+ } else {
+ // No chained alignments/controls needed, so we can just add as a plain method,
+ // probably more efficient this way?
+ const method = function (...args) {
+ const func = this;
+ const composed = (pat) => func(pat)[name](...args);
+ return composify(composed);
+ };
+
+ methodRegistry.push([name, method]);
+
+ // Add to functions already 'composified'
+ for (const composified of composifiedRegistry) {
+ composified[name] = method;
+ }
+ }
+}
+
+export function registerControl(controlname, controlfunc) {
+ registerMethod(controlname);
+ controlRegistry.push([controlname, controlfunc]);
+ for (const subscriber of controlSubscribers) {
+ subscriber(controlname, controlfunc);
+ }
+}
+
+export function withControls(func) {
+ for (const [controlname, controlfunc] of controlRegistry) {
+ func(controlname, controlfunc);
+ }
+ controlSubscribers.push(func);
+}
+
+export function addToPrototype(name, func) {
+ Pattern.prototype[name] = func;
+ registerMethod(name);
+}
+
+export function curryPattern(func, arity = func.length) {
+ const fn = function curried(...args) {
+ if (args.length >= arity) {
+ return func.apply(this, args);
+ }
+
+ const partial = function (...args2) {
+ return curried.apply(this, args.concat(args2));
+ };
+ if (args.length == arity - 1) {
+ return composify(partial);
+ }
+
+ return partial;
+ };
+ if (arity == 1) {
+ composify(fn);
+ }
+ return fn;
+}
+
+//////////////////////////////////////////////////////////////////////
+// The core Pattern class
+
/** @class Class representing a pattern. */
export class Pattern {
/**
@@ -643,7 +778,9 @@ export class Pattern {
* @noAutocomplete
*/
get firstCycleValues() {
- return this.firstCycle().map((hap) => hap.value);
+ return this.sortHapsByPart()
+ .firstCycle()
+ .map((hap) => hap.value);
}
/**
@@ -693,7 +830,7 @@ export class Pattern {
const otherPat = reify(other);
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
}
- _opSqueezeOut(other, func) {
+ _opSqueezeout(other, func) {
const thisPat = this;
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
@@ -860,11 +997,11 @@ function groupHapsBy(eq, haps) {
const congruent = (a, b) => a.spanEquals(b);
// Pattern> -> Pattern>
// returned pattern contains arrays of congruent haps
-Pattern.prototype.collect = function () {
+addToPrototype('collect', function () {
return this.withHaps((haps) =>
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
);
-};
+});
/**
* Selects indices in in stacked notes.
@@ -872,12 +1009,12 @@ Pattern.prototype.collect = function () {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
-Pattern.prototype.arpWith = function (func) {
+addToPrototype('arpWith', function (func) {
return this.collect()
.fmap((v) => reify(func(v)))
.innerJoin()
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
-};
+});
/**
* Selects indices in in stacked notes.
@@ -885,9 +1022,9 @@ Pattern.prototype.arpWith = function (func) {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* */
-Pattern.prototype.arp = function (pat) {
+addToPrototype('arp', function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
-};
+});
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -985,15 +1122,15 @@ function _composeOp(a, b, func) {
func: [(a, b) => b(a)],
};
- const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
+ const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1));
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
// make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat
- Pattern.prototype['_' + what] = function (value) {
+ addToPrototype('_' + what, function (value) {
return this.fmap((x) => op(x, value));
- };
+ });
// make patternified monster version
Object.defineProperty(Pattern.prototype, what, {
@@ -1007,7 +1144,7 @@ function _composeOp(a, b, func) {
// add methods to that function for each behaviour
for (const how of hows) {
- wrapper[how.toLowerCase()] = function (...other) {
+ const howfunc = function (...other) {
var howpat = pat;
other = sequence(other);
if (preprocess) {
@@ -1025,19 +1162,41 @@ function _composeOp(a, b, func) {
}
return result;
};
+
+ for (const [controlname, controlfunc] of controlRegistry) {
+ howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
+ }
+ wrapper[how.toLowerCase()] = howfunc;
}
wrapper.squeezein = wrapper.squeeze;
+ for (const [controlname, controlfunc] of controlRegistry) {
+ wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args));
+ }
+
return wrapper;
},
});
- // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
- for (const how of hows) {
- Pattern.prototype[how.toLowerCase()] = function (...args) {
- return this.set[how.toLowerCase()](args);
- };
- }
+ registerMethod(what, true, true);
+ }
+
+ // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
+ for (const howLower of alignments) {
+ // Using a 'get'ted function so that all the controls are added
+ Object.defineProperty(Pattern.prototype, howLower, {
+ get: function () {
+ const pat = this;
+ const howfunc = function (...args) {
+ return pat.set[howLower](args);
+ };
+ for (const [controlname, controlfunc] of controlRegistry) {
+ howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
+ }
+ return howfunc;
+ },
+ });
+ registerMethod(howLower, false, true);
}
// binary composers
@@ -1049,36 +1208,36 @@ function _composeOp(a, b, func) {
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4)
*/
- Pattern.prototype.struct = function (...args) {
+ addToPrototype('struct', function (...args) {
return this.keepif.out(...args);
- };
- Pattern.prototype.structAll = function (...args) {
+ });
+ addToPrototype('structAll', function (...args) {
return this.keep.out(...args);
- };
+ });
/**
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
*/
- Pattern.prototype.mask = function (...args) {
+ addToPrototype('mask', function (...args) {
return this.keepif.in(...args);
- };
- Pattern.prototype.maskAll = function (...args) {
+ });
+ addToPrototype('maskAll', function (...args) {
return this.keep.in(...args);
- };
+ });
/**
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s(" sd, hh*4").reset("")
*/
- Pattern.prototype.reset = function (...args) {
+ addToPrototype('reset', function (...args) {
return this.keepif.trig(...args);
- };
- Pattern.prototype.resetAll = function (...args) {
+ });
+ addToPrototype('resetAll', function (...args) {
return this.keep.trig(...args);
- };
+ });
/**
* Restarts the pattern for each onset of the restart pattern.
* While reset will only reset the current cycle, restart will start from cycle 0.
@@ -1086,12 +1245,12 @@ function _composeOp(a, b, func) {
* @example
* s(" sd, hh*4").restart("")
*/
- Pattern.prototype.restart = function (...args) {
+ addToPrototype('restart', function (...args) {
return this.keepif.trigzero(...args);
- };
- Pattern.prototype.restartAll = function (...args) {
+ });
+ addToPrototype('restartAll', function (...args) {
return this.keep.trigzero(...args);
- };
+ });
})();
// aliases
@@ -1336,36 +1495,68 @@ export function pm(...args) {
polymeter(...args);
}
-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));
+export const mask = curryPattern((a, b) => reify(b).mask(a));
+export const struct = curryPattern((a, b) => reify(b).struct(a));
+export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a));
+
+const methodToFunction = function (name, addAlignments = false) {
+ const func = curryPattern((a, b) => reify(b)[name](a));
+
+ withControls((controlname, controlfunc) => {
+ func[controlname] = function (...pats) {
+ return func(controlfunc(...pats));
+ };
+ });
+
+ if (addAlignments) {
+ for (const alignment of alignments) {
+ func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a));
+ withControls((controlname, controlfunc) => {
+ func[alignment][controlname] = function (...pats) {
+ return func[alignment](controlfunc(...pats));
+ };
+ });
+ }
+ }
+
+ return func;
+};
// operators
-export const set = curry((a, b) => reify(b).set(a));
-export const keep = curry((a, b) => reify(b).keep(a));
-export const keepif = curry((a, b) => reify(b).keepif(a));
-export const add = curry((a, b) => reify(b).add(a));
-export const sub = curry((a, b) => reify(b).sub(a));
-export const mul = curry((a, b) => reify(b).mul(a));
-export const div = curry((a, b) => reify(b).div(a));
-export const mod = curry((a, b) => reify(b).mod(a));
-export const pow = curry((a, b) => reify(b).pow(a));
-export const band = curry((a, b) => reify(b).band(a));
-export const bor = curry((a, b) => reify(b).bor(a));
-export const bxor = curry((a, b) => reify(b).bxor(a));
-export const blshift = curry((a, b) => reify(b).blshift(a));
-export const brshift = curry((a, b) => reify(b).brshift(a));
-export const lt = curry((a, b) => reify(b).lt(a));
-export const gt = curry((a, b) => reify(b).gt(a));
-export const lte = curry((a, b) => reify(b).lte(a));
-export const gte = curry((a, b) => reify(b).gte(a));
-export const eq = curry((a, b) => reify(b).eq(a));
-export const eqt = curry((a, b) => reify(b).eqt(a));
-export const ne = curry((a, b) => reify(b).ne(a));
-export const net = curry((a, b) => reify(b).net(a));
-export const and = curry((a, b) => reify(b).and(a));
-export const or = curry((a, b) => reify(b).or(a));
-export const func = curry((a, b) => reify(b).func(a));
+export const set = methodToFunction('set', true);
+export const keep = methodToFunction('keep', true);
+export const keepif = methodToFunction('keepif', true);
+export const add = methodToFunction('add', true);
+export const sub = methodToFunction('sub', true);
+export const mul = methodToFunction('mul', true);
+export const div = methodToFunction('div', true);
+export const mod = methodToFunction('mod', true);
+export const pow = methodToFunction('pow', true);
+export const band = methodToFunction('band', true);
+export const bor = methodToFunction('bor', true);
+export const bxor = methodToFunction('bxor', true);
+export const blshift = methodToFunction('blshift', true);
+export const brshift = methodToFunction('brshift', true);
+export const lt = methodToFunction('lt', true);
+export const gt = methodToFunction('gt', true);
+export const lte = methodToFunction('lte', true);
+export const gte = methodToFunction('gte', true);
+export const eq = methodToFunction('eq', true);
+export const eqt = methodToFunction('eqt', true);
+export const ne = methodToFunction('ne', true);
+export const net = methodToFunction('net', true);
+export const and = methodToFunction('and', true);
+export const or = methodToFunction('or', true);
+export const func = methodToFunction('func', true);
+
+// alignments
+// export const in = methodToFunction('in'); // reserved word :(
+export const out = methodToFunction('out');
+export const mix = methodToFunction('mix');
+export const squeeze = methodToFunction('squeeze');
+export const squeezeout = methodToFunction('squeezeout');
+export const trig = methodToFunction('trig');
+export const trigzero = methodToFunction('trigzero');
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
@@ -1384,9 +1575,10 @@ export function register(name, func) {
return result;
}
const arity = func.length;
- var pfunc; // the patternified function
- pfunc = function (...args) {
+ registerMethod(name);
+
+ const pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
@@ -1402,8 +1594,12 @@ export function register(name, func) {
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
- mapFn = curry(mapFn, null, arity - 1);
- return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
+ mapFn = curryPattern(mapFn, arity - 1);
+
+ const app = (acc, p) => acc.appLeft(p);
+ const start = left.fmap(mapFn);
+
+ return right.reduce(app, start).innerJoin();
};
Pattern.prototype[name] = function (...args) {
@@ -1428,7 +1624,7 @@ export function register(name, func) {
// toplevel functions get curried as well as patternified
// because pfunc uses spread args, we need to state the arity explicitly!
- return curry(pfunc, null, arity);
+ return curryPattern(pfunc, arity);
}
//////////////////////////////////////////////////////////////////////
diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs
index 08638d18..2142242d 100644
--- a/packages/core/test/pattern.test.mjs
+++ b/packages/core/test/pattern.test.mjs
@@ -45,6 +45,8 @@ import {
rev,
time,
run,
+ hitch,
+ set,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -204,7 +206,7 @@ describe('Pattern', () => {
),
);
});
- it('can SqueezeOut() structure', () => {
+ it('can squeezeout() structure', () => {
sameFirst(
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
@@ -252,7 +254,7 @@ describe('Pattern', () => {
),
);
});
- it('can SqueezeOut() structure', () => {
+ it('can squeezeout() structure', () => {
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
});
});
@@ -294,7 +296,7 @@ describe('Pattern', () => {
),
);
});
- it('can SqueezeOut() structure', () => {
+ it('can squeezeout() structure', () => {
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
@@ -929,6 +931,14 @@ describe('Pattern', () => {
});
});
describe('alignments', () => {
+ it('Can combine controls', () => {
+ sameFirst(s('bd').set.in.n(3), s('bd').n(3));
+ sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4)));
+ });
+ it('Can combine functions with alignmed controls', () => {
+ sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3));
+ sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3));
+ });
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
});
@@ -959,4 +969,16 @@ describe('Pattern', () => {
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
});
});
+ describe('composable functions', () => {
+ it('Can compose functions', () => {
+ sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
+ });
+ it('Can compose by method chaining operators with controls', () => {
+ sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
+ });
+ it('Can compose by method chaining operators and alignments with controls', () => {
+ sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
+ // sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
+ });
+ });
});
diff --git a/packages/core/util.mjs b/packages/core/util.mjs
index 3127e0d1..6ba8f397 100644
--- a/packages/core/util.mjs
+++ b/packages/core/util.mjs
@@ -139,7 +139,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
-export const constant = (a, b) => a;
+export const constant = curry((a, b) => a);
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);