Connor McCutcheon
/ Music
pattern.test.mjs
mjs
/*
pattern.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import {
  TimeSpan,
  Hap,
  State,
  Pattern,
  pure,
  stack,
  fastcat,
  firstOf,
  slowcat,
  cat,
  sequence,
  palindrome,
  polymeter,
  polyrhythm,
  silence,
  fast,
  timeCat,
  add,
  sub,
  mul,
  div,
  saw,
  saw2,
  isaw,
  isaw2,
  sine,
  sine2,
  square,
  square2,
  tri,
  tri2,
  id,
  ply,
  rev,
  time,
  run,
  binaryN,
  pick,
  stackLeft,
  stackRight,
  stackCentre,
  stepcat,
  sometimes,
  expand,
} from '../index.mjs';
import { log, logValues } from '../pattern.mjs';
import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs';
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
  return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
};
describe('TimeSpan', () => {
  describe('equals()', () => {
    it('Should be equal to the same value', () => {
      expect(new TimeSpan(0, 4).equals(new TimeSpan(0, 4))).toBe(true);
    });
  });
  describe('splitCycles', () => {
    it('Should split two cycles into two', () => {
      expect(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length).toBe(2);
    });
  });
  describe('intersection_e', () => {
    var a = new TimeSpan(Fraction(0), Fraction(2));
    var b = new TimeSpan(Fraction(1), Fraction(3));
    var c = new TimeSpan(Fraction(1), Fraction(2));
    var d = new TimeSpan(Fraction(1), Fraction(2));
    it('Should create an intersection', () => {
      expect(a.intersection_e(b).equals(c)).toBe(true);
    });
  });
});
describe('Hap', () => {
  describe('hasOnset()', () => {
    it('True if part includes onset from whole', () => {
      expect(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset()).toBe(true);
    });
  });
  var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'a');
  var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'b');
  var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), 'c');
  var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'd');
  var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'e');
  describe('spanEquals', () => {
    it('True if two haps have the same whole and part', () => {
      expect(a.spanEquals(b)).toBe(true);
    });
    it("False if two haps don't the same whole and part", () => {
      expect(a.spanEquals(c)).toBe(false);
    });
    it('True if two haps have the same part and undefined wholes', () => {
      expect(d.spanEquals(e)).toBe(true);
    });
  });
  describe('resolveState()', () => {
    it('Can increment some state', () => {
      const stateful_value = (state) => {
        const newValue = state['incrementme'];
        // TODO Does the state *need* duplicating here?
        const newState = { ...state };
        newState['incrementme']++;
        return [newState, newValue];
      };
      const state = { incrementme: 10 };
      const ev1 = new Hap(ts(0, 1), ts(0, 1), stateful_value, {}, true);
      const [state2, ev2] = ev1.resolveState(state);
      const [state3, ev3] = ev1.resolveState(state2);
      expect(ev3).toStrictEqual(new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
      expect(state3).toStrictEqual({ incrementme: 12 });
    });
  });
  describe('wholeOrPart()', () => {
    const ts1 = new TimeSpan(Fraction(0), Fraction(1));
    const ts0_5 = new TimeSpan(Fraction(0), Fraction(0.5));
    const continuousHap = new Hap(undefined, ts1, 'hello');
    const discreteHap = new Hap(ts1, ts0_5, 'hello');
    it('Can pick a whole', () => {
      expect(discreteHap.wholeOrPart()).toStrictEqual(ts1);
    });
    it('Can pick a part', () => {
      expect(continuousHap.wholeOrPart()).toStrictEqual(ts1);
    });
  });
});
describe('Pattern', () => {
  describe('pure', () => {
    it('Can make a pattern', () => {
      expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
    });
    it('Supports zero-width queries', () => {
      expect(pure('hello').queryArc(0, 0).length).toBe(1);
    });
  });
  describe('fmap()', () => {
    it('Can add things', () => {
      expect(
        pure(3)
          .fmap((x) => x + 4)
          .firstCycle()[0].value,
      ).toBe(7);
    });
  });
  describe('out()', () => {
    it('is an alias for set.out()', () => {
      sameFirst(sequence(1, 2).out(5, 6, 7, 8), sequence(1, 2).set.out(5, 6, 7, 8));
    });
  });
  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);
    });
    it('can structure Out()', () => {
      sameFirst(sequence(1, 2).add.out(4), sequence(5, 6).struct(true));
    });
    it('can Mix() structure', () => {
      expect(sequence(1, 2).add.mix(silence, 5, silence).firstCycle()).toStrictEqual([
        new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
        new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
      ]);
    });
    it('can Reset() structure', () => {
      sameFirst(
        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
          .add.reset(20, 30)
          .early(2),
        sequence(26, 27, 36, 37),
      );
    });
    it('can Restart() structure', () => {
      sameFirst(
        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
          .add.restart(20, 30)
          .early(2),
        sequence(21, 22, 31, 32),
      );
    });
    it('can Squeeze() structure', () => {
      sameFirst(
        sequence(1, [2, 3]).add.squeeze(sequence(10, 20, 30)),
        sequence(
          [11, 21, 31],
          [
            [12, 22, 32],
            [13, 23, 33],
          ],
        ),
      );
    });
    it('can SqueezeOut() structure', () => {
      sameFirst(
        sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
        sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
      );
    });
    it('can add object patterns', () => {
      sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
    });
  });
  describe('keep()', () => {
    it('can structure In()', () => {
      expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
      expect(pure(3).keep.in(pure(4)).query(st(0, 1))[0].value).toBe(3);
    });
    it('can structure Out()', () => {
      sameFirst(sequence(1, 2).keep.out(4), sequence(1, 2).struct(true));
    });
    it('can Mix() structure', () => {
      expect(sequence(1, 2).keep.mix(silence, 5, silence).firstCycle()).toStrictEqual([
        new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
        new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
      ]);
    });
    it('can Reset() structure', () => {
      sameFirst(
        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
          .keep.reset(20, 30)
          .early(2),
        sequence(6, 7, 6, 7),
      );
    });
    it('can Restart() structure', () => {
      sameFirst(
        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
          .keep.restart(20, 30)
          .early(2),
        sequence(1, 2, 1, 2),
      );
    });
    it('can Squeeze() structure', () => {
      sameFirst(
        sequence(1, [2, 3]).keep.squeeze(sequence(10, 20, 30)),
        sequence(
          [1, 1, 1],
          [
            [2, 2, 2],
            [3, 3, 3],
          ],
        ),
      );
    });
    it('can SqueezeOut() structure', () => {
      sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
    });
  });
  describe('keepif()', () => {
    it('can structure In()', () => {
      sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
      sameFirst(sequence(3, 4).keepif.in(true, false), sequence(3, silence));
    });
    it('can structure Out()', () => {
      sameFirst(pure(1).keepif.out(true, false), sequence(1, silence));
    });
    it('can Mix() structure', () => {
      expect(sequence(1, 2).keepif.mix(false, true, false).firstCycle()).toStrictEqual([
        new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
        new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
      ]);
    });
    it('can Reset() structure', () => {
      sameFirst(
        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
          .keepif.reset(false, true)
          .early(2),
        sequence(silence, silence, 6, 7),
      );
    });
    it('can Restart() structure', () => {
      sameFirst(
        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
          .keepif.restart(false, true)
          .early(2),
        sequence(silence, silence, 1, 2),
      );
    });
    it('can Squeeze() structure', () => {
      sameFirst(
        sequence(1, [2, 3]).keepif.squeeze(sequence(true, true, false)),
        sequence(
          [1, 1, silence],
          [
            [2, 2, silence],
            [3, 3, silence],
          ],
        ),
      );
    });
    it('can SqueezeOut() structure', () => {
      sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
    });
  });
  describe('sub()', () => {
    it('Can subtract things', () => {
      expect(pure(3).sub(pure(4)).query(st(0, 1))[0].value).toBe(-1);
    });
  });
  describe('mul()', () => {
    it('Can multiply things', () => {
      expect(pure(3).mul(pure(2)).firstCycle()[0].value).toBe(6);
    });
  });
  describe('div()', () => {
    it('Can divide things', () => {
      expect(pure(3).div(pure(2)).firstCycle()[0].value).toBe(1.5);
    });
  });
  describe('set()', () => {
    it('Can set things in objects', () => {
      expect(
        pure({ a: 4, b: 6 })
          .set(pure({ c: 7 }))
          .firstCycle()[0].value,
      ).toStrictEqual({
        a: 4,
        b: 6,
        c: 7,
      });
      sameFirst(
        sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
        sequence({ a: 4, b: 2, c: 5 }).fast(3),
      );
    });
    it('Can set things with plain values', () => {
      sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
    });
    describe('setOut()', () => {
      it('Can set things with structure from second pattern', () => {
        sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
      });
    });
    describe('setSqueeze()', () => {
      it('Can squeeze one pattern inside the haps of another', () => {
        sameFirst(
          sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
          sequence(
            ['a', 'b', 'c'],
            [
              ['a', 'b', 'c'],
              ['a', 'b', 'c'],
            ],
          ),
        );
        sameFirst(
          sequence(1, [2, 3]).set.squeeze('a', 'b', 'c'),
          sequence(
            ['a', 'b', 'c'],
            [
              ['a', 'b', 'c'],
              ['a', 'b', 'c'],
            ],
          ),
        );
      });
    });
  });
  describe('stack()', () => {
    it('Can stack things', () => {
      expect(
        stack(pure('a'), pure('b'), pure('c'))
          .firstCycle()
          .map((h) => h.value),
      ).toStrictEqual(['a', 'b', 'c']);
    });
    it('Can stack subpatterns', () => {
      sameFirst(stack('a', ['b', 'c']), stack('a', sequence('b', 'c')));
    });
  });
  describe('_fast()', () => {
    it('Makes things faster', () => {
      expect(pure('a')._fast(2).firstCycle().length).toBe(2);
    });
  });
  describe('_fastGap()', () => {
    it('Makes things faster, with a gap', () => {
      expect(sequence('a', 'b', 'c')._fastGap(2).firstCycle()).toStrictEqual(
        sequence(['a', 'b', 'c'], silence).firstCycle(),
      );
      expect(sequence('a', 'b', 'c')._fastGap(3).firstCycle()).toStrictEqual(
        sequence(['a', 'b', 'c'], silence, silence).firstCycle(),
      );
    });
    it('Makes things faster, with a gap, when speeded up further', () => {
      expect(sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle()).toStrictEqual(
        sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
      );
    });
    it('copes with breaking up events across cycles', () => {
      expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([
        hap(ts(0, 1), ts(0, 0.5), 'a'),
        hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
      ]);
    });
  });
  describe('_compressSpan()', () => {
    it('Can squash cycles of a pattern into a given timespan', () => {
      expect(pure('a')._compressSpan(ts(0.25, 0.5)).firstCycle()).toStrictEqual(
        sequence(silence, 'a', silence, silence).firstCycle(),
      );
    });
  });
  describe('fast()', () => {
    it('Makes things faster', () => {
      expect(pure('a').fast(2).firstCycle().length).toBe(2);
    });
    it('Makes things faster, with a pattern of factors', () => {
      expect(pure('a').fast(sequence(1, 4)).firstCycle().length).toBe(3);
      expect(pure('a').fast(sequence(1, 4)).firstCycle()).toStrictEqual(
        stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
      );
    });
    it('defaults to accepting sequences', () => {
      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', () => {
      expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
        fast(sequence(1, 2), sequence(1, 2, 3)).firstCycle(),
      );
    });
    it('works as a curried static function', () => {
      expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
        fast(sequence(1, 2))(sequence(1, 2, 3)).firstCycle(),
      );
    });
  });
  describe('_slow()', () => {
    it('Makes things slower', () => {
      expect(pure('a')._slow(2).firstCycle()[0]).toStrictEqual(hap(ts(0, 2), ts(0, 1), 'a'));
      const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
      expect(pat.query(st(0, 1))[1]).toStrictEqual(hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
      // the following test fails
      //  assert.deepStrictEqual(
      //   pat.query(ts(1,2))[1], undefined
      // )
      // expecting [c3 eb3] [c3 ~]
      // what happens [c3 eb3] [c3 eb3]
      // notable examples:
      // mini('[c3 g3]/2 eb3') always plays [c3 eb3]
      // mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
    });
    it('Supports zero-length queries', () => {
      expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
    });
  });
  describe('slow()', () => {
    it('Supports zero-length queries', () => {
      expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
        steady('a').setContext({}).queryArc(0, 0),
      );
    });
  });
  describe('inside', () => {
    it('can rev inside a cycle', () => {
      sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev), sequence('b', 'a', 'd', 'c'));
    });
  });
  describe('outside', () => {
    it('can rev outside a cycle', () => {
      sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev), sequence('d', 'c'));
    });
  });
  describe('_filterValues()', () => {
    it('Filters true', () => {
      expect(
        pure(true)
          .filterValues((x) => x)
          .firstCycle().length,
      ).toBe(1);
    });
  });
  describe('when()', () => {
    it('Always faster', () => {
      expect(
        pure('a')
          .when(pure(true), (x) => x._fast(2))
          .firstCycle().length,
      ).toBe(2);
    });
    it('Never faster', () => {
      expect(
        pure('a')
          .when(pure(false), (x) => x._fast(2))
          .firstCycle().length,
      ).toBe(1);
    });
    it('Can alternate', () => {
      expect(
        pure(10)
          .when(slowcat(true, false), (x) => x.add(3))
          .fast(4)
          .sortHapsByPart()
          .firstCycle(),
      ).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
    });
  });
  describe('fastcat()', () => {
    it('Can concatenate two things', () => {
      expect(
        fastcat(pure('a'), pure('b'))
          .firstCycle()
          .map((x) => x.value),
      ).toStrictEqual(['a', 'b']);
    });
  });
  describe('fastcat()', () => {
    it('Can go into negative time', () => {
      sameFirst(fastcat('a', 'b', 'c').late(1000000), fastcat('a', 'b', 'c'));
    });
  });
  describe('slowcat()', () => {
    it('Can concatenate things slowly', () => {
      expect(
        slowcat('a', 'b')
          .firstCycle()
          .map((x) => x.value),
      ).toStrictEqual(['a']);
      expect(
        slowcat('a', 'b')
          ._early(1)
          .firstCycle()
          .map((x) => x.value),
      ).toStrictEqual(['b']);
      expect(
        slowcat('a', slowcat('b', 'c'))
          ._early(1)
          .firstCycle()
          .map((x) => x.value),
      ).toStrictEqual(['b']);
      expect(
        slowcat('a', slowcat('b', 'c'))
          ._early(3)
          .firstCycle()
          .map((x) => x.value),
      ).toStrictEqual(['c']);
    });
    it('Can cat subpatterns', () => {
      sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
    });
  });
  describe('rev()', () => {
    it('Can reverse things', () => {
      expect(
        fastcat('a', 'b', 'c')
          .rev()
          .firstCycle()
          .sort((a, b) => a.part.begin.sub(b.part.begin))
          .map((a) => a.value),
      ).toStrictEqual(['c', 'b', 'a']);
    });
  });
  describe('sequence()', () => {
    it('Can work like fastcat', () => {
      expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
    });
  });
  describe('palindrome()', () => {
    it('Can create palindrome', () => {
      expect(
        fastcat('a', 'b', 'c')
          .palindrome()
          .fast(2)
          .firstCycle()
          .sort((a, b) => a.part.begin.sub(b.part.begin))
          .map((a) => a.value),
      ).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']);
    });
  });
  describe('polyrhythm()', () => {
    it('Can layer up cycles', () => {
      expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
        stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
      );
    });
  });
  describe('polymeter()', () => {
    it('Can layer up cycles, stepwise, with lists', () => {
      expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
        stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
      );
    });
  });
  describe('firstOf()', () => {
    it('Can apply a function every 3rd time', () => {
      expect(
        pure('a')
          .firstOf(3, (x) => x._fast(2))
          ._fast(3)
          .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(),
      );
      expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual(
        sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
      );
      expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
        sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
      );
      expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
        sequence(5, 6, 7, 3, 4, 5).firstCycle(),
      );
    });
  });
  describe('brak()', () => {
    it('Can make something a bit breakbeaty', () => {
      sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
    });
  });
  describe('timeCat()', () => {
    it('Can concatenate patterns with different relative durations', () => {
      expect(sequence('a', ['a', 'a']).firstCycle()).toStrictEqual(
        timeCat([1, 'a'], [0.5, 'a'], [0.5, 'a']).firstCycle(),
      );
    });
  });
  describe('struct()', () => {
    it('Can restructure a discrete pattern', () => {
      expect(
        sequence('a', 'b')
          .struct(sequence(true, true, true))
          .firstCycle(),
      ).toStrictEqual([
        hap(ts(0, third), ts(0, third), 'a'),
        hap(ts(third, twothirds), ts(third, 0.5), 'a'),
        hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
        hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
      ]);
      expect(
        pure('a')
          .struct(sequence(true, [true, false], true))
          .firstCycle(),
      ).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
      expect(
        pure('a')
          .struct(sequence(true, [true, false], true).invert())
          .firstCycle(),
      ).toStrictEqual(sequence(silence, [silence, 'a'], silence).firstCycle());
      expect(
        pure('a')
          .struct(sequence(true, [true, silence], true))
          .firstCycle(),
      ).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
    });
    it('Can structure a continuous pattern', () => {
      expect(steady('a').struct(true, [true, true]).firstCycle()).toStrictEqual(sequence('a', ['a', 'a']).firstCycle());
    });
  });
  describe('mask()', () => {
    it('Can fragment a pattern', () => {
      expect(
        sequence('a', 'b')
          .mask(sequence(true, true, true))
          .firstCycle(),
      ).toStrictEqual([
        hap(ts(0, 0.5), ts(0, third), 'a'),
        hap(ts(0, 0.5), ts(third, 0.5), 'a'),
        hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
        hap(ts(0.5, 1), ts(twothirds, 1), 'b'),
      ]);
    });
    it('Can mask off parts of a pattern', () => {
      expect(sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle()).toStrictEqual(
        sequence(['a', 'b'], silence).firstCycle(),
      );
      expect(sequence('a').mask(sequence(true, false)).firstCycle()).toStrictEqual([hap(ts(0, 1), ts(0, 0.5), 'a')]);
    });
  });
  describe('invert()', () => {
    it('Can invert a binary pattern', () => {
      expect(sequence(true, false, [true, false]).invert().firstCycle()).toStrictEqual(
        sequence(false, true, [false, true]).firstCycle(),
      );
    });
  });
  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(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());
    });
  });
  describe('_setContext()', () => {
    it('Can set the hap context', () => {
      expect(
        pure('a')
          .setContext([
            [
              [0, 1],
              [1, 2],
            ],
          ])
          .firstCycle(true),
      ).toStrictEqual([
        hap(ts(0, 1), ts(0, 1), 'a', [
          [
            [0, 1],
            [1, 2],
          ],
        ]),
      ]);
    });
  });
  describe('_withContext()', () => {
    it('Can update the hap context', () => {
      expect(
        pure('a')
          .setContext([
            [
              [0, 1],
              [1, 2],
            ],
          ])
          .withContext((c) => [
            ...c,
            [
              [3, 4],
              [3, 4],
            ],
          ])
          .firstCycle(true),
      ).toStrictEqual([
        hap(ts(0, 1), ts(0, 1), 'a', [
          [
            [0, 1],
            [1, 2],
          ],
          [
            [3, 4],
            [3, 4],
          ],
        ]),
      ]);
    });
  });
  describe('apply', () => {
    it('Can apply a function', () => {
      expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
    }),
      it('Can apply a pattern of functions', () => {
        expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
        expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual(
          sequence('a', 'b').fast(2, 3).firstCycle(),
        );
      });
  });
  describe('layer', () => {
    it('Can layer up multiple functions', () => {
      expect(
        sequence(1, 2, 3)
          .layer(fast(2), (pat) => pat.add(3, 4))
          .firstCycle(),
      ).toStrictEqual(stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle());
    });
  });
  describe('early', () => {
    it('Can shift a hap earlier', () => {
      expect(pure(30)._late(0.25).query(st(1, 2))).toStrictEqual([
        hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
        hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
      ]);
    });
    it('Can shift a hap earlier, into negative time', () => {
      expect(pure(30)._late(0.25).query(st(0, 1))).toStrictEqual([
        hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
        hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
      ]);
    });
  });
  describe('off', () => {
    it('Can offset a transformed pattern from the original', () => {
      expect(pure(30).off(0.25, add(2)).firstCycle()).toStrictEqual(
        stack(pure(30), pure(30).late(0.25).add(2)).firstCycle(),
      );
    });
  });
  describe('jux', () => {
    it('Can juxtapose', () => {
      expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
        stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
          .sortHapsByPart()
          .firstCycle(),
      );
    });
  });
  describe('juxBy', () => {
    it('Can juxtapose by half', () => {
      expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
        stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
          .sortHapsByPart()
          .firstCycle(),
      );
    });
  });
  describe('_squeezeJoin', () => {
    it('Can squeeze', () => {
      expect(
        sequence('a', ['a', 'a'])
          .fmap((a) => fastcat('b', 'c'))
          .squeezeJoin()
          .firstCycle(),
      ).toStrictEqual(
        sequence(
          ['b', 'c'],
          [
            ['b', 'c'],
            ['b', 'c'],
          ],
        ).firstCycle(),
      );
    });
    it('Squeezes to the correct cycle', () => {
      expect(
        pure(time.struct(true))
          .squeezeJoin()
          .queryArc(3, 4)
          .map((x) => x.value),
      ).toStrictEqual([Fraction(3)]);
    });
  });
  describe('ply', () => {
    it('Can ply(3)', () => {
      expect(sequence('a', ['b', 'c']).ply(3).firstCycle()).toStrictEqual(
        sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
      );
    });
    it('Doesnt drop haps in the 9th cycle', () => {
      // fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
      expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
    });
  });
  describe('striate', () => {
    it('Can striate(2)', () => {
      sameFirst(
        sequence({ sound: 'a' }).striate(2),
        sequence({ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0.5, end: 1 }),
      );
    });
  });
  describe('chop', () => {
    it('Can _chop(2)', () => {
      expect(sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle()).toStrictEqual(
        sequence(
          { sound: 'a', begin: 0, end: 0.5 },
          { sound: 'a', begin: 0.5, end: 1 },
          { sound: 'b', begin: 0, end: 0.5 },
          { sound: 'b', begin: 0.5, end: 1 },
        ).firstCycle(),
      );
    });
    it('Can chop(2,3)', () => {
      expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual(
        sequence(
          [
            { sound: 'a', begin: 0, end: 0.5 },
            { sound: 'a', begin: 0.5, end: 1 },
          ],
          [
            { sound: 'a', begin: 0, end: 1 / 3 },
            { sound: 'a', begin: 1 / 3, end: 2 / 3 },
            { sound: 'a', begin: 2 / 3, end: 1 },
          ],
        )
          .sortHapsByPart()
          .firstCycle(),
      );
    });
    it('Can chop chops', () => {
      expect(pure({ s: 'bev' }).chop(2).chop(2).firstCycle()).toStrictEqual(pure({ s: 'bev' }).chop(4).firstCycle());
    });
  });
  describe('range', () => {
    it('Can be patterned', () => {
      expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
    });
  });
  describe('range2', () => {
    it('Can change the range of a bipolar pattern', () => {
      expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
        sequence(1000, 1025, 1050, 1075).firstCycle(),
      );
    });
  });
  describe('run', () => {
    it('Can run', () => {
      expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
    });
  });
  describe('binaryN', () => {
    it('Can make a binary pattern from a decimal', () => {
      expect(binaryN(55532).firstCycle()).toStrictEqual(
        sequence(1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0).firstCycle(),
      );
    });
    it('Can make a binary pattern from patterned inputs', () => {
      expect(binaryN(pure(0x1337), pure(14)).firstCycle()).toStrictEqual(
        sequence(0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1).firstCycle(),
      );
    });
  });
  describe('ribbon', () => {
    it('Can ribbon', () => {
      expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
        sequence(2, 3, 4, 5).firstCycle(),
      );
    });
  });
  describe('linger', () => {
    it('Can linger on the first quarter of a cycle', () => {
      expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
        sequence(0, 1, 0, 1, 0, 1, 0, 1).firstCycle(),
      );
    });
  });
  describe('alignments', () => {
    it('Can squeeze arguments', () => {
      expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
    });
  });
  describe('defragmentHaps', () => {
    it('Can merge two touching haps with same whole and value', () => {
      expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
    });
    it('Doesnt merge two overlapping haps', () => {
      expect(
        stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1))
          .defragmentHaps()
          .firstCycle().length,
      ).toStrictEqual(2);
    });
    it('Doesnt merge two touching haps with different values', () => {
      expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
    });
    it('Doesnt merge two touching haps with different wholes', () => {
      expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
    });
  });
  describe('press', () => {
    it('Can syncopate events', () => {
      sameFirst(sequence('a', 'b', 'c', 'd').press(), sequence(silence, 'a', silence, 'b', silence, 'c', silence, 'd'));
    });
  });
  describe('hurry', () => {
    it('Can speed up patterns and sounds', () => {
      sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
    });
  });
  /*describe('composable functions', () => {
    it('Can compose functions', () => {
      sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
    });
    it('Can compose by method chaining operators with controls', () => {
      sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
    });
    it('Can compose by method chaining operators and alignments with controls', () => {
      sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
      //      sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
    });
  });
  describe('weave', () => {
    it('Can distribute patterns along a pattern', () => {
      sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
    });
  });
  */
  describe('slice', () => {
    it('Can slice a sample', () => {
      sameFirst(
        s('break').slice(4, sequence(0, 1, 2, 3)),
        sequence(
          { begin: 0, end: 0.25, s: 'break', _slices: 4 },
          { begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
          { begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
          { begin: 0.75, end: 1, s: 'break', _slices: 4 },
        ),
      );
    });
  });
  describe('splice', () => {
    it('Can splice a sample', () => {
      sameFirst(
        s('break').splice(4, sequence(0, 1, 2, 3)),
        sequence(
          { begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
          { begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
          { begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
          { begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
        ),
      );
    });
  });
  describe('chunk', () => {
    it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
      expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
        10, 1, 0, 11, 12, 3, 2, 13,
      ]);
    });
  });
  describe('fastChunk', () => {
    it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
      expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
        10, 1, 2, 13, 10, 1, 2, 13,
      ]);
    });
  });
  describe('repeatCycles', () => {
    it('Repeats each cycle of the source pattern the given number of times', () => {
      expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
    });
  });
  describe('inhabit', () => {
    it('Can pattern named patterns', () => {
      expect(
        sameFirst(
          sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }),
          sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
        ),
      );
    });
    it('Can pattern indexed patterns', () => {
      expect(
        sameFirst(
          sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]),
          sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
        ),
      );
    });
  });
  describe('pick', () => {
    it('Can pattern named patterns', () => {
      expect(
        sameFirst(
          sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }),
          sequence(1, 20, 3, stack(4, 40)),
        ),
      );
    });
    it('Can pattern indexed patterns', () => {
      expect(
        sameFirst(
          sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
          sequence(1, 20, 3, stack(4, 40)),
        ),
      );
    });
    it('Clamps indexes', () => {
      expect(
        sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)),
      );
    });
    it('Is backwards compatible', () => {
      expect(
        sameFirst(
          pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)),
          pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]),
        ),
      );
    });
  });
  describe('pickmod', () => {
    it('Wraps indexes', () => {
      expect(
        sameFirst(
          sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
          sequence(1, 20, 3, 40),
        ),
      );
    });
  });
  describe('_steps', () => {
    it('Is correctly preserved/calculated through transformations', () => {
      expect(sequence(0, 1, 2, 3).linger(4)._steps).toStrictEqual(Fraction(4));
      expect(sequence(0, 1, 2, 3).iter(4)._steps).toStrictEqual(Fraction(4));
      expect(sequence(0, 1, 2, 3).fast(4)._steps).toStrictEqual(Fraction(4));
      expect(sequence(0, 1, 2, 3).hurry(4)._steps).toStrictEqual(Fraction(4));
      expect(sequence(0, 1, 2, 3).rev()._steps).toStrictEqual(Fraction(4));
      expect(sequence(1).segment(10)._steps).toStrictEqual(Fraction(10));
      expect(sequence(1, 0, 1).invert()._steps).toStrictEqual(Fraction(3));
      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4)._steps).toStrictEqual(Fraction(8));
      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4)._steps).toStrictEqual(Fraction(8));
      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3))._steps).toStrictEqual(
        Fraction(4),
      );
      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3))._steps).toStrictEqual(
        Fraction(4),
      );
      expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4)._steps).toStrictEqual(Fraction(12));
      expect(
        pure((x) => x + 1)
          .setSteps(3)
          .appBoth(pure(1).setSteps(2))._steps,
      ).toStrictEqual(Fraction(6));
      expect(
        pure((x) => x + 1)
          .setSteps(undefined)
          .appBoth(pure(1).setSteps(2))._steps,
      ).toStrictEqual(Fraction(2));
      expect(
        pure((x) => x + 1)
          .setSteps(3)
          .appBoth(pure(1).setSteps(undefined))._steps,
      ).toStrictEqual(Fraction(3));
      expect(stack(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(6));
      expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setSteps(undefined))._steps).toStrictEqual(Fraction(3));
      expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4))._steps).toStrictEqual(Fraction(4));
      expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4))._steps).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))._steps).toStrictEqual(Fraction(3));
      expect(fastcat(0, 1).ply(3)._steps).toStrictEqual(Fraction(6));
      expect(fastcat(0, 1).setSteps(undefined).ply(3)._steps).toStrictEqual(undefined);
      expect(fastcat(0, 1).fast(3)._steps).toStrictEqual(Fraction(2));
      expect(fastcat(0, 1).setSteps(undefined).fast(3)._steps).toStrictEqual(undefined);
    });
  });
  describe('stepcat', () => {
    it('can cat', () => {
      expect(sameFirst(stepcat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
      expect(sameFirst(stepcat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
    });
    it('calculates undefined steps as the average', () => {
      expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
    });
    it('works with auto-reified values', () => {
      expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim'))));
    });
  });
  describe('shrink', () => {
    it('can shrink', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).shrink(1), sequence(0, 1, 2, 3, 4, 1, 2, 3, 4, 2, 3, 4, 3, 4, 4)));
    });
    it('can shrink backwards', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).shrink(-1), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
    });
  });
  describe('grow', () => {
    it('can grow', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).grow(1), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
    });
    it('can grow backwards', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).grow(-1), sequence(4, 3, 4, 2, 3, 4, 1, 2, 3, 4, 0, 1, 2, 3, 4)));
    });
  });
  describe('take and drop', () => {
    it('can take from the left', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).take(2), sequence(0, 1)));
    });
    it('can drop from the left', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).drop(2), sequence(2, 3, 4)));
    });
    it('can take from the right', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).take(-2), sequence(3, 4)));
    });
    it('can drop from the right', () => {
      expect(sameFirst(sequence(0, 1, 2, 3, 4).drop(-2), sequence(0, 1, 2)));
    });
    it('can drop nothing', () => {
      expect(sameFirst(pure('a').drop(0), pure('a')));
    });
    it('can drop nothing, repeatedly', () => {
      expect(sameFirst(pure('a').drop(0, 0), fastcat('a', 'a')));
      for (var i = 0; i < 100; ++i) {
        expect(sameFirst(pure('a').drop(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
      }
    });
  });
  describe('expand', () => {
    it('can expand four things in half', () => {
      expect(
        sameFirst(sequence(0, 1, 2, 3).expand(1, 0.5), stepcat(sequence(0, 1, 2, 3), sequence(0, 1, 2, 3).expand(0.5))),
      );
    });
    it('can expand five things in half', () => {
      expect(
        sameFirst(
          sequence(0, 1, 2, 3, 4).expand(1, 0.5),
          stepcat(sequence(0, 1, 2, 3, 4), sequence(0, 1, 2, 3, 4).expand(0.5)),
        ),
      );
    });
  });
  describe('stepJoin', () => {
    it('can join a pattern with steps of 2', () => {
      expect(
        sameFirst(
          sequence(pure(pure('a')), pure(pure('b').setSteps(2))).stepJoin(),
          stepcat(pure('a'), pure('b').setSteps(2)),
        ),
      );
    });
    it('can join a pattern with steps of 0.5', () => {
      expect(
        sameFirst(
          sequence(pure(pure('a')), pure(pure('b').setSteps(0.5))).stepJoin(),
          stepcat(pure('a'), pure('b').setSteps(0.5)),
        ),
      );
    });
  });
  describe('loopAt', () => {
    it('maintains steps', () => {
      expect(s('bev').chop(8).loopAt(2)._steps).toStrictEqual(Fraction(4));
    });
  });
  describe('bite', () => {
    it('works with uneven patterns', () => {
      sameFirst(
        fastcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5))
          .bite(2, stepcat(pure(0), pure(1).expand(2)))
          .fast(5),
        stepcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5).expand(2)).fast(5),
      );
    });
  });
  describe('unjoin', () => {
    it('destructures a pattern into subcycles', () => {
      sameFirst(
        fastcat('a', 'b', 'c', 'd')
          .unjoin(fastcat(true, fastcat(true, true)))
          .fmap(fast(2))
          .join(),
        fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
      );
    });
  });
  describe('into', () => {
    it('applies a function to subcycles of a pattern', () => {
      sameFirst(
        fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
        fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
      );
    });
  });
  describe('chunkinto', () => {
    it('chunks into subcycles', () => {
      sameFirst(
        fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
        fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
      );
    });
  });
  describe('chunkbackinto', () => {
    it('chunks into subcycles backwards', () => {
      sameFirst(
        fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
        fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
      );
    });
  });
  describe('log', () => {
    it('logs to console', () => {
      const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
      const pattern = pure('a').log();
      const haps = pattern.queryArc(0, 1);
      // Force a trigger
      haps.forEach((hap) => {
        hap.context?.onTrigger?.(hap);
      });
      expect(mockConsoleLog).toHaveBeenCalledWith(
        '%c[hap] 0/1 → 1/1: a',
        'background-color: black;color:white;border-radius:15px',
      );
      mockConsoleLog.mockRestore();
    });
  });
  describe('logValues', () => {
    it('logs values to console', () => {
      const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
      const pattern = pure('a').note('c#').logValues();
      const haps = pattern.queryArc(0, 1);
      // Force a trigger
      haps.forEach((hap) => {
        hap.context?.onTrigger?.(hap);
      });
      expect(mockConsoleLog).toHaveBeenCalledWith(
        '%c[hap] value:a note:c#',
        'background-color: black;color:white;border-radius:15px',
      );
      mockConsoleLog.mockRestore();
    });
  });
});
No comments yet.