Compare commits

..

6 Commits

Author SHA1 Message Date
Alex McLean 7bcd5897da format 2025-10-13 15:17:46 +01:00
Aria a7adebe036 Let pos or neg run until sign switches 2025-10-11 23:25:56 -05:00
Aria 62379269f0 Polarity approach 2025-10-11 15:12:10 -05:00
Aria 1162a42218 One more example 2025-10-11 01:28:31 -05:00
Aria 32d11d0332 Move timeline back to pattern file 2025-10-11 01:11:05 -05:00
Aria e5eb3b8d7b First pass at timelines 2025-10-10 18:47:33 -05:00
4 changed files with 96 additions and 19 deletions
+72 -17
View File
@@ -8,6 +8,7 @@ import TimeSpan from './timespan.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { getTime } from './time.mjs';
import { unionWithObj } from './value.mjs';
import {
@@ -963,13 +964,26 @@ export const arp = register(
false,
);
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) {
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});
*/
/*
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
* @name weaveWith
* @memberof Pattern
*/
export const weaveWith = register('weaveWith', function (t, funcs, pat) {
addToPrototype('weaveWith', function (t, ...funcs) {
const pat = this;
const l = funcs.length;
t = Fraction(t);
if (l == 0) {
@@ -977,21 +991,7 @@ export const weaveWith = register('weaveWith', function (t, funcs, pat) {
}
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, [s("bd(3,8)"), s("~ sd")])
* @example n("0 1 2 3 4 5 6 7").weave(8, [s("bd(3,8)"), s("~ sd")])
*/
export const weave = register('weave', function (t, pats, pat) {
return pat.weaveWith(
t,
pats.map((x) => (y) => y.set.out(x)),
);
});
*/
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -3534,3 +3534,58 @@ export const morph = (frompat, topat, bypat) => {
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
export const TIMELINES = {
state: {},
polarities: {},
};
/**
* Aligns the pattern with the specified `timeline` by ID.
*
* More specifically, if a timeline ID is encountered for the first time, that moment
* in time is marked as the "start" of that pattern. Thereafter, any other patterns aligned
* with that same id will begin at that same moment in time.
*
* If the sign of the id changes (positive to negative or vice versa), we reset the timeline.
*
* @param {number | Pattern} id Timeline id. Must be a non-zero number. Switch signs to reset.
* @returns Pattern
* @example
* s("tri").seg(8).n(irand(12)).scale("G#:minor").lpf(400).room(2)
* .timeline("<1 -1 1 2>")
* @example
* $: n("[0 .. 6]/4").scale("F:minor")
* // Execute the following line at any point in the session
* // and it will always start on note 0
* // $: n("[0 .. 6]/4").scale("F:minor").timeline(2)
*/
export const timeline = register('timeline', (id, pat) => {
if (typeof id !== 'number' || id === 0) {
logger(
`[query] ${id} is not a valid timeline id. Please ensure it is a non-zero number. Defaulting to timeline 1.`,
);
id = 1;
}
const { state, polarities } = TIMELINES;
const key = Math.abs(id);
// We let the pattern run until the sign switches
const p = id > 0 ? 1 : -1;
let t = polarities[key] === p ? state[key] : Math.ceil(getTime());
return (
pat
.late(t)
.onTrigger((hap) => {
const T = Number(hap.part.begin);
// Set state on the first trigger
state[key] ??= T;
polarities[key] ??= p;
if (polarities[key] !== p) {
state[key] = T;
}
polarities[key] = p;
}, false)
// Add labels
.withValue((v) => ({ ...v, timeline: id, offset: t }))
);
});
+6 -1
View File
@@ -4,7 +4,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { register, Pattern, isPattern, silence, stack, TIMELINES } from './pattern.mjs';
export function repl({
defaultOutput,
@@ -52,6 +52,11 @@ export function repl({
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
if (!started) {
// Reset timeline state
TIMELINES.state = {};
TIMELINES.polarities = {};
}
},
setInterval,
clearInterval,
+17
View File
@@ -10844,6 +10844,23 @@ exports[`runs examples > example "take" example index 2 1`] = `
]
`;
exports[`runs examples > example "timeline" example index 0 1`] = `[]`;
exports[`runs examples > example "timeline" example index 1 1`] = `
[
"[ 0/1 → 4/7 | note:F3 ]",
"[ (4/7 → 1/1) ⇝ 8/7 | note:G3 ]",
"[ 4/7 ⇜ (1/1 → 8/7) | note:G3 ]",
"[ 8/7 → 12/7 | note:Ab3 ]",
"[ (12/7 → 2/1) ⇝ 16/7 | note:Bb3 ]",
"[ 12/7 ⇜ (2/1 → 16/7) | note:Bb3 ]",
"[ 16/7 → 20/7 | note:C4 ]",
"[ (20/7 → 3/1) ⇝ 24/7 | note:Db4 ]",
"[ 20/7 ⇜ (3/1 → 24/7) | note:Db4 ]",
"[ 24/7 → 4/1 | note:Eb4 ]",
]
`;
exports[`runs examples > example "tour" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:e s:folkharp ]",
+1 -1
View File
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
<JsDoc client:idle name="Pattern.scrub" h={0} />
### speed