diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index aa5e1060f..0f5083901 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1002,7 +1002,7 @@ export const arpWith = register('arpWith', (func, pat) => { * */ export const arp = register( 'arp', - (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])), + (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[_mod(i, haps.length)])), false, ); @@ -1529,7 +1529,9 @@ export function slowcat(...pats) { // Array test here is to avoid infinite recursions.. pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat))); - if (pats.length == 1) { + if (!pats.length) { + return silence; + } else if (pats.length == 1) { return pats[0]; } @@ -1537,10 +1539,6 @@ export function slowcat(...pats) { const span = state.span; const pat_n = _mod(span.begin.sam(), pats.length); const pat = pats[pat_n]; - if (!pat) { - // pat_n can be negative, if the span is in the past.. - return []; - } // A bit of maths to make sure that cycles from constituent patterns aren't skipped. // For example if three patterns are slowcat-ed, the fourth cycle of the result should // be the second (rather than fourth) cycle from the first pattern. @@ -1557,11 +1555,14 @@ export function slowcat(...pats) { * @return {Pattern} */ export function slowcatPrime(...pats) { + if (!pats.length) { + return silence; + } pats = pats.map(reify); const query = function (state) { - const pat_n = Math.floor(state.span.begin) % pats.length; - const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp - return pat?.query(state) || []; + const pat_n = _mod(Math.floor(state.span.begin), pats.length); + const pat = pats[pat_n]; + return pat.query(state); }; return new Pattern(query).splitQueries(); } diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 4398e2ab7..b86f16e8a 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { Hap } from './hap.mjs'; import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs'; +import { _mod } from './util.mjs'; import Fraction from './fraction.mjs'; import { id, keyAlias, getCurrentKeyboardState } from './util.mjs'; @@ -33,7 +34,7 @@ export const signal = (func) => { * .scale('C major') * */ -export const saw = signal((t) => t % 1); +export const saw = signal((t) => _mod(t, 1)); /** * A sawtooth signal between -1 and 1 (like `saw`, but bipolar). @@ -56,7 +57,7 @@ export const saw2 = saw.toBipolar(); * .scale('C major') * */ -export const isaw = signal((t) => 1 - (t % 1)); +export const isaw = signal((t) => 1 - _mod(t, 1)); /** * A sawtooth signal between 1 and -1 (like `saw2`, but flipped). @@ -113,7 +114,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4)); * n(square.segment(4).range(0,7)).scale("C:minor") * */ -export const square = signal((t) => Math.floor((t * 2) % 2)); +export const square = signal((t) => Math.floor(_mod(t * 2, 2))); /** * A square signal between -1 and 1 (like `square`, but bipolar). @@ -123,6 +124,22 @@ export const square = signal((t) => Math.floor((t * 2) % 2)); */ export const square2 = square.toBipolar(); +/** + * A square signal between 1 and 0 (like `square` but flipped). + * + * @return {Pattern} + * @tags generators + */ +export const isquare = signal((t) => 1 - Math.floor(_mod(t * 2, 2))); + +/** + * A square signal between 1 and -1 (like `isquare`, but bipolar). + * + * @return {Pattern} + * @tags generators + */ +export const isquare2 = isquare.toBipolar(); + /** * A triangle signal between 0 and 1. * @@ -396,7 +413,7 @@ export const randrun = (n) => { .map((n, i) => [n, i]) .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) .map((x) => x[1]); - const i = t.cyclePos().mul(n).floor() % n; + const i = _mod(t.cyclePos().mul(n).floor(), n); return nums[i]; })._segment(n); }; diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index aaa14bd27..cb5345610 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -18,6 +18,7 @@ import { fastcat, firstOf, slowcat, + slowcatPrime, cat, sequence, palindrome, @@ -53,6 +54,7 @@ import { stepcat, sometimes, expand, + arp, } from '../index.mjs'; import { log, logValues } from '../pattern.mjs'; @@ -544,6 +546,9 @@ describe('Pattern', () => { }); }); describe('slowcat()', () => { + it('Can be empty', () => { + expect(slowcat().firstCycle()).toStrictEqual([]); + }); it('Can concatenate things slowly', () => { expect( slowcat('a', 'b') @@ -576,6 +581,38 @@ describe('Pattern', () => { sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2)); }); }); + describe('slowcatPrime()', () => { + it('Can be empty', () => { + expect(slowcatPrime().firstCycle()).toStrictEqual([]); + }); + it('Can slowcat patterns swapping back and forth skipping the expected notes', () => { + expect( + slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2)) + .fast(4) + .firstCycle() + .map((a) => a.value), + ).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]); + }); + it('Can go into negative time', () => { + expect( + slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2)) + .fast(4) + .late(8) + .firstCycle() + .map((a) => a.value), + ).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]); + }); + }); + describe('arp()', () => { + it('It wraps around with both positive and negative indices', () => { + expect( + stack('a', 'b', 'c') + .arp(fastcat(-3, -2, -1, 0, 1, 2, 3, 4)) + .firstCycle() + .map((a) => a.value), + ).toStrictEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']); + }); + }); describe('rev()', () => { it('Can reverse things', () => { expect( diff --git a/packages/core/test/signal.test.mjs b/packages/core/test/signal.test.mjs index 26c1d656a..628864c79 100644 --- a/packages/core/test/signal.test.mjs +++ b/packages/core/test/signal.test.mjs @@ -8,7 +8,23 @@ import Fraction from 'fraction.js'; import { describe, it, expect, vi } from 'vitest'; -import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs'; +import { + saw, + saw2, + isaw, + isaw2, + tri, + tri2, + itri, + itri2, + square, + square2, + isquare, + isquare2, + per, + perx, + cyclesPer, +} from '../signal.mjs'; import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs'; const st = (begin, end) => new State(ts(begin, end)); @@ -24,17 +40,61 @@ const sameFirst = (a, b) => { describe('signal()', () => { it('Can make saw/saw2', () => { - expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual( - sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(), - ); - + expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.25, 0.5, 0.75).firstCycle()); expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle()); }); it('Can make isaw/isaw2', () => { expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle()); - expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle()); }); + it('Can make tri/tri2', () => { + expect(tri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.5, 1, 0.5).firstCycle()); + expect(tri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle()); + }); + it('Can make itri/itri2', () => { + expect(itri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, 0.5).firstCycle()); + expect(itri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle()); + }); + it('Can make square/square2', () => { + expect(square.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle()); + expect(square2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -1, 1, 1).firstCycle()); + }); + it('Can make isquare/isquare2', () => { + expect(isquare.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, 0, 0).firstCycle()); + expect(isquare2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, -1, -1).firstCycle()); + }); + it('Can go into negative time', () => { + expect(saw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(0, 0.25, 0.5, 0.75).firstCycle(), + ); + expect(saw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(-1, -0.5, 0, 0.5).firstCycle(), + ); + expect(isaw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(1, 0.75, 0.5, 0.25).firstCycle(), + ); + expect(isaw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(1, 0.5, 0, -0.5).firstCycle(), + ); + expect(tri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(0, 0.5, 1, 0.5).firstCycle(), + ); + expect(tri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle()); + expect(itri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(1, 0.5, 0, 0.5).firstCycle(), + ); + expect(itri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle()); + expect(square.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle()); + expect(square2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(-1, -1, 1, 1).firstCycle(), + ); + expect(isquare.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(1, 1, 0, 0).firstCycle(), + ); + expect(isquare2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(1, 1, -1, -1).firstCycle(), + ); + }); }); describe('cyclesPer', () => { diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index fc77616c4..1fb178801 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -7203,8 +7203,12 @@ exports[`renders tunes > tune: holyflute 1`] = ` exports[`renders tunes > tune: juxUndTollerei 1`] = ` [ + "[ -99/200 ⇜ (0/1 → 1/200) | note:63 s:triangle pan:0 cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ -99/200 ⇜ (0/1 → 1/200) | note:67 s:triangle pan:1 color:green cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ 1/200 → 101/200 | note:55 s:triangle pan:0 cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ 1/200 → 101/200 | note:65 s:triangle pan:1 color:green cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",