Compare commits

...

11 Commits

Author SHA1 Message Date
alex 185abf1fc1 abandoned experiment 2023-01-21 09:12:41 +00:00
alex 37efdd9ce1 prettier 2023-01-16 21:55:16 +00:00
alex f4039d3666 Merge branch 'main' into composable 2023-01-16 21:47:46 +00:00
Alex McLean c0d6e36113 Merge branch 'main' into composable 2023-01-12 16:47:47 +00:00
Alex McLean 8261802078 more hackery, composing seems to work now 2023-01-12 16:36:42 +00:00
alex 6dda6bac66 more composable magic 2023-01-11 22:27:00 +00:00
alex d5ab1a3471 actually compose functions together 2023-01-11 21:59:10 +00:00
alex ded90733f6 mark up more function args 2023-01-11 20:30:07 +00:00
alex c64485db99 hack register to support function parameters that can be patterns which compose 2023-01-10 23:28:22 +00:00
alex afa5d6e704 prettier 2023-01-10 11:16:43 +00:00
alex bde7e79a38 failed attempt at composifying 2023-01-10 11:05:51 +00:00
3 changed files with 182 additions and 52 deletions
+11 -1
View File
@@ -820,13 +820,23 @@ const _setter = (func, name) =>
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
controls[name] = function (...pats) {
const result = _name(name, ...pats);
// Add a function for composing this control with another pattern
result.__as_function = function (pat) {
return pat[name](...pats);
};
return result;
};
Pattern.__registered.push(name);
Pattern.prototype[name] = _setter(controls[name], name);
});
// create custom param
controls.createParam = (name) => {
const func = (...pats) => _name(name, ...pats);
Pattern.__registered.push(name);
Pattern.prototype[name] = _setter(func, name);
return (...pats) => _name(name, ...pats);
};
+150 -51
View File
@@ -28,11 +28,24 @@ export class Pattern {
*
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
*/
constructor(query) {
constructor(query, as_function) {
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.__as_function = as_function;
}
// TODO - would a default 'as_function' be useful?
// /**
// * Accessor for pattern-as-function
// */
// get as_function() {
// if (!this.__as_function) {
// // TODO - add other alignments
// this.__as_function = x => x.set(this);
// }
// return this.__as_function;
// }
//////////////////////////////////////////////////////////////////////
// Haskell-style functor, applicative and monadic operations
@@ -56,6 +69,10 @@ export class Pattern {
return this.withValue(func);
}
applyValue(func) {
return this.withValue((x) => x(func));
}
/**
* Assumes 'this' is a pattern of functions, and given a function to
* resolve wholes, applies a given pattern of values to that
@@ -1345,11 +1362,11 @@ export const func = curry((a, b) => reify(b).func(a));
* @param {function} func function with 1 or more params, where last is the current pattern
*
*/
export function register(name, func) {
export function register(name, func, f_params = null) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
result[name_item] = register(name_item, func);
result[name_item] = register(name_item, func, f_params);
}
return result;
}
@@ -1373,9 +1390,32 @@ export function register(name, func) {
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
// Don't use applicative for arguments that a) have a '__as_function' function and b) are
// marked as being a higher order function parameter
function app(acc, p, i) {
if (f_params != null && f_params.length > i + 1 && f_params[i + 1] && '__as_function' in p) {
return acc.applyValue(p.__as_function);
}
return acc.appLeft(p);
}
var start;
// Do the same check for the first parameter.. a bit repetitive
if (f_params != null && f_params.length > 0 && f_params[0] && '__as_function' in left) {
start = pure(mapFn(left.__as_function));
} else {
start = left.fmap(mapFn);
}
return right.reduce(app, start).innerJoin();
};
if (!Pattern.__registered) {
Pattern.__registered = [];
}
Pattern.__registered.push(name);
Pattern.prototype[name] = function (...args) {
args = args.map(reify);
// For methods that take a single argument (plus 'this'), allow
@@ -1385,7 +1425,19 @@ export function register(name, func) {
} else if (arity !== args.length + 1) {
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
}
return pfunc(...args, this);
const result = pfunc(...args, this);
// speed(2,3).__as_function(pat) = pat.speed(2,3)
// and so
// speed(2,3).fast(2).__as_function(pat) = pat.speed(2,3).fast(2)
if ('__as_function' in this) {
const pata = this;
result.__as_function = function (patb) {
return pata.__as_function(patb)[name](...args);
};
}
return result;
};
if (arity > 1) {
@@ -1624,9 +1676,13 @@ export const { slow, sparsity } = register(['slow', 'sparsity'], function (facto
* "0 1 2 3 4 3 2 1".inside(4, rev).scale('C major').note()
* // "0 1 2 3 4 3 2 1".slow(4).rev().fast(4).scale('C major').note()
*/
export const inside = register('inside', function (factor, f, pat) {
return f(pat._slow(factor))._fast(factor);
});
export const inside = register(
'inside',
function (factor, f, pat) {
return f(pat._slow(factor))._fast(factor);
},
[false, true],
);
/**
* Carries out an operation 'outside' a cycle.
@@ -1634,9 +1690,13 @@ export const inside = register('inside', function (factor, f, pat) {
* "<[0 1] 2 [3 4] 5>".outside(4, rev).scale('C major').note()
* // "<[0 1] 2 [3 4] 5>".fast(4).rev().slow(4).scale('C major').note()
*/
export const outside = register('outside', function (factor, f, pat) {
return f(pat._fast(factor))._slow(factor);
});
export const outside = register(
'outside',
function (factor, f, pat) {
return f(pat._fast(factor))._slow(factor);
},
[false, true],
);
/**
* Applies the given function every n cycles, starting from the last cycle.
@@ -1648,11 +1708,16 @@ export const outside = register('outside', function (factor, f, pat) {
* @example
* note("c3 d3 e3 g3").lastOf(4, x=>x.rev())
*/
export const lastOf = register('lastOf', function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.push(func(pat));
return slowcatPrime(...pats);
});
export const lastOf = register(
'lastOf',
function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.push(func(pat));
return slowcatPrime(...pats);
},
// second parameter is a function
[false, true],
);
/**
* Applies the given function every n cycles, starting from the first cycle.
@@ -1675,11 +1740,16 @@ export const lastOf = register('lastOf', function (n, func, pat) {
* @example
* note("c3 d3 e3 g3").every(4, x=>x.rev())
*/
export const { firstOf, every } = register(['firstOf', 'every'], function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.unshift(func(pat));
return slowcatPrime(...pats);
});
export const { firstOf, every } = register(
['firstOf', 'every'],
function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.unshift(func(pat));
return slowcatPrime(...pats);
},
// second parameter is a function
[false, true],
);
/**
* Like layer, but with a single function:
@@ -1689,9 +1759,13 @@ export const { firstOf, every } = register(['firstOf', 'every'], function (n, fu
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
*/
// TODO: remove or dedupe with layer?
export const apply = register('apply', function (func, pat) {
return func(pat);
});
export const apply = register(
'apply',
function (func, pat) {
return func(pat);
},
[true],
);
/**
* Plays the pattern at the given cycles per minute.
@@ -1801,9 +1875,13 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
* @example
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
*/
export const when = register('when', function (on, func, pat) {
return on ? func(pat) : pat;
});
export const when = register(
'when',
function (on, func, pat) {
return on ? func(pat) : pat;
},
[false, true],
);
/**
* Superimposes the function result on top of the original pattern, delayed by the given time.
@@ -1815,9 +1893,13 @@ export const when = register('when', function (on, func, pat) {
* @example
* "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
*/
export const off = register('off', function (time_pat, func, pat) {
return stack(pat, func(pat.late(time_pat)));
});
export const off = register(
'off',
function (time_pat, func, pat) {
return stack(pat, func(pat.late(time_pat)));
},
[false, true],
);
/**
* Returns a new pattern where every other cycle is played once, twice as
@@ -1885,28 +1967,36 @@ export const palindrome = register('palindrome', function (pat) {
* @example
* s("lt ht mt ht hh").juxBy("<0 .5 1>/2", rev)
*/
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
by /= 2;
const elem_or = function (dict, key, dflt) {
if (key in dict) {
return dict[key];
}
return dflt;
};
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
const right = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }));
export const { juxBy, juxby } = register(
['juxBy', 'juxby'],
function (by, func, pat) {
by /= 2;
const elem_or = function (dict, key, dflt) {
if (key in dict) {
return dict[key];
}
return dflt;
};
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
const right = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }));
return stack(left, func(right));
});
return stack(left, func(right));
},
[false, true],
);
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @example
* s("lt ht mt ht hh").jux(rev)
*/
export const jux = register('jux', function (func, pat) {
return pat._juxBy(1, func, pat);
});
export const jux = register(
'jux',
function (func, pat) {
return pat._juxBy(1, func, pat);
},
[true],
);
/**
* Superimpose and offset multiple times, applying the given function each time.
@@ -1925,6 +2015,7 @@ export const { echoWith, echowith, stutWith, stutwith } = register(
function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
},
[false, false, true],
);
/**
@@ -2005,9 +2096,13 @@ const _chunk = function (n, func, pat, back = false) {
return pat.when(binary_pat, func);
};
export const chunk = register('chunk', function (n, func, pat) {
return _chunk(n, func, pat, false);
});
export const chunk = register(
'chunk',
function (n, func, pat) {
return _chunk(n, func, pat, false);
},
[false, true],
);
/**
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
@@ -2018,9 +2113,13 @@ export const chunk = register('chunk', function (n, func, pat) {
* @example
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
return _chunk(n, func, pat, true);
});
export const { chunkBack, chunkback } = register(
['chunkBack', 'chunkback'],
function (n, func, pat) {
return _chunk(n, func, pat, true);
},
[false, true],
);
// TODO - redefine elsewhere in terms of mask
export const bypass = register('bypass', function (on, pat) {
+21
View File
@@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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/>.
*/
import { Pattern } from './pattern.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
@@ -149,6 +151,25 @@ export function curry(func, overload, arity = func.length) {
const partial = function (...args2) {
return curried.apply(this, args.concat(args2));
};
if (args.length == arity - 1) {
// The penultimate arg.. so add some composition magic
// TODO - To make this useful, we also need to add stub functions for every pattern method to
// do the actual composing
for (const r of Pattern.__registered) {
partial[r] = function (...args) {
const result = new Pattern(() => []);
result.__compose = function (pat) {
return partial(pat)[r](...args);
};
return result;
};
}
partial.__compose = function (pat) {
return partial(pat);
};
}
if (overload) {
overload(partial, args);
}