mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
11 Commits
docs
...
composable
| Author | SHA1 | Date | |
|---|---|---|---|
| 185abf1fc1 | |||
| 37efdd9ce1 | |||
| f4039d3666 | |||
| c0d6e36113 | |||
| 8261802078 | |||
| 6dda6bac66 | |||
| d5ab1a3471 | |||
| ded90733f6 | |||
| c64485db99 | |||
| afa5d6e704 | |||
| bde7e79a38 |
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user