diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index ce614246..a339567f 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import Fraction from 'fraction.js'; import { TimeSpan } from './timespan.mjs'; +import { removeUndefineds } from './util.mjs'; // Returns the start of the cycle. Fraction.prototype.sam = function () { @@ -64,6 +65,22 @@ Fraction.prototype.min = function (other) { return this.lt(other) ? this : other; }; +Fraction.prototype.mulmaybe = function (other) { + return other !== undefined ? this.mul(other) : undefined; +}; + +Fraction.prototype.divmaybe = function (other) { + return other !== undefined ? this.div(other) : undefined; +}; + +Fraction.prototype.addmaybe = function (other) { + return other !== undefined ? this.add(other) : undefined; +}; + +Fraction.prototype.submaybe = function (other) { + return other !== undefined ? this.sub(other) : undefined; +}; + Fraction.prototype.show = function (/* excludeWhole = false */) { // return this.toFraction(excludeWhole); return this.s * this.n + '/' + this.d; @@ -89,11 +106,24 @@ const fraction = (n) => { }; export const gcd = (...fractions) => { + fractions = removeUndefineds(fractions); + if (fractions.length === 0) { + return undefined; + } + return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1)); }; export const lcm = (...fractions) => { - return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1)); + fractions = removeUndefineds(fractions); + if (fractions.length === 0) { + return undefined; + } + + return fractions.reduce( + (lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)), + fraction(1), + ); }; fraction._original = Fraction; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 91174857..7291c12f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -42,16 +42,16 @@ export class Pattern { */ constructor(query, tactus = undefined) { this.query = query; - this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern - this.__tactus = Fraction(tactus); // in terms of number of steps per cycle + this._Pattern = true; // this property is used to detectinstance of another Pattern + this.tactus = tactus; // in terms of number of steps per cycle } get tactus() { - return this.__tactus ?? Fraction(1); + return this.__tactus; } set tactus(tactus) { - this.__tactus = Fraction(tactus); + this.__tactus = tactus === undefined ? undefined : Fraction(tactus); } setTactus(tactus) { @@ -63,6 +63,10 @@ export class Pattern { return new Pattern(this.query, this.tactus === undefined ? undefined : f(this.tactus)); } + get hasTactus() { + return this.tactus !== undefined; + } + ////////////////////////////////////////////////////////////////////// // Haskell-style functor, applicative and monadic operations @@ -1181,7 +1185,7 @@ export const pm = s_polymeter; * @example * gap(3) // "~@3" */ -export const gap = (tactus) => new Pattern(() => [], Fraction(tactus)); +export const gap = (tactus) => new Pattern(() => [], tactus); /** * Does absolutely nothing.. @@ -1762,7 +1766,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun */ export const ply = register('ply', function (factor, pat) { const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); - result.tactus = pat.tactus.mul(factor); + result.tactus = Fraction(factor).mulmaybe(pat.tactus); return result; }); @@ -1784,7 +1788,7 @@ export const { fast, density } = register(['fast', 'density'], function (factor, factor = Fraction(factor); const fastQuery = pat.withQueryTime((t) => t.mul(factor)); const result = fastQuery.withHapTime((t) => t.div(factor)); - result.tactus = factor.mul(pat.tactus); + result.tactus = factor.mulmaybe(pat.tactus); return result; }); @@ -1954,7 +1958,7 @@ export const zoom = register('zoom', function (s, e, pat) { return nothing; } const d = e.sub(s); - const tactus = pat.tactus.mul(d); + const tactus = pat.tactus.mulmaybe(d); return pat .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) @@ -2408,15 +2412,15 @@ Pattern.prototype.stepJoin = function () { }; export function _retime(timedHaps) { - const occupied_perc = timedHaps.filter((t, pat) => pat.tactus != undefined).reduce((a, b) => a.add(b), Fraction(0)); + const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0)); const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce( (a, b) => a.add(b), Fraction(0), ); - const total_tactus = occupied_perc.eq(0) ? 0 : occupied_tactus.div(occupied_perc); + const total_tactus = occupied_perc.eq(0) ? undefined : occupied_tactus.div(occupied_perc); function adjust(dur, pat) { if (pat.tactus === undefined) { - return [dur.mul(total_tactus), pat]; + return [dur.mulmaybe(total_tactus), pat]; } return [pat.tactus, pat]; } @@ -2454,7 +2458,10 @@ export function _match(span, hap_p) { * // The same as s("{bd sd cp}%4") */ export const steps = register('steps', function (targetTactus, pat) { - if (pat.tactus.eq(0)) { + if (pat.tactus === undefined) { + return pat; + } + if (pat.tactus.eq(Fraction(0))) { // avoid divide by zero.. return nothing; } @@ -2520,10 +2527,16 @@ export function s_polymeter(...args) { return _polymeterListSteps(0, ...args); } + // TODO currently ignoring arguments without tactus... + args = args.filter((arg) => arg.hasTactus); + if (args.length == 0) { return silence; } const tactus = args[0].tactus; + if (tactus.eq(Fraction(0))) { + return nothing; + } const [head, ...tail] = args; const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus)))); @@ -2543,8 +2556,26 @@ export function s_polymeter(...args) { * // the same as "bd sd cp hh hh".sound() */ export function s_cat(...timepats) { + if (timepats.length === 0) { + return nothing; + } const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]); timepats = timepats.map(findtactus); + if (timepats.find((x) => x[0] === undefined)) { + const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); + if (times.length === 0) { + return fastcat(...timepats.map((x) => x[1])); + } + if (times.length === timepats.length) { + return nothing; + } + const avg = times.reduce((a, b) => a.add(b), Fraction(0)).div(times.length); + for (let timepat of timepats) { + if (timepat[0] === undefined) { + timepat[0] = avg; + } + } + } if (timepats.length == 1) { const result = reify(timepats[0][1]); result.tactus = timepats[0][0]; @@ -2588,7 +2619,7 @@ export function s_alt(...groups) { for (let cycle = 0; cycle < cycles; ++cycle) { result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length]))); } - result = result.filter((x) => x.tactus > 0); + result = result.filter((x) => x.hasTactus && x.tactus > 0); const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0)); result = s_cat(...result); result.tactus = tactus; @@ -2599,6 +2630,9 @@ export function s_alt(...groups) { * *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'. */ export const s_add = stepRegister('s_add', function (i, pat) { + if (!pat.hasTactus) { + return nothing; + } if (pat.tactus.lte(0)) { return nothing; } @@ -2627,6 +2661,10 @@ export const s_add = stepRegister('s_add', function (i, pat) { * *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'. */ export const s_sub = stepRegister('s_sub', function (i, pat) { + if (!pat.hasTactus) { + return nothing; + } + i = Fraction(i); if (i.lt(0)) { return pat.s_add(Fraction(0).sub(pat.tactus.add(i))); @@ -2647,10 +2685,15 @@ export const s_contract = stepRegister('s_contract', function (factor, pat) { */ Pattern.prototype.s_taperlist = function (amount, times) { const pat = this; + + if (!pat.hasTactus) { + return [pat]; + } + times = times - 1; if (times === 0) { - return pat; + return [pat]; } const list = []; @@ -2675,6 +2718,10 @@ export const s_taperlist = (amount, times, pat) => pat.s_taperlist(amount, times export const s_taper = register( 's_taper', function (amount, times, pat) { + if (!pat.hasTactus) { + return nothing; + } + const list = pat.s_taperlist(amount, times); const result = s_cat(...list); result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0)); @@ -2726,7 +2773,7 @@ export const chop = register('chop', function (n, pat) { const func = function (o) { return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); }; - return pat.squeezeBind(func).setTactus(pat.tactus.mul(n)); + return pat.squeezeBind(func).setTactus(Fraction(n).mulmaybe(pat.tactus)); }); /** diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 9c17d8be..4bed34cb 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -47,6 +47,10 @@ import { time, run, pick, + stackLeft, + stackRight, + stackCentre, + s_cat, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1136,6 +1140,41 @@ describe('Pattern', () => { expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual( Fraction(4), ); + expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12)); + expect( + pure((x) => x + 1) + .setTactus(3) + .appBoth(pure(1).setTactus(2)).tactus, + ).toStrictEqual(Fraction(6)); + expect( + pure((x) => x + 1) + .setTactus(undefined) + .appBoth(pure(1).setTactus(2)).tactus, + ).toStrictEqual(Fraction(2)); + expect( + pure((x) => x + 1) + .setTactus(3) + .appBoth(pure(1).setTactus(undefined)).tactus, + ).toStrictEqual(Fraction(3)); + expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6)); + expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3)); + expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4)); + expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3)); + // maybe this should double when they are either all even or all odd + expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3)); + expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6)); + expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined); + expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(6)); + expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined); + }); + }); + describe('s_cat', () => { + it('can cat', () => { + expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5))); + expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3))); + }); + it('calculates undefined tactuses as the average', () => { + expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3))); }); }); describe('s_taper', () => { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a2e01092..4c25d23e 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -188,61 +188,66 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n * .s("piano") */ -export const scale = register('scale', function (scale, pat) { - // Supports ':' list syntax in mininotation - if (Array.isArray(scale)) { - scale = scale.flat().join(' '); - } - return ( - pat - .fmap((value) => { - const isObject = typeof value === 'object'; - let step = isObject ? value.n : value; - if (isObject) { - delete value.n; // remove n so it won't cause trouble - } - if (isNote(step)) { - // legacy.. - return pure(step); - } - let asNumber = Number(step); - let semitones = 0; - if (isNaN(asNumber)) { - step = String(step); - if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) { - logger( - `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, - 'error', - ); - return silence; +export const scale = register( + 'scale', + function (scale, pat) { + // Supports ':' list syntax in mininotation + if (Array.isArray(scale)) { + scale = scale.flat().join(' '); + } + return ( + pat + .fmap((value) => { + const isObject = typeof value === 'object'; + let step = isObject ? value.n : value; + if (isObject) { + delete value.n; // remove n so it won't cause trouble } - const isharp = step.indexOf('#'); - if (isharp >= 0) { - asNumber = Number(step.substring(0, isharp)); - semitones = step.length - isharp; - } else { - const iflat = step.indexOf('b'); - asNumber = Number(step.substring(0, iflat)); - semitones = iflat - step.length; + if (isNote(step)) { + // legacy.. + return pure(step); } - } - try { - let note; - if (isObject && value.anchor) { - note = stepInNamedScale(asNumber, scale, value.anchor); - } else { - note = scaleStep(asNumber, scale); + let asNumber = Number(step); + let semitones = 0; + if (isNaN(asNumber)) { + step = String(step); + if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) { + logger( + `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, + 'error', + ); + return silence; + } + const isharp = step.indexOf('#'); + if (isharp >= 0) { + asNumber = Number(step.substring(0, isharp)); + semitones = step.length - isharp; + } else { + const iflat = step.indexOf('b'); + asNumber = Number(step.substring(0, iflat)); + semitones = iflat - step.length; + } } - if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones)); - value = pure(isObject ? { ...value, note } : note); - } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); - value = silence; - } - return value; - }) - .outerJoin() - // legacy: - .withHap((hap) => hap.setContext({ ...hap.context, scale })) - ); -}); + try { + let note; + if (isObject && value.anchor) { + note = stepInNamedScale(asNumber, scale, value.anchor); + } else { + note = scaleStep(asNumber, scale); + } + if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones)); + value = pure(isObject ? { ...value, note } : note); + } catch (err) { + logger(`[tonal] ${err.message}`, 'error'); + value = silence; + } + return value; + }) + .outerJoin() + // legacy: + .withHap((hap) => hap.setContext({ ...hap.context, scale })) + ); + }, + true, + true, // preserve tactus +);