diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 4d23daf5..7df34671 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -204,7 +204,7 @@ const generic_params = [ * "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>") * */ - ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'], + // ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'], // ['f', 'clhatdecay', ''], /** * bit crusher effect. @@ -562,7 +562,7 @@ const generic_params = [ // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare ['f', 'semitone', ''], // TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano - ['f', 'velocity', ''], + // ['f', 'velocity', ''], ['f', 'voice', ''], // TODO: synth param /** * Sets the level of reverb. diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 254e1d7f..9ba55e7e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -637,109 +637,7 @@ export class Pattern { // magically transformed to accept patterns for all their arguments. ////////////////////////////////////////////////////////////////////// - // Numerical transformations - - /** - * Assumes a numerical pattern. Returns a new pattern with all values rounded - * to the nearest integer. - * @name round - * @memberof Pattern - * @returns Pattern - * @example - * "0.5 1.5 2.5".round().scale('C major').note() - */ - round() { - return this.asNumber().fmap((v) => Math.round(v)); - } - - /** - * Assumes a numerical pattern. Returns a new pattern with all values set to - * their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2` - * replaced with `-5`. - * @returns Pattern - */ - floor() { - return this.asNumber().fmap((v) => Math.floor(v)); - } - - /** - * Assumes a numerical pattern. Returns a new pattern with all values set to - * their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2` - * replaced with `-4`. - * @returns Pattern - */ - ceil() { - return this.asNumber().fmap((v) => Math.ceil(v)); - } - - /** - * Assumes a numerical pattern, containing unipolar values in the range 0 .. - * 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1 - * @returns Pattern - */ - toBipolar() { - return this.fmap((x) => x * 2 - 1); - } - - /** - * Assumes a numerical pattern, containing bipolar values in the range -1 .. - * 1. Returns a new pattern with values scaled to the unipolar range 0 .. 1 - * @returns Pattern - */ - fromBipolar() { - return this.fmap((x) => (x + 1) / 2); - } - - /** - * Assumes a numerical pattern, containing unipolar values in the range 0 .. 1. - * Returns a new pattern with values scaled to the given min/max range. - * Most useful in combination with continuous patterns. - * @name range - * @memberof Pattern - * @returns Pattern - * @example - * s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4)) - */ - _range(min, max) { - return this.mul(max - min).add(min); - } - - /** - * Assumes a numerical pattern, containing unipolar values in the range 0 .. - * 1. Returns a new pattern with values scaled to the given min/max range, - * following an exponential curve. - * @param {Number} min - * @param {Number} max - * @returns Pattern - */ - _rangex(min, max) { - return this._range(Math.log(min), Math.log(max)).fmap(Math.exp); - } - - /** - * Assumes a numerical pattern, containing bipolar values in the range -1 .. - * 1. Returns a new pattern with values scaled to the given min/max range. - * @param {Number} min - * @param {Number} max - * @returns Pattern - */ - _range2(min, max) { - return this.fromBipolar()._range(min, max); - } - - ////////////////////////////////////////////////////////////////////// - // Structural and temporal transformations - - /** - * Like layer, but with a single function: - * @name _apply - * @memberof Pattern - * @example - * "".scale('C minor').apply(scaleTranspose("0,2,4")).note() - */ - _apply(func) { - return func(this); - } + // Methods without corresponding toplevel functions /** * Layers the result of the given function(s). Like {@link superimpose}, but without the original pattern: @@ -755,318 +653,22 @@ export class Pattern { return stack(...funcs.map((func) => func(this))); } - _ply(factor) { - return this.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); - } - - _fastGap(factor) { - // Maybe it's better without this fallback.. - // if (factor < 1) { - // // there is no gap.. so maybe revert to _fast? - // return this._fast(factor) - // } - // A bit fiddly, to drop zero-width queries at the start of the next cycle - const qf = function (span) { - const cycle = span.begin.sam(); - const bpos = span.begin.sub(cycle).mul(factor).min(1); - const epos = span.end.sub(cycle).mul(factor).min(1); - if (bpos >= 1) { - return undefined; - } - return new TimeSpan(cycle.add(bpos), cycle.add(epos)); - }; - // Also fiddly, to maintain the right 'whole' relative to the part - const ef = function (hap) { - const begin = hap.part.begin; - const end = hap.part.end; - const cycle = begin.sam(); - const beginPos = begin.sub(cycle).div(factor).min(1); - const endPos = end.sub(cycle).div(factor).min(1); - const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos)); - const newWhole = !hap.whole - ? undefined - : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); - return new Hap(newWhole, newPart, hap.value, hap.context); - }; - return this.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); - } - - // Compress each cycle into the given timespan, leaving a gap - _compress(b, e) { - if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) { - return silence; - } - return this._fastGap(Fraction(1).div(e.sub(b)))._late(b); - } - - _compressSpan(span) { - return this._compress(span.begin, span.end); - } - - // Similar to compress, but doesn't leave gaps, and the 'focus' can be - // bigger than a cycle - _focus(b, e) { - return this._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos()); - } - - _focusSpan(span) { - return this._focus(span.begin, span.end); - } - /** - * Speed up a pattern by the given factor. Used by "*" in mini notation. - * - * @name fast - * @memberof Pattern - * @param {number | Pattern} factor speed up factor - * @returns Pattern - * @example - * s(" hh").fast(2) // s("[ hh]*2") - */ - _fast(factor) { - factor = Fraction(factor); - const fastQuery = this.withQueryTime((t) => t.mul(factor)); - return fastQuery.withHapTime((t) => t.div(factor)); - } - - /** - * Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation. - * - * @name slow - * @memberof Pattern - * @param {number | Pattern} factor slow down factor - * @returns Pattern - * @example - * s(" hh").slow(2) // s("[ hh]/2") - */ - _slow(factor) { - return this._fast(Fraction(1).div(factor)); - } - - _inside(factor, f) { - return f(this._slow(factor))._fast(factor); - } - - _outside(factor, f) { - return f(this._fast(factor))._slow(factor); - } - - // cpm = cycles per minute - _cpm(cpm) { - return this._fast(cpm / 60); - } - - /** - * Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator - * - * @name early - * @memberof Pattern - * @param {number | Pattern} cycles number of cycles to nudge left - * @returns Pattern - * @example - * "bd ~".stack("hh ~".early(.1)).s() - */ - _early(offset) { - offset = Fraction(offset); - return this.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset)); - } - - /** - * Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator - * - * @name late - * @memberof Pattern - * @param {number | Pattern} cycles number of cycles to nudge right - * @returns Pattern - * @example - * "bd ~".stack("hh ~".late(.1)).s() - */ - _late(offset) { - offset = Fraction(offset); - return this._early(Fraction(0).sub(offset)); - } - - _zoom(s, e) { - e = Fraction(e); - s = Fraction(s); - const d = e.sub(s); - return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) - .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) - .splitQueries(); - } - - _zoomArc(a) { - return this.zoom(a.begin, a.end); - } - - _linger(t) { - if (t == 0) { - return silence; - } else if (t < 0) { - return this._zoom(t.add(1), 1)._slow(t); - } - return this._zoom(0, t)._slow(t); - } - - _segment(rate) { - return this.struct(pure(true)._fast(rate)); - } - - invert() { - // Swap true/false in a binary pattern - return this.fmap((x) => !x); - } - - inv() { - // alias for invert() - return this.invert(); - } - - /** - * Applies the given function whenever the given pattern is in a true state. - * @name when - * @memberof Pattern - * @param {Pattern} binary_pat - * @param {function} func - * @returns Pattern - * @example - * "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note() - */ - when(binary_pat, func) { - //binary_pat = sequence(binary_pat) - const true_pat = binary_pat.filterValues(id); - const false_pat = binary_pat.filterValues((val) => !val); - const with_pat = true_pat.fmap(() => (y) => y).appRight(func(this)); - const without_pat = false_pat.fmap(() => (y) => y).appRight(this); - return stack(with_pat, without_pat); - } - - /** - * Superimposes the function result on top of the original pattern, delayed by the given time. - * @name off - * @memberof Pattern - * @param {Pattern | number} time offset time - * @param {function} func function to apply - * @returns Pattern - * @example - * "c3 eb3 g3".off(1/8, x=>x.add(7)).note() - */ - off(time_pat, func) { - return stack(this, func(this.late(time_pat))); - } - - /** - * Applies the given function every n cycles, starting from the first cycle. - * @name firstOf - * @memberof Pattern - * @param {number} n how many cycles - * @param {function} func function to apply - * @returns Pattern - * @example - * note("c3 d3 e3 g3").firstOf(4, x=>x.rev()) - */ - // TODO - patternify - firstOf(n, func) { - const pat = this; - const pats = Array(n - 1).fill(pat); - pats.unshift(func(pat)); - return slowcatPrime(...pats); - } - - /** - * Applies the given function every n cycles, starting from the last cycle. - * @name lastOf - * @memberof Pattern - * @param {number} n how many cycles - * @param {function} func function to apply - * @returns Pattern - * @example - * note("c3 d3 e3 g3").lastOf(4, x=>x.rev()) - */ - lastOf(n, func) { - const pat = this; - const pats = Array(n - 1).fill(pat); - pats.push(func(pat)); - return slowcatPrime(...pats); - } - - /** - * An alias for {@link firstOf} - * @name every - * @memberof Pattern - * @param {number} n how many cycles - * @param {function} func function to apply - * @returns Pattern - * @example - * note("c3 d3 e3 g3").every(4, x=>x.rev()) - */ - every(n, func) { - return this.firstOf(n, func); - } - - /** - * Returns a new pattern where every other cycle is played once, twice as - * fast, and offset in time by one quarter of a cycle. Creates a kind of - * breakbeat feel. - * @returns Pattern - */ - brak() { - return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25)); - } - - /** - * Reverse all haps in a pattern - * - * @name rev + * Superimposes the result of the given function(s) on top of the original pattern: + * @name superimpose * @memberof Pattern * @returns Pattern * @example - * note("c3 d3 e3 g3").rev() + * "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4" + * .superimpose(x=>x.add(2)) + * .scale('C minor').note() */ - rev() { - const pat = this; - const query = function (state) { - const span = state.span; - const cycle = span.begin.sam(); - const next_cycle = span.begin.nextSam(); - const reflect = function (to_reflect) { - const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time))); - // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work - const tmp = reflected.begin; - reflected.begin = reflected.end; - reflected.end = tmp; - return reflected; - }; - const haps = pat.query(state.setSpan(reflect(span))); - return haps.map((hap) => hap.withSpan(reflect)); - }; - return new Pattern(query).splitQueries(); + superimpose(...funcs) { + return this.stack(...funcs.map((func) => func(this))); } - palindrome() { - return this.every(2, rev); - } - - juxBy(by, func) { - by /= 2; - const elem_or = function (dict, key, dflt) { - if (key in dict) { - return dict[key]; - } - return dflt; - }; - const left = this.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); - const right = this.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by })); - - return stack(left, func(right)); - } - - _jux(func) { - return this.juxBy(1, func); - } + ////////////////////////////////////////////////////////////////////// + // Multi-pattern functions /** * Stacks the given pattern(s) to the current pattern. @@ -1119,189 +721,9 @@ export class Pattern { return slowcat(this, ...pats); } - /** - * Superimposes the result of the given function(s) on top of the original pattern: - * @name superimpose - * @memberof Pattern - * @returns Pattern - * @example - * "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4" - * .superimpose(x=>x.add(2)) - * .scale('C minor').note() - */ - superimpose(...funcs) { - return this.stack(...funcs.map((func) => func(this))); - } - - stutWith(times, time, func) { - return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i))); - } - - stut(times, feedback, time) { - return this.stutWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i))); - } - - /** - * Superimpose and offset multiple times, applying the given function each time. - * @name echoWith - * @memberof Pattern - * @returns Pattern - * @param {number} times how many times to repeat - * @param {number} time cycle offset between iterations - * @param {function} func function to apply, given the pattern and the iteration index - * @example - * "<0 [2 4]>" - * .echoWith(4, 1/8, (p,n) => p.add(n*2)) - * .scale('C minor').note().legato(.2) - */ - _echoWith(times, time, func) { - return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i))); - } - - /** - * Superimpose and offset multiple times, gradually decreasing the velocity - * @name echo - * @memberof Pattern - * @returns Pattern - * @param {number} times how many times to repeat - * @param {number} time cycle offset between iterations - * @param {number} feedback velocity multiplicator for each iteration - * @example - * s("bd sd").echo(3, 1/6, .8) - */ - _echo(times, time, feedback) { - return this._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i))); - } - - /** - * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. - * @name iter - * @memberof Pattern - * @returns Pattern - * @example - * note("0 1 2 3".scale('A minor')).iter(4) - */ - _iter(times, back = false) { - times = Fraction(times); - return slowcat( - ...listRange(0, times.sub(1)).map((i) => - back ? this.late(Fraction(i).div(times)) : this.early(Fraction(i).div(times)), - ), - ); - } - - /** - * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles - * @name iterBack - * @memberof Pattern - * @returns Pattern - * @example - * note("0 1 2 3".scale('A minor')).iterBack(4) - */ - _iterBack(times) { - return this._iter(times, true); - } - - /** - * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). - * @name chunk - * @memberof Pattern - * @returns Pattern - * @example - * "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note() - */ - _chunk(n, func, back = false) { - const binary = Array(n - 1).fill(false); - binary.unshift(true); - const binary_pat = sequence(...binary)._iter(n, back); - return this.when(binary_pat, func); - } - - /** - * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles - * @name chunkBack - * @memberof Pattern - * @returns Pattern - * @example - * "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note() - */ - _chunkBack(n, func) { - return this._chunk(n, func, true); - } - - _bypass(on) { - on = Boolean(parseInt(on)); - return on ? silence : this; - } - - ////////////////////////////////////////////////////////////////////// - // Control-related methods, which manipulate patterns of objects - - /** - * Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'. - * It turns a pattern of samples into a pattern of parts of samples. - * @name chop - * @memberof Pattern - * @returns Pattern - * @example - * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) - * s("rhodes") - * .chop(4) - * .rev() // reverse order of chops - * .loopAt(4,1) // fit sample into 4 cycles - * - */ - _chop(n) { - const slices = Array.from({ length: n }, (x, i) => i); - const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); - const func = function (o) { - return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); - }; - return this.squeezeBind(func); - } - - _striate(n) { - const slices = Array.from({ length: n }, (x, i) => i); - const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); - const slicePat = slowcat(...slice_objects); - return this.set(slicePat)._fast(n); - } - - /** - * Makes the sample fit the given number of cycles by changing the speed. - * @name loopAt - * @memberof Pattern - * @returns Pattern - * @example - * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) - * s("rhodes").loopAt(4,1) - */ - _loopAt(factor, cps = 1) { - return this.speed((1 / factor) * cps) - .unit('c') - .slow(factor); - } - ////////////////////////////////////////////////////////////////////// // Context methods - ones that deal with metadata - _color(color) { - return this.withContext((context) => ({ ...context, color })); - } - - /** - * - * Sets the velocity from 0 to 1. Is multiplied together with gain. - * @name velocity - * @example - * s("hh*8") - * .gain(".4!2 1 .4!2 1 .4 1") - * .velocity(".4 1") - */ - _velocity(velocity) { - return this.withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity })); - } - onTrigger(onTrigger, dominant = true) { return this.withHap((hap) => hap.setContext({ @@ -1333,32 +755,6 @@ export class Pattern { console.log(drawLine(this)); return this; } - - ////////////////////////////////////////////////////////////////////// - // Misc. - - hush() { - return silence; - } - - // sets absolute duration of haps - // TODO - fix - _duration(value) { - return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value))); - } - - /** - * - * Multiplies the hap duration with the given factor. - * @name legato - * @memberof Pattern - * @example - * note("c3 eb3 g3 c4").legato("<.25 .5 1 2>") - */ - // TODO - fix - _legato(value) { - return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value)))); - } } ////////////////////////////////////////////////////////////////////// @@ -1475,11 +871,11 @@ function _composeOp(a, b, func) { div: [numeralArgs((a, b) => a / b)], mod: [numeralArgs(_mod)], pow: [numeralArgs(Math.pow)], - _and: [numeralArgs((a, b) => a & b)], - _or: [numeralArgs((a, b) => a | b)], - _xor: [numeralArgs((a, b) => a ^ b)], - _lshift: [numeralArgs((a, b) => a << b)], - _rshift: [numeralArgs((a, b) => a >> b)], + band: [numeralArgs((a, b) => a & b)], + bor: [numeralArgs((a, b) => a | b)], + bxor: [numeralArgs((a, b) => a ^ b)], + blshift: [numeralArgs((a, b) => a << b)], + brshift: [numeralArgs((a, b) => a >> b)], // TODO - force numerical comparison if both look like numbers? lt: [(a, b) => a < b], @@ -1501,10 +897,17 @@ function _composeOp(a, b, func) { // 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) { + return this.fmap((x) => op(x, value)); + }; + + // make patternified monster version Object.defineProperty(Pattern.prototype, what, { // a getter that returns a function, so 'pat' can be // accessed by closures that are methods of that function.. - get: function() { + get: function () { const pat = this; // wrap the 'in' function as default behaviour @@ -1529,17 +932,19 @@ function _composeOp(a, b, func) { result = howpat['_op' + how](other, (a) => (b) => _composeOp(a, b, op)); } return result; - } + }; } - wrapper.squeezein = wrapper.squeeze + wrapper.squeezein = wrapper.squeeze; 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) }; + Pattern.prototype[how.toLowerCase()] = function (...args) { + return this.set[how.toLowerCase()](args); + }; } } @@ -1555,38 +960,32 @@ function _composeOp(a, b, func) { * .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~") * .slow(4) */ - Pattern.prototype.struct = function(...args) { return this.keepif.out(...args) } - Pattern.prototype.structAll = function(...args) { return this.keep.out(...args) } - Pattern.prototype.mask = function(...args) { return this.keepif.in(...args) } - Pattern.prototype.maskAll = function(...args) { return this.keep.in(...args) } - Pattern.prototype.reset = function(...args) { return this.keepif.trig(...args) } - Pattern.prototype.resetAll = function(...args) { return this.keep.trig(...args) } - Pattern.prototype.restart = function(...args) { return this.keepif.trigzero(...args) } - Pattern.prototype.restartAll = function(...args) { return this.keep.trigzero(...args) } + Pattern.prototype.struct = function (...args) { + return this.keepif.out(...args); + }; + Pattern.prototype.structAll = function (...args) { + return this.keep.out(...args); + }; + Pattern.prototype.mask = function (...args) { + return this.keepif.in(...args); + }; + Pattern.prototype.maskAll = function (...args) { + return this.keep.in(...args); + }; + Pattern.prototype.reset = function (...args) { + return this.keepif.trig(...args); + }; + Pattern.prototype.resetAll = function (...args) { + return this.keep.trig(...args); + }; + Pattern.prototype.restart = function (...args) { + return this.keepif.trigzero(...args); + }; + Pattern.prototype.restartAll = function (...args) { + return this.keep.trigzero(...args); + }; })(); -// methods of Pattern that get callable factories -Pattern.prototype.patternified = [ - 'apply', - 'chop', - 'color', - 'cpm', - 'duration', - 'early', - 'fast', - 'iter', - 'iterBack', - 'jux', - 'late', - 'legato', - 'linger', - 'ply', - 'segment', - 'striate', - 'slow', - 'velocity', -]; - // aliases export const polyrhythm = stack; export const pr = stack; @@ -1812,65 +1211,722 @@ export function pm(...args) { polymeter(...args); } -export const chop = curry((a, pat) => pat.chop(a)); -export const chunk = curry((a, pat) => pat.chunk(a)); -export const chunkBack = curry((a, pat) => pat.chunkBack(a)); -export const early = curry((a, pat) => pat.early(a)); -export const echo = curry((a, b, c, pat) => pat.echo(a, b, c)); -export const every = curry((i, f, pat) => pat.every(i, f)); -export const fast = curry((a, pat) => pat.fast(a)); -export const inv = (pat) => pat.inv(); -export const invert = (pat) => pat.invert(); -export const iter = curry((a, pat) => pat.iter(a)); -export const iterBack = curry((a, pat) => pat.iterBack(a)); -export const jux = curry((f, pat) => pat.jux(f)); -export const juxBy = curry((by, f, pat) => pat.juxBy(by, f)); -export const late = curry((a, pat) => pat.late(a)); -export const linger = curry((a, pat) => pat.linger(a)); -export const mask = curry((a, pat) => pat.mask(a)); -export const off = curry((t, f, pat) => pat.off(t, f)); -export const ply = curry((a, pat) => pat.ply(a)); -export const range = curry((a, b, pat) => pat.range(a, b)); -export const rangex = curry((a, b, pat) => pat.rangex(a, b)); -export const range2 = curry((a, b, pat) => pat.range2(a, b)); -export const rev = (pat) => pat.rev(); -export const slow = curry((a, pat) => pat.slow(a)); -export const struct = curry((a, pat) => pat.struct(a)); -export const superimpose = curry((array, pat) => pat.superimpose(...array)); -export const when = curry((binary, f, pat) => pat.when(binary, f)); +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)); -// operators -export const set = curry((a, pat) => pat.set(a)); -export const keep = curry((a, pat) => pat.keep(a)); -export const keepif = curry((a, pat) => pat.keepif(a)); -export const add = curry((a, pat) => pat.add(a)); -export const sub = curry((a, pat) => pat.sub(a)); -export const mul = curry((a, pat) => pat.mul(a)); -export const div = curry((a, pat) => pat.div(a)); -//export const mod = curry((a, pat) => pat.mod(a)); -export const pow = curry((a, pat) => pat.pow(a)); -export const _and = curry((a, pat) => pat._and(a)); -export const _or = curry((a, pat) => pat._or(a)); -export const _xor = curry((a, pat) => pat._xor(a)); -export const _lshift = curry((a, pat) => pat._lshift(a)); -export const _rshift = curry((a, pat) => pat._rshift(a)); -export const lt = curry((a, pat) => pat.lt(a)); -export const gt = curry((a, pat) => pat.gt(a)); -export const lte = curry((a, pat) => pat.lte(a)); -export const gte = curry((a, pat) => pat.gte(a)); -export const eq = curry((a, pat) => pat.eq(a)); -export const eqt = curry((a, pat) => pat.eqt(a)); -export const ne = curry((a, pat) => pat.ne(a)); -export const net = curry((a, pat) => pat.net(a)); -export const and = curry((a, pat) => pat.and(a)); -export const or = curry((a, pat) => pat.or(a)); -export const func = curry((a, pat) => pat.func(a)); +// 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 function register(name, func) { + if (Array.isArray(name)) { + const result = {}; + for (const name_item of name) { + result[name_item] = register(name_item, func); + } + return result; + } + const arity = func.length; + var pfunc; // the patternified function + + pfunc = function (...args) { + args = args.map(reify); + const pat = args[args.length - 1]; + if (arity === 1) { + return func(pat); + } + const [left, ...right] = args.slice(0, -1); + let mapFn = (...args) => { + // make sure to call func with the correct argument count + // args.length is expected to be <= arity-1 + // so we set undefined args explicitly undefined + Array(arity - 1) + .fill() + .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(); + }; + + Pattern.prototype[name] = function (...args) { + args = args.map(reify); + // For methods that take a single argument (plus 'this'), allow + // multiple arguments but sequence them + if (arity == 2 && args.length != 1) { + args = [sequence(...args)]; + } else if (arity != args.length + 1) { + throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`); + } + return pfunc(...args, this); + }; + + if (arity > 1) { + // There are patternified args, so lets make an unpatternified + // version, prefixed by '_' + Pattern.prototype['_' + name] = function (...args) { + return func(...args, this); + }; + } + + // 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); +} + +////////////////////////////////////////////////////////////////////// +// Numerical transformations + +/** + * Assumes a numerical pattern. Returns a new pattern with all values rounded + * to the nearest integer. + * @name round + * @memberof Pattern + * @returns Pattern + * @example + * "0.5 1.5 2.5".round().scale('C major').note() + */ +export const round = register('round', function (pat) { + return pat.asNumber().fmap((v) => Math.round(v)); +}); + +/** + * Assumes a numerical pattern. Returns a new pattern with all values set to + * their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2` + * replaced with `-5`. + * @returns Pattern + */ +export const floor = register('floor', function (pat) { + return pat.asNumber().fmap((v) => Math.floor(v)); +}); + +/** + * Assumes a numerical pattern. Returns a new pattern with all values set to + * their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2` + * replaced with `-4`. + * @returns Pattern + */ +export const ceil = register('ceil', function (pat) { + return pat.asNumber().fmap((v) => Math.ceil(v)); +}); +/** + * Assumes a numerical pattern, containing unipolar values in the range 0 .. + * 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1 + * @returns Pattern + */ +export const toBipolar = register('toBipolar', function (pat) { + return pat.fmap((x) => x * 2 - 1); +}); + +/** + * Assumes a numerical pattern, containing bipolar values in the range -1 .. + * 1. Returns a new pattern with values scaled to the unipolar range 0 .. 1 + * @returns Pattern + */ +export const fromBipolar = register('fromBipolar', function (pat) { + return pat.fmap((x) => (x + 1) / 2); +}); + +/** + * Assumes a numerical pattern, containing unipolar values in the range 0 .. 1. + * Returns a new pattern with values scaled to the given min/max range. + * Most useful in combination with continuous patterns. + * @name range + * @memberof Pattern + * @returns Pattern + * @example + * s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4)) + */ +export const range = register('range', function (min, max, pat) { + return pat.mul(max - min).add(min); +}); + +/** + * Assumes a numerical pattern, containing unipolar values in the range 0 .. + * 1. Returns a new pattern with values scaled to the given min/max range, + * following an exponential curve. + * @param {Number} min + * @param {Number} max + * @returns Pattern + */ +export const rangex = register('rangex', function (min, max, pat) { + return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp); +}); + +/** + * Assumes a numerical pattern, containing bipolar values in the range -1 .. + * 1. Returns a new pattern with values scaled to the given min/max range. + * @param {Number} min + * @param {Number} max + * @returns Pattern + */ +export const range2 = register('range2', function (min, max, pat) { + return pat.fromBipolar()._range(min, max); +}); + +////////////////////////////////////////////////////////////////////// +// Structural and temporal transformations + +// Compress each cycle into the given timespan, leaving a gap +export const compress = register('compress', function (b, e, pat) { + if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) { + return silence; + } + return pat._fastGap(Fraction(1).div(e.sub(b)))._late(b); +}); + +export const { compressSpan, compressspan } = register(['compressSpan', 'compressspan'], function (span, pat) { + return pat._compress(span.begin, span.end); +}); + +export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (factor, pat) { + // A bit fiddly, to drop zero-width queries at the start of the next cycle + const qf = function (span) { + const cycle = span.begin.sam(); + const bpos = span.begin.sub(cycle).mul(factor).min(1); + const epos = span.end.sub(cycle).mul(factor).min(1); + if (bpos >= 1) { + return undefined; + } + return new TimeSpan(cycle.add(bpos), cycle.add(epos)); + }; + // Also fiddly, to maintain the right 'whole' relative to the part + const ef = function (hap) { + const begin = hap.part.begin; + const end = hap.part.end; + const cycle = begin.sam(); + const beginPos = begin.sub(cycle).div(factor).min(1); + const endPos = end.sub(cycle).div(factor).min(1); + const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos)); + const newWhole = !hap.whole + ? undefined + : new TimeSpan( + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); + return new Hap(newWhole, newPart, hap.value, hap.context); + }; + return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); +}); + +// Similar to compress, but doesn't leave gaps, and the 'focus' can be +// bigger than a cycle + +export const focus = register('focus', function (b, e, pat) { + return pat._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos()); +}); + +export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], function (span, pat) { + return pat._focus(span.begin, span.end); +}); + +export const ply = register('ply', function (factor, pat) { + return pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); +}); + +/** + * Speed up a pattern by the given factor. Used by "*" in mini notation. + * + * @name fast + * @memberof Pattern + * @param {number | Pattern} factor speed up factor + * @returns Pattern + * @example + * s(" hh").fast(2) // s("[ hh]*2") + */ +export const { fast, density } = register(['fast', 'density'], function (factor, pat) { + factor = Fraction(factor); + const fastQuery = pat.withQueryTime((t) => t.mul(factor)); + return fastQuery.withHapTime((t) => t.div(factor)); +}); + +/** + * Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation. + * + * @name slow + * @memberof Pattern + * @param {number | Pattern} factor slow down factor + * @returns Pattern + * @example + * s(" hh").slow(2) // s("[ hh]/2") + */ +export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) { + return pat._fast(Fraction(1).div(factor)); +}); + +// Should these really be in alphabetical order? a shame to split +// fast/slow, inside/outside.. +export const inside = register('inside', function (factor, f, pat) { + return f(pat._slow(factor))._fast(factor); +}); + +export const outside = register('outside', function (factor, f, pat) { + return f(pat._fast(factor))._slow(factor); +}); + +/** + * Applies the given function every n cycles, starting from the last cycle. + * @name lastOf + * @memberof Pattern + * @param {number} n how many cycles + * @param {function} func function to apply + * @returns Pattern + * @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); +}); + +/** + * Applies the given function every n cycles, starting from the first cycle. + * @name firstOf + * @memberof Pattern + * @param {number} n how many cycles + * @param {function} func function to apply + * @returns Pattern + * @example + * note("c3 d3 e3 g3").firstOf(4, x=>x.rev()) + */ + +/** + * An alias for {@link firstOf} + * @name every + * @memberof Pattern + * @param {number} n how many cycles + * @param {function} func function to apply + * @returns Pattern + * @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); +}); + +/** + * Like layer, but with a single function: + * @name _apply + * @memberof Pattern + * @example + * "".scale('C minor').apply(scaleTranspose("0,2,4")).note() + */ +export const apply = register('apply', function (func, pat) { + return func(pat); +}); + +// cpm = cycles per minute +// TODO - global clock +export const cpm = register('cpm', function (cpm, pat) { + return pat._fast(cpm / 60); +}); + +/** + * Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator + * + * @name early + * @memberof Pattern + * @param {number | Pattern} cycles number of cycles to nudge left + * @returns Pattern + * @example + * "bd ~".stack("hh ~".early(.1)).s() + */ +export const early = register('early', function (offset, pat) { + offset = Fraction(offset); + return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset)); +}); + +/** + * Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator + * + * @name late + * @memberof Pattern + * @param {number | Pattern} cycles number of cycles to nudge right + * @returns Pattern + * @example + * "bd ~".stack("hh ~".late(.1)).s() + */ +export const late = register('late', function (offset, pat) { + offset = Fraction(offset); + return pat._early(Fraction(0).sub(offset)); +}); + +export const zoom = register('zoom', function (s, e, pat) { + e = Fraction(e); + s = Fraction(s); + const d = e.sub(s); + return pat + .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) + .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) + .splitQueries(); +}); + +export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a, pat) { + return pat.zoom(a.begin, a.end); +}); + +export const linger = register('linger', function (t, pat) { + if (t == 0) { + return silence; + } else if (t < 0) { + return pat._zoom(t.add(1), 1)._slow(t); + } + return pat._zoom(0, t)._slow(t); +}); + +export const segment = register('segment', function (rate, pat) { + return pat.struct(pure(true)._fast(rate)); +}); + +export const { invert, inv } = register(['invert', 'inv'], function (pat) { + // Swap true/false in a binary pattern + return pat.fmap((x) => !x); +}); + +/** + * Applies the given function whenever the given pattern is in a true state. + * @name when + * @memberof Pattern + * @param {Pattern} binary_pat + * @param {function} func + * @returns Pattern + * @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; +}); + +/** + * Superimposes the function result on top of the original pattern, delayed by the given time. + * @name off + * @memberof Pattern + * @param {Pattern | number} time offset time + * @param {function} func function to apply + * @returns Pattern + * @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))); +}); + +/** + * Returns a new pattern where every other cycle is played once, twice as + * fast, and offset in time by one quarter of a cycle. Creates a kind of + * breakbeat feel. + * @returns Pattern + */ +export const brak = register('brak', function (pat) { + return pat.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25)); +}); + +/** + * Reverse all haps in a pattern + * + * @name rev + * @memberof Pattern + * @returns Pattern + * @example + * note("c3 d3 e3 g3").rev() + */ +export const rev = register('rev', function (pat) { + const query = function (state) { + const span = state.span; + const cycle = span.begin.sam(); + const next_cycle = span.begin.nextSam(); + const reflect = function (to_reflect) { + const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time))); + // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work + const tmp = reflected.begin; + reflected.begin = reflected.end; + reflected.end = tmp; + return reflected; + }; + const haps = pat.query(state.setSpan(reflect(span))); + return haps.map((hap) => hap.withSpan(reflect)); + }; + return new Pattern(query).splitQueries(); +}); + +export const hush = register('hush', function (pat) { + return silence; +}); + +export const palindrome = register('palindrome', function (pat) { + return pat.every(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 })); + + return stack(left, func(right)); +}); + +export const jux = register('jux', function (func, pat) { + return pat._juxBy(1, func, pat); +}); + +export const { stutWith, stutwith } = register(['stutWith', 'stutwith'], function (times, time, func, pat) { + return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i))); +}); + +export const stut = register('stut', function (times, feedback, time, pat) { + return pat._stutWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i))); +}); + +/** + * Superimpose and offset multiple times, applying the given function each time. + * @name echoWith + * @memberof Pattern + * @returns Pattern + * @param {number} times how many times to repeat + * @param {number} time cycle offset between iterations + * @param {function} func function to apply, given the pattern and the iteration index + * @example + * "<0 [2 4]>" + * .echoWith(4, 1/8, (p,n) => p.add(n*2)) + * .scale('C minor').note().legato(.2) + */ +export const { echoWith, echowith } = register(['echoWith', 'echowith'], function (times, time, func, pat) { + return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i))); +}); + +/** + * Superimpose and offset multiple times, gradually decreasing the velocity + * @name echo + * @memberof Pattern + * @returns Pattern + * @param {number} times how many times to repeat + * @param {number} time cycle offset between iterations + * @param {number} feedback velocity multiplicator for each iteration + * @example + * s("bd sd").echo(3, 1/6, .8) + */ +export const echo = register('echo', function (times, time, feedback, pat) { + return pat._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i))); +}); + +/** + * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. + * @name iter + * @memberof Pattern + * @returns Pattern + * @example + * note("0 1 2 3".scale('A minor')).iter(4) + */ + +const _iter = function (times, pat, back = false) { + times = Fraction(times); + return slowcat( + ...listRange(0, times.sub(1)).map((i) => + back ? pat.late(Fraction(i).div(times)) : pat.early(Fraction(i).div(times)), + ), + ); +}; + +export const iter = register('iter', function (times, pat) { + return _iter(times, pat, false); +}); + +/** + * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles + * @name iterBack + * @memberof Pattern + * @returns Pattern + * @example + * note("0 1 2 3".scale('A minor')).iterBack(4) + */ +export const { iterBack, iterback } = register(['iterBack', 'iterback'], function (times, pat) { + return _iter(times, pat, true); +}); + +/** + * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). + * @name chunk + * @memberof Pattern + * @returns Pattern + * @example + * "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note() + */ +const _chunk = function (n, func, pat, back = false) { + const binary = Array(n - 1).fill(false); + binary.unshift(true); + const binary_pat = _iter(n, sequence(...binary), back); + return pat.when(binary_pat, func); +}; + +export const chunk = register('chunk', function (n, func, pat) { + return _chunk(n, func, pat, false); +}); + +/** + * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles + * @name chunkBack + * @memberof Pattern + * @returns Pattern + * @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); +}); + +// TODO - redefine elsewhere in terms of mask +export const bypass = register('bypass', function (on, pat) { + on = Boolean(parseInt(on)); + return on ? silence : this; +}); + +// sets absolute duration of haps +// TODO - fix +export const duration = register('duration', function (value, pat) { + return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value))); +}); + +// TODO - make control? +export const { color, colour } = register(['color', 'colour'], function (color, pat) { + return pat.withContext((context) => ({ ...context, color })); +}); + +/** + * + * Sets the velocity from 0 to 1. Is multiplied together with gain. + * @name velocity + * @example + * s("hh*8") + * .gain(".4!2 1 .4!2 1 .4 1") + * .velocity(".4 1") + */ +export const velocity = register('velocity', function (velocity, pat) { + return pat.withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity })); +}); + +/** + * + * Multiplies the hap duration with the given factor. + * @name legato + * @memberof Pattern + * @example + * note("c3 eb3 g3 c4").legato("<.25 .5 1 2>") + */ +// TODO - fix +export const legato = register('legato', function (value, pat) { + return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value)))); +}); + +////////////////////////////////////////////////////////////////////// +// Control-related functions, i.e. ones that manipulate patterns of +// objects + +/** + * Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'. + * It turns a pattern of samples into a pattern of parts of samples. + * @name chop + * @memberof Pattern + * @returns Pattern + * @example + * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) + * s("rhodes") + * .chop(4) + * .rev() // reverse order of chops + * .loopAt(4) // fit sample into 4 cycles + * + */ +export const chop = register('chop', function (n, pat) { + const slices = Array.from({ length: n }, (x, i) => i); + const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); + const func = function (o) { + return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); + }; + return pat.squeezeBind(func); +}); + +export const striate = register('striate', function (n, pat) { + const slices = Array.from({ length: n }, (x, i) => i); + const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); + const slicePat = slowcat(...slice_objects); + return pat.set(slicePat)._fast(n); +}); + +/** + * Makes the sample fit the given number of cycles by changing the speed. + * @name loopAt + * @memberof Pattern + * @returns Pattern + * @example + * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) + * s("rhodes").loopAt(4) + */ +// TODO - global cps clock +const _loopAt = function (factor, pat, cps = 1) { + return pat + .speed((1 / factor) * cps) + .unit('c') + .slow(factor); +}; + +const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) { + return _loopAt(factor, pat, 1); +}); + +/** + * Makes the sample fit the given number of cycles and cps value, by + * changing the speed. Please note that at some point cps will be + * given by a global clock and this function will be + * deprecated/removed. + * @name loopAtCps + * @memberof Pattern + * @returns Pattern + * @example + * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) + * s("rhodes").loopAtCps(4,1.5).cps(1.5) + */ +// TODO - global cps clock +const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) { + return _loopAt(factor, pat, cps); +}); // problem: curried functions with spread arguments must have pat at the beginning // with this, we cannot keep the pattern open at the end.. solution for now: use array to keep using pat as last arg // these are the core composable functions. they are extended with Pattern.prototype.define below -Pattern.prototype.composable = { fast, slow, early, late, superimpose }; +Pattern.prototype.composable = { early, late, superimpose }; // adds Pattern.prototype.composable to given function as child functions // then you can do transpose(2).late(0.2) instead of x => x.transpose(2).late(0.2) @@ -1888,11 +1944,14 @@ export function makeComposable(func) { return func; } -export const patternify2 = (f) => (pata, patb, pat) => - pata - .fmap((a) => (b) => f.call(pat, a, b)) - .appLeft(patb) - .innerJoin(); +export const patternify = (f) => (pata, pat) => pata.fmap((a) => f.call(pat, a)).innerJoin(); +export const patternify2 = (f) => + function (pata, patb, pat) { + return pata + .fmap((a) => (b) => f.call(pat, a, b)) + .appLeft(patb) + .innerJoin(); + }; export const patternify3 = (f) => (pata, patb, patc, pat) => pata .fmap((a) => (b) => (c) => f.call(pat, a, b, c)) @@ -1907,58 +1966,10 @@ export const patternify4 = (f) => (pata, patb, patc, patd, pat) => .appLeft(patd) .innerJoin(); -Pattern.prototype.echo = function (...args) { - args = args.map(reify); - return patternify3(Pattern.prototype._echo)(...args, this); -}; -Pattern.prototype.echoWith = function (...args) { - args = args.map(reify); - return patternify3(Pattern.prototype._echoWith)(...args, this); -}; -Pattern.prototype.chunk = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._chunk)(...args, this); -}; -Pattern.prototype.chunkBack = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._chunkBack)(...args, this); -}; -Pattern.prototype.loopAt = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._loopAt)(...args, this); -}; -Pattern.prototype.zoom = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._zoom)(...args, this); -}; -Pattern.prototype.compress = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._compress)(...args, this); -}; -Pattern.prototype.outside = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._outside)(...args, this); -}; -Pattern.prototype.inside = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._inside)(...args, this); -}; -Pattern.prototype.range = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._range)(...args, this); -}; -Pattern.prototype.rangex = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._rangex)(...args, this); -}; -Pattern.prototype.range2 = function (...args) { - args = args.map(reify); - return patternify2(Pattern.prototype._range2)(...args, this); -}; +Pattern.prototype.patternified = []; // call this after all Pattern.prototype.define calls have been executed! (right before evaluate) Pattern.prototype.bootstrap = function () { - // makeComposable(Pattern.prototype); const bootstrapped = Object.fromEntries( Object.entries(Pattern.prototype.composable).map(([functionName, composable]) => { if (Pattern.prototype[functionName]) { @@ -1968,53 +1979,12 @@ Pattern.prototype.bootstrap = function () { return [functionName, curry(composable, makeComposable)]; }), ); - // note: this === Pattern.prototype + this.patternified.forEach((prop) => { // the following will patternify all functions in Pattern.prototype.patternified Pattern.prototype[prop] = function (...args) { return this.patternify((x) => x.innerJoin(), Pattern.prototype['_' + prop])(...args); }; - - /* - const func = Pattern.prototype['_' + prop]; - Pattern.prototype[prop] = function (...args) { - return this.patternify(x => x.innerJoin(), func); - }; - - Object.defineProperty(Pattern.prototype, prop, { - // a getter that returns a function, so 'pat' can be - // accessed by closures that are methods of that function.. - get: function() { - const pat = this; - // wrap the default behaviour - const wrapper = pat.patternify(x => x.innerJoin(), func); - - // add the variants - wrapper['in'] = pat.patternify(x => x.innerJoin(), func); - wrapper['out'] = pat.patternify(x => x.outerJoin(), func); - wrapper['trig'] = pat.patternify(x => x.trigJoin(), func); - wrapper['trigzero'] = pat.patternify(x => x.trigzeroJoin(), func); - wrapper['squeeze'] = pat.patternify(x => x.squeezeJoin(), func); - - return wrapper; - } - }); - */ - - // with the following, you can do, e.g. `stack(c3).fast.slowcat(1, 2, 4, 8)` instead of `stack(c3).fast(slowcat(1, 2, 4, 8))` - // TODO: find a way to implement below outside of constructor (code only worked there) - /* Object.assign( - Pattern.prototype[prop], - Object.fromEntries( - Object.entries(Pattern.prototype.factories).map(([type, func]) => [ - type, - function(...args) { - console.log('this', this); - return this[prop](func(...args)) - } - ]) - ) - ); */ }); return bootstrapped; }; @@ -2028,9 +1998,6 @@ Pattern.prototype.define = (name, func, options = {}) => { if (options.patternified) { Pattern.prototype.patternified = Pattern.prototype.patternified.concat([name]); } + Pattern.prototype.bootstrap(); // automatically bootstrap after new definition }; - -// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true }); -Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true }); -Pattern.prototype.define('bypass', (pat) => pat.bypass(1), { patternified: true, composable: true }); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index ba5a3aa3..589c18e3 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -16,6 +16,7 @@ import { pure, stack, fastcat, + firstOf, slowcat, cat, sequence, @@ -154,6 +155,12 @@ describe('Pattern', () => { }); }); describe('add()', () => { + it('works as toplevel function', () => { + expect(add(pure(4), pure(5)).query(st(0, 1))[0].value).toBe(9); + }); + it('works as toplevel function, with bare values for arguments', () => { + expect(add(4, 5).query(st(0, 1))[0].value).toBe(9); + }); it('can structure In()', () => { expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7); expect(pure(3).add.in(pure(4)).query(st(0, 1))[0].value).toBe(7); @@ -406,8 +413,8 @@ describe('Pattern', () => { ); }); it('defaults to accepting sequences', () => { - expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual( - sequence(1, 2, 3).fast(1.5, 2).firstCycle(), + expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual( + sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(), ); }); it('works as a static function', () => { @@ -586,6 +593,16 @@ describe('Pattern', () => { .firstCycle(), ).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle()); }); + it('Works as a toplevel function', () => { + expect(firstOf(3, fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual( + sequence(sequence('a', 'a'), 'a', 'a').firstCycle(), + ); + }); + it('Works as a toplevel function, with a patterned first argument', () => { + expect(firstOf(pure(3), fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual( + sequence(sequence('a', 'a'), 'a', 'a').firstCycle(), + ); + }); it('works with currying', () => { expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual( sequence(sequence('a', 'a'), 'a', 'a').firstCycle(), @@ -895,9 +912,7 @@ describe('Pattern', () => { }); describe('alignments', () => { it('Can squeeze arguments', () => { - expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual( - sequence(5, 6, 6, 7).firstCycle() - ); + expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle()); }); }); }); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 12f2276b..b1997977 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -116,9 +116,9 @@ export const constant = (a, b) => a; export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min); -export function curry(func, overload) { +export function curry(func, overload, arity = func.length) { const fn = function curried(...args) { - if (args.length >= func.length) { + if (args.length >= arity) { return func.apply(this, args); } else { const partial = function (...args2) { diff --git a/repl/src/test/__snapshots__/tunes.test.mjs.snap b/repl/src/test/__snapshots__/tunes.test.mjs.snap index 46a40e42..b21a3a7f 100644 --- a/repl/src/test/__snapshots__/tunes.test.mjs.snap +++ b/repl/src/test/__snapshots__/tunes.test.mjs.snap @@ -1414,9 +1414,9 @@ exports[`renders tunes > tune: csoundDemo 1`] = ` "[ -1/4 ⇜ (0/1 → 1/4) | note:Bb3 ]", "[ (1/4 → 1/1) ⇝ 5/4 | note:F3 ]", "[ 0/1 → 1/2 | note:F4 ]", - "[ (1/2 → 1/1) ⇝ 3/2 | note:C4 ]", "[ -1/4 ⇜ (0/1 → 1/4) | note:A4 ]", "[ (1/4 → 1/2) ⇝ 3/4 | note:A4 ]", + "[ (1/2 → 1/1) ⇝ 3/2 | note:C4 ]", "[ 1/4 ⇜ (1/2 → 3/4) | note:A4 ]", "[ (3/4 → 1/1) ⇝ 7/4 | note:E4 ]", ] @@ -1456,24 +1456,24 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ 0/1 → 1/2 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (1/4 → 5/8) ⇝ 3/4 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 0/1 → 1/4 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (1/2 → 3/4) ⇝ 1/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (1/4 → 3/8) ⇝ 1/2 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (1/2 → 3/4) ⇝ 1/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (3/4 → 7/8) ⇝ 5/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ -1/8 ⇜ (0/1 → 1/16) ⇝ 1/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ -1/8 ⇜ (0/1 → 1/16) ⇝ 1/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ (1/8 → 9/16) ⇝ 5/8 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (3/8 → 11/16) ⇝ 7/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 1/8 → 3/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (5/8 → 13/16) ⇝ 9/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (3/8 → 1/2) ⇝ 5/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (5/8 → 13/16) ⇝ 9/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (7/8 → 15/16) ⇝ 11/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (0/1 → 1/8) ⇝ 1/4 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ (0/1 → 1/8) ⇝ 1/4 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ (1/4 → 5/8) ⇝ 3/4 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (1/2 → 3/4) ⇝ 1/1 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 1/4 → 1/2 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (3/4 → 7/8) ⇝ 5/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (1/2 → 5/8) ⇝ 3/4 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (3/4 → 7/8) ⇝ 5/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ -1/8 ⇜ (0/1 → 1/8) | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", "[ (1/8 → 1/4) ⇝ 3/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ -1/8 ⇜ (0/1 → 1/8) | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", @@ -1481,40 +1481,40 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (3/8 → 11/16) ⇝ 7/8 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (5/8 → 13/16) ⇝ 9/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 3/8 → 5/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (7/8 → 15/16) ⇝ 11/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (5/8 → 3/4) ⇝ 7/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (7/8 → 15/16) ⇝ 11/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 1/1 → 3/2 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (5/4 → 13/8) ⇝ 7/4 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (3/2 → 7/4) ⇝ 2/1 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 3/4 ⇜ (1/1 → 5/4) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (3/2 → 7/4) ⇝ 2/1 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (7/4 → 15/8) ⇝ 9/4 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 5/8 ⇜ (1/1 → 17/16) ⇝ 9/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 7/8 ⇜ (1/1 → 17/16) ⇝ 11/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (9/8 → 25/16) ⇝ 13/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (11/8 → 27/16) ⇝ 15/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (13/8 → 29/16) ⇝ 17/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 7/8 ⇜ (9/8 → 11/8) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (13/8 → 29/16) ⇝ 17/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (15/8 → 31/16) ⇝ 19/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 3/4 ⇜ (1/1 → 9/8) ⇝ 5/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (1/1 → 9/8) ⇝ 3/2 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (5/4 → 13/8) ⇝ 7/4 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (3/2 → 7/4) ⇝ 2/1 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (7/4 → 15/8) ⇝ 9/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 1/1 ⇜ (5/4 → 3/2) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (7/4 → 15/8) ⇝ 9/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 5/8 ⇜ (1/1 → 9/8) | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 7/8 ⇜ (1/1 → 19/16) ⇝ 11/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (9/8 → 5/4) ⇝ 13/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (11/8 → 27/16) ⇝ 15/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (13/8 → 29/16) ⇝ 17/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (15/8 → 31/16) ⇝ 19/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 9/8 ⇜ (11/8 → 13/8) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (15/8 → 31/16) ⇝ 19/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 2/1 → 17/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 5/2 → 21/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 11/4 → 23/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 9/4 → 19/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 11/4 → 23/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", - "[ 5/2 → 21/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 7/4 ⇜ (2/1 → 9/4) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 5/2 → 21/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 11/4 → 23/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 13/8 ⇜ (2/1 → 33/16) ⇝ 17/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 15/8 ⇜ (2/1 → 33/16) ⇝ 19/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", @@ -1523,24 +1523,24 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (23/8 → 47/16) ⇝ 3/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 19/8 → 5/2 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (23/8 → 47/16) ⇝ 3/1 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", - "[ 21/8 → 11/4 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 15/8 ⇜ (17/8 → 19/8) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 21/8 → 11/4 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ (23/8 → 47/16) ⇝ 3/1 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 7/4 ⇜ (2/1 → 17/8) ⇝ 9/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (2/1 → 17/8) ⇝ 5/2 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 9/4 → 19/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 11/4 → 23/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 5/2 → 21/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 11/4 → 23/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 2/1 ⇜ (9/4 → 5/2) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 11/4 → 23/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 13/8 ⇜ (2/1 → 17/8) | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 15/8 ⇜ (2/1 → 35/16) ⇝ 19/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (17/8 → 9/4) ⇝ 21/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 19/8 → 5/2 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ (23/8 → 47/16) ⇝ 3/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 21/8 → 11/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ (23/8 → 47/16) ⇝ 3/1 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 17/8 ⇜ (19/8 → 21/8) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ (23/8 → 47/16) ⇝ 3/1 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 3/1 → 13/4 | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 7/2 → 15/4 | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 3/1 → 25/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", @@ -1548,8 +1548,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (15/4 → 31/8) ⇝ 4/1 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 3/1 → 25/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 13/4 → 27/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ 7/2 → 15/4 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 13/4 → 27/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ 7/2 → 15/4 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 7/2 → 29/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ (15/4 → 31/8) ⇝ 4/1 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ 25/8 → 27/8 | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", @@ -1559,8 +1559,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (31/8 → 63/16) ⇝ 33/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 25/8 → 13/4 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 27/8 → 7/2 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ (29/8 → 61/16) ⇝ 31/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 27/8 → 7/2 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (29/8 → 61/16) ⇝ 31/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 29/8 → 15/4 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ (31/8 → 63/16) ⇝ 33/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ 3/1 → 25/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", @@ -1572,8 +1572,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ 7/2 → 15/4 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 13/4 → 27/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 7/2 → 29/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ (15/4 → 31/8) ⇝ 4/1 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 7/2 → 29/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (15/4 → 31/8) ⇝ 4/1 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 15/4 → 31/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ 25/8 → 13/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 25/8 → 13/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", @@ -1584,30 +1584,30 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (29/8 → 61/16) ⇝ 31/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 27/8 → 7/2 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 29/8 → 15/4 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ (31/8 → 63/16) ⇝ 33/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 29/8 → 15/4 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (31/8 → 63/16) ⇝ 33/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ (31/8 → 63/16) ⇝ 4/1 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ 4/1 → 9/2 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (17/4 → 37/8) ⇝ 19/4 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 4/1 → 17/4 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (9/2 → 19/4) ⇝ 5/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (17/4 → 35/8) ⇝ 9/2 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (9/2 → 19/4) ⇝ 5/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (19/4 → 39/8) ⇝ 21/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 31/8 ⇜ (4/1 → 65/16) ⇝ 33/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 31/8 ⇜ (4/1 → 65/16) ⇝ 33/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ (33/8 → 73/16) ⇝ 37/8 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (35/8 → 75/16) ⇝ 39/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 33/8 → 35/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (37/8 → 77/16) ⇝ 41/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (35/8 → 9/2) ⇝ 37/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (37/8 → 77/16) ⇝ 41/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (39/8 → 79/16) ⇝ 43/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (4/1 → 33/8) ⇝ 17/4 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ (4/1 → 33/8) ⇝ 17/4 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ (17/4 → 37/8) ⇝ 19/4 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (9/2 → 19/4) ⇝ 5/1 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 17/4 → 9/2 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (19/4 → 39/8) ⇝ 21/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (9/2 → 37/8) ⇝ 19/4 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (19/4 → 39/8) ⇝ 21/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 31/8 ⇜ (4/1 → 33/8) | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", "[ (33/8 → 17/4) ⇝ 35/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 31/8 ⇜ (4/1 → 33/8) | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", @@ -1615,40 +1615,40 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (35/8 → 75/16) ⇝ 39/8 | note:D3 clip:1 s:piano release:0.1 pan:0.4814814814814815 ]", "[ (37/8 → 77/16) ⇝ 41/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 35/8 → 37/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", - "[ (39/8 → 79/16) ⇝ 43/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (37/8 → 19/4) ⇝ 39/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", + "[ (39/8 → 79/16) ⇝ 43/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 5/1 → 11/2 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (21/4 → 45/8) ⇝ 23/4 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (11/2 → 23/4) ⇝ 6/1 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 19/4 ⇜ (5/1 → 21/4) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (11/2 → 23/4) ⇝ 6/1 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (23/4 → 47/8) ⇝ 25/4 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 37/8 ⇜ (5/1 → 81/16) ⇝ 41/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 39/8 ⇜ (5/1 → 81/16) ⇝ 43/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (41/8 → 89/16) ⇝ 45/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (43/8 → 91/16) ⇝ 47/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (45/8 → 93/16) ⇝ 49/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 39/8 ⇜ (41/8 → 43/8) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (45/8 → 93/16) ⇝ 49/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (47/8 → 95/16) ⇝ 51/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 19/4 ⇜ (5/1 → 41/8) ⇝ 21/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (5/1 → 41/8) ⇝ 11/2 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (21/4 → 45/8) ⇝ 23/4 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (11/2 → 23/4) ⇝ 6/1 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (23/4 → 47/8) ⇝ 25/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 5/1 ⇜ (21/4 → 11/2) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (23/4 → 47/8) ⇝ 25/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 37/8 ⇜ (5/1 → 41/8) | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ 39/8 ⇜ (5/1 → 83/16) ⇝ 43/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (41/8 → 21/4) ⇝ 45/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (43/8 → 91/16) ⇝ 47/8 | note:F3 clip:1 s:piano release:0.1 pan:0.49537037037037035 ]", "[ (45/8 → 93/16) ⇝ 49/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", - "[ (47/8 → 95/16) ⇝ 51/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 41/8 ⇜ (43/8 → 45/8) | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ (47/8 → 95/16) ⇝ 51/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 6/1 → 49/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 13/2 → 53/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 27/4 → 55/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 25/4 → 51/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 27/4 → 55/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", - "[ 13/2 → 53/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 23/4 ⇜ (6/1 → 25/4) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 13/2 → 53/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 27/4 → 55/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 45/8 ⇜ (6/1 → 97/16) ⇝ 49/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ 47/8 ⇜ (6/1 → 97/16) ⇝ 51/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", @@ -1657,24 +1657,24 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (55/8 → 111/16) ⇝ 7/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 51/8 → 13/2 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ (55/8 → 111/16) ⇝ 7/1 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", - "[ 53/8 → 27/4 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 47/8 ⇜ (49/8 → 51/8) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 53/8 → 27/4 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ (55/8 → 111/16) ⇝ 7/1 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 23/4 ⇜ (6/1 → 49/8) ⇝ 25/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (6/1 → 49/8) ⇝ 13/2 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 25/4 → 51/8 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 27/4 → 55/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 13/2 → 53/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ 27/4 → 55/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 6/1 ⇜ (25/4 → 13/2) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ 27/4 → 55/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 45/8 ⇜ (6/1 → 49/8) | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ 47/8 ⇜ (6/1 → 99/16) ⇝ 51/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", "[ (49/8 → 25/4) ⇝ 53/8 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 51/8 → 13/2 | note:A3 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", "[ (55/8 → 111/16) ⇝ 7/1 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 53/8 → 27/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", - "[ (55/8 → 111/16) ⇝ 7/1 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 49/8 ⇜ (51/8 → 53/8) | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", + "[ (55/8 → 111/16) ⇝ 7/1 | note:G4 clip:1 s:piano release:0.1 pan:0.5601851851851851 ]", "[ 7/1 → 29/4 | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 15/2 → 31/4 | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", "[ 7/1 → 57/8 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", @@ -1682,8 +1682,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (31/4 → 63/8) ⇝ 8/1 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 7/1 → 57/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 29/4 → 59/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ 15/2 → 31/4 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 29/4 → 59/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ 15/2 → 31/4 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 15/2 → 61/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ (31/4 → 63/8) ⇝ 8/1 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ 57/8 → 59/8 | note:G3 clip:1 s:piano release:0.1 pan:0.5046296296296297 ]", @@ -1693,8 +1693,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (63/8 → 127/16) ⇝ 65/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 57/8 → 29/4 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 59/8 → 15/2 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ (61/8 → 125/16) ⇝ 63/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 59/8 → 15/2 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (61/8 → 125/16) ⇝ 63/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 61/8 → 31/4 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ (63/8 → 127/16) ⇝ 65/8 | note:A4 clip:1 s:piano release:0.1 pan:0.5694444444444444 ]", "[ 7/1 → 57/8 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", @@ -1706,8 +1706,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ 15/2 → 31/4 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 29/4 → 59/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 15/2 → 61/8 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ (31/4 → 63/8) ⇝ 8/1 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 15/2 → 61/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (31/4 → 63/8) ⇝ 8/1 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 31/4 → 63/8 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", "[ 57/8 → 29/4 | note:C4 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", "[ 57/8 → 29/4 | note:E4 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", @@ -1718,8 +1718,8 @@ exports[`renders tunes > tune: echoPiano 1`] = ` "[ (61/8 → 125/16) ⇝ 63/8 | note:Bb3 clip:1 s:piano release:0.1 pan:0.5185185185185186 ]", "[ 59/8 → 15/2 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", "[ 61/8 → 31/4 | note:Bb4 clip:1 s:piano release:0.1 pan:0.5740740740740741 ]", - "[ (63/8 → 127/16) ⇝ 65/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ 61/8 → 31/4 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", + "[ (63/8 → 127/16) ⇝ 65/8 | note:F4 clip:1 s:piano release:0.1 pan:0.5509259259259259 ]", "[ (63/8 → 127/16) ⇝ 8/1 | note:D5 clip:1 s:piano release:0.1 pan:0.5925925925925926 ]", ] `; @@ -9910,13 +9910,13 @@ exports[`renders tunes > tune: waa2 1`] = ` "[ 1/2 ⇜ (1/1 → 2752604/2452023) | n:69 s:sawtooth cutoff:3850.2031844444546 gain:0.5 room:0.5 ]", "[ -1/4 ⇜ (0/1 → 1/4) ⇝ 3654347/9808092 | n:48 s:sawtooth cutoff:3995.31915093835 gain:0.5 room:0.5 ]", "[ -1/4 ⇜ (1/4 → 3053185/9808092) ⇝ 3654347/9808092 | n:48 s:square cutoff:3995.31915093835 gain:0.5 room:0.5 ]", + "[ -1/4 ⇜ (0/1 → 1/4) ⇝ 3654347/9808092 | n:64 s:sawtooth cutoff:3995.31915093835 gain:0.5 room:0.5 ]", + "[ -1/4 ⇜ (1/4 → 3053185/9808092) ⇝ 3654347/9808092 | n:64 s:square cutoff:3995.31915093835 gain:0.5 room:0.5 ]", "[ (1/4 → 1/2) ⇝ 8558393/9808092 | n:74 s:square cutoff:3957.6603580168244 gain:0.5 room:0.5 ]", "[ 1/4 ⇜ (1/2 → 3/4) ⇝ 8558393/9808092 | n:74 s:sawtooth cutoff:3957.6603580168244 gain:0.5 room:0.5 ]", "[ 1/4 ⇜ (3/4 → 8558393/9808092) | n:74 s:square cutoff:3957.6603580168244 gain:0.5 room:0.5 ]", "[ (3/4 → 1/1) ⇝ 5204627/4904046 | n:62 s:square cutoff:3897.7021140702864 gain:0.5 room:0.5 ]", "[ 3/4 ⇜ (1/1 → 5204627/4904046) | n:62 s:sawtooth cutoff:3858.612673535166 gain:0.5 room:0.5 ]", - "[ -1/4 ⇜ (0/1 → 1/4) ⇝ 3654347/9808092 | n:64 s:sawtooth cutoff:3995.31915093835 gain:0.5 room:0.5 ]", - "[ -1/4 ⇜ (1/4 → 3053185/9808092) ⇝ 3654347/9808092 | n:64 s:square cutoff:3995.31915093835 gain:0.5 room:0.5 ]", "[ (1/4 → 1/2) ⇝ 8558393/9808092 | n:55 s:square cutoff:3957.6603580168244 gain:0.5 room:0.5 ]", "[ 1/4 ⇜ (1/2 → 3/4) ⇝ 8558393/9808092 | n:55 s:sawtooth cutoff:3957.6603580168244 gain:0.5 room:0.5 ]", "[ 1/4 ⇜ (3/4 → 8558393/9808092) | n:55 s:square cutoff:3957.6603580168244 gain:0.5 room:0.5 ]", diff --git a/repl/src/tunes.mjs b/repl/src/tunes.mjs index fcb7b4db..843be832 100644 --- a/repl/src/tunes.mjs +++ b/repl/src/tunes.mjs @@ -582,7 +582,7 @@ export const chop = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.or samples({ p: 'https://cdn.freesound.org/previews/648/648433_11943129-lq.mp3' }) s("p") - .loopAt(32,1) + .loopAt(32) .chop(128) .jux(rev) .shape(.4) @@ -652,7 +652,7 @@ samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3', dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}}) stack( -s('bass').loopAt(8,1).clip(1), +s('bass').loopAt(8).clip(1), s("bd*2, ~ sd,hh*4"), note("Abm7".voicings(['c3','a4']).struct("x(3,8,1)".slow(2))), "0 1 2 3".scale('ab4 minor pentatonic') diff --git a/tutorial/test/__snapshots__/examples.test.mjs.snap b/tutorial/test/__snapshots__/examples.test.mjs.snap index 7806bb8c..64dd5cb6 100644 --- a/tutorial/test/__snapshots__/examples.test.mjs.snap +++ b/tutorial/test/__snapshots__/examples.test.mjs.snap @@ -374,16 +374,16 @@ exports[`runs examples > example "chunk" example index 0 1`] = ` "[ 1/4 → 1/2 | note:B3 ]", "[ 1/2 → 3/4 | note:C4 ]", "[ 3/4 → 1/1 | note:D4 ]", - "[ 7/4 → 2/1 | note:D5 ]", "[ 1/1 → 5/4 | note:A3 ]", "[ 5/4 → 3/2 | note:B3 ]", "[ 3/2 → 7/4 | note:C4 ]", - "[ 5/2 → 11/4 | note:C5 ]", + "[ 7/4 → 2/1 | note:D5 ]", "[ 2/1 → 9/4 | note:A3 ]", "[ 9/4 → 5/2 | note:B3 ]", + "[ 5/2 → 11/4 | note:C5 ]", "[ 11/4 → 3/1 | note:D4 ]", - "[ 13/4 → 7/2 | note:B4 ]", "[ 3/1 → 13/4 | note:A3 ]", + "[ 13/4 → 7/2 | note:B4 ]", "[ 7/2 → 15/4 | note:C4 ]", "[ 15/4 → 4/1 | note:D4 ]", ] @@ -395,18 +395,18 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = ` "[ 1/4 → 1/2 | note:B3 ]", "[ 1/2 → 3/4 | note:C4 ]", "[ 3/4 → 1/1 | note:D4 ]", - "[ 5/4 → 3/2 | note:B4 ]", "[ 1/1 → 5/4 | note:A3 ]", + "[ 5/4 → 3/2 | note:B4 ]", "[ 3/2 → 7/4 | note:C4 ]", "[ 7/4 → 2/1 | note:D4 ]", - "[ 5/2 → 11/4 | note:C5 ]", "[ 2/1 → 9/4 | note:A3 ]", "[ 9/4 → 5/2 | note:B3 ]", + "[ 5/2 → 11/4 | note:C5 ]", "[ 11/4 → 3/1 | note:D4 ]", - "[ 15/4 → 4/1 | note:D5 ]", "[ 3/1 → 13/4 | note:A3 ]", "[ 13/4 → 7/2 | note:B3 ]", "[ 7/2 → 15/4 | note:C4 ]", + "[ 15/4 → 4/1 | note:D5 ]", ] `; @@ -1906,6 +1906,15 @@ exports[`runs examples > example "loopAt" example index 0 1`] = ` ] `; +exports[`runs examples > example "loopAtCps" example index 0 1`] = ` +[ + "[ (0/1 → 1/1) ⇝ 4/1 | s:rhodes speed:0.375 unit:c cps:1.5 ]", + "[ 0/1 ⇜ (1/1 → 2/1) ⇝ 4/1 | s:rhodes speed:0.375 unit:c cps:1.5 ]", + "[ 0/1 ⇜ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.375 unit:c cps:1.5 ]", + "[ 0/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.375 unit:c cps:1.5 ]", +] +`; + exports[`runs examples > example "lrate" example index 0 1`] = ` [ "[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]", @@ -2059,27 +2068,27 @@ exports[`runs examples > example "off" example index 0 1`] = ` "[ 0/1 → 1/3 | note:c3 ]", "[ 1/3 → 2/3 | note:eb3 ]", "[ 2/3 → 1/1 | note:g3 ]", - "[ 1/1 → 4/3 | note:c3 ]", - "[ 4/3 → 5/3 | note:eb3 ]", - "[ 5/3 → 2/1 | note:g3 ]", - "[ 2/1 → 7/3 | note:c3 ]", - "[ 7/3 → 8/3 | note:eb3 ]", - "[ 8/3 → 3/1 | note:g3 ]", - "[ 3/1 → 10/3 | note:c3 ]", - "[ 10/3 → 11/3 | note:eb3 ]", - "[ 11/3 → 4/1 | note:g3 ]", "[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]", "[ 1/8 → 11/24 | note:55 ]", "[ 11/24 → 19/24 | note:58 ]", "[ (19/24 → 1/1) ⇝ 9/8 | note:62 ]", + "[ 1/1 → 4/3 | note:c3 ]", + "[ 4/3 → 5/3 | note:eb3 ]", + "[ 5/3 → 2/1 | note:g3 ]", "[ 19/24 ⇜ (1/1 → 9/8) | note:62 ]", "[ 9/8 → 35/24 | note:55 ]", "[ 35/24 → 43/24 | note:58 ]", "[ (43/24 → 2/1) ⇝ 17/8 | note:62 ]", + "[ 2/1 → 7/3 | note:c3 ]", + "[ 7/3 → 8/3 | note:eb3 ]", + "[ 8/3 → 3/1 | note:g3 ]", "[ 43/24 ⇜ (2/1 → 17/8) | note:62 ]", "[ 17/8 → 59/24 | note:55 ]", "[ 59/24 → 67/24 | note:58 ]", "[ (67/24 → 3/1) ⇝ 25/8 | note:62 ]", + "[ 3/1 → 10/3 | note:c3 ]", + "[ 10/3 → 11/3 | note:eb3 ]", + "[ 11/3 → 4/1 | note:g3 ]", "[ 67/24 ⇜ (3/1 → 25/8) | note:62 ]", "[ 25/8 → 83/24 | note:55 ]", "[ 83/24 → 91/24 | note:58 ]", @@ -3181,17 +3190,17 @@ exports[`runs examples > example "webdirt" example index 0 1`] = ` exports[`runs examples > example "when" example index 0 1`] = ` [ - "[ 2/1 → 7/3 | note:43 ]", - "[ 7/3 → 8/3 | note:46 ]", - "[ 8/3 → 3/1 | note:50 ]", - "[ 3/1 → 10/3 | note:43 ]", - "[ 10/3 → 11/3 | note:46 ]", - "[ 11/3 → 4/1 | note:50 ]", "[ 0/1 → 1/3 | note:c3 ]", "[ 1/3 → 2/3 | note:eb3 ]", "[ 2/3 → 1/1 | note:g3 ]", "[ 1/1 → 4/3 | note:c3 ]", "[ 4/3 → 5/3 | note:eb3 ]", "[ 5/3 → 2/1 | note:g3 ]", + "[ 2/1 → 7/3 | note:43 ]", + "[ 7/3 → 8/3 | note:46 ]", + "[ 8/3 → 3/1 | note:50 ]", + "[ 3/1 → 10/3 | note:43 ]", + "[ 10/3 → 11/3 | note:46 ]", + "[ 11/3 → 4/1 | note:50 ]", ] `;