jevstrudel.git / packages / core / test / pattern.test.mjs
1/*
2pattern.test.mjs - <short description TODO>
3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
4This 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/>.
5*/
6
7import Fraction from 'fraction.js';
8
9import { describe, it, expect, vi } from 'vitest';
10
11import {
12  TimeSpan,
13  Hap,
14  State,
15  Pattern,
16  pure,
17  stack,
18  fastcat,
19  firstOf,
20  slowcat,
21  slowcatPrime,
22  cat,
23  sequence,
24  palindrome,
25  polymeter,
26  polyrhythm,
27  silence,
28  fast,
29  timeCat,
30  add,
31  sub,
32  mul,
33  div,
34  saw,
35  saw2,
36  isaw,
37  isaw2,
38  sine,
39  sine2,
40  square,
41  square2,
42  tri,
43  tri2,
44  id,
45  ply,
46  rev,
47  time,
48  run,
49  randrun,
50  binaryN,
51  pick,
52  stackLeft,
53  stackRight,
54  stackCentre,
55  stepcat,
56  sometimes,
57  expand,
58  arp,
59} from '../index.mjs';
60
61import { log, logValues } from '../pattern.mjs';
62
63import { steady } from '../signal.mjs';
64
65import { n, s } from '../controls.mjs';
66
67const st = (begin, end) => new State(ts(begin, end));
68const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
69const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
70
71const third = Fraction(1, 3);
72const twothirds = Fraction(2, 3);
73
74const sameFirst = (a, b) => {
75  return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
76};
77
78describe('TimeSpan', () => {
79  describe('equals()', () => {
80    it('Should be equal to the same value', () => {
81      expect(new TimeSpan(0, 4).equals(new TimeSpan(0, 4))).toBe(true);
82    });
83  });
84  describe('splitCycles', () => {
85    it('Should split two cycles into two', () => {
86      expect(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length).toBe(2);
87    });
88  });
89  describe('intersection_e', () => {
90    var a = new TimeSpan(Fraction(0), Fraction(2));
91    var b = new TimeSpan(Fraction(1), Fraction(3));
92    var c = new TimeSpan(Fraction(1), Fraction(2));
93    var d = new TimeSpan(Fraction(1), Fraction(2));
94    it('Should create an intersection', () => {
95      expect(a.intersection_e(b).equals(c)).toBe(true);
96    });
97  });
98});
99
100describe('Hap', () => {
101  describe('hasOnset()', () => {
102    it('True if part includes onset from whole', () => {
103      expect(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset()).toBe(true);
104    });
105  });
106  var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'a');
107  var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'b');
108  var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), 'c');
109  var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'd');
110  var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'e');
111  describe('spanEquals', () => {
112    it('True if two haps have the same whole and part', () => {
113      expect(a.spanEquals(b)).toBe(true);
114    });
115    it("False if two haps don't the same whole and part", () => {
116      expect(a.spanEquals(c)).toBe(false);
117    });
118    it('True if two haps have the same part and undefined wholes', () => {
119      expect(d.spanEquals(e)).toBe(true);
120    });
121  });
122  describe('resolveState()', () => {
123    it('Can increment some state', () => {
124      const stateful_value = (state) => {
125        const newValue = state['incrementme'];
126        // TODO Does the state *need* duplicating here?
127        const newState = { ...state };
128        newState['incrementme']++;
129        return [newState, newValue];
130      };
131      const state = { incrementme: 10 };
132      const ev1 = new Hap(ts(0, 1), ts(0, 1), stateful_value, {}, true);
133      const [state2, ev2] = ev1.resolveState(state);
134      const [state3, ev3] = ev1.resolveState(state2);
135      expect(ev3).toStrictEqual(new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
136      expect(state3).toStrictEqual({ incrementme: 12 });
137    });
138  });
139  describe('wholeOrPart()', () => {
140    const ts1 = new TimeSpan(Fraction(0), Fraction(1));
141    const ts0_5 = new TimeSpan(Fraction(0), Fraction(0.5));
142    const continuousHap = new Hap(undefined, ts1, 'hello');
143    const discreteHap = new Hap(ts1, ts0_5, 'hello');
144    it('Can pick a whole', () => {
145      expect(discreteHap.wholeOrPart()).toStrictEqual(ts1);
146    });
147    it('Can pick a part', () => {
148      expect(continuousHap.wholeOrPart()).toStrictEqual(ts1);
149    });
150  });
151});
152describe('Pattern', () => {
153  describe('pure', () => {
154    it('Can make a pattern', () => {
155      expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
156    });
157    it('Supports zero-width queries', () => {
158      expect(pure('hello').queryArc(0, 0).length).toBe(1);
159    });
160  });
161  describe('fmap()', () => {
162    it('Can add things', () => {
163      expect(
164        pure(3)
165          .fmap((x) => x + 4)
166          .firstCycle()[0].value,
167      ).toBe(7);
168    });
169  });
170  describe('out()', () => {
171    it('is an alias for set.out()', () => {
172      sameFirst(sequence(1, 2).out(5, 6, 7, 8), sequence(1, 2).set.out(5, 6, 7, 8));
173    });
174  });
175  describe('add()', () => {
176    it('works as toplevel function', () => {
177      expect(add(pure(4), pure(5)).query(st(0, 1))[0].value).toBe(9);
178    });
179    it('works as toplevel function, with bare values for arguments', () => {
180      expect(add(4, 5).query(st(0, 1))[0].value).toBe(9);
181    });
182    it('can structure In()', () => {
183      expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
184      expect(pure(3).add.in(pure(4)).query(st(0, 1))[0].value).toBe(7);
185    });
186    it('can structure Out()', () => {
187      sameFirst(sequence(1, 2).add.out(4), sequence(5, 6).struct(true));
188    });
189    it('can Mix() structure', () => {
190      expect(sequence(1, 2).add.mix(silence, 5, silence).firstCycle()).toStrictEqual([
191        new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
192        new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
193      ]);
194    });
195    it('can Reset() structure', () => {
196      sameFirst(
197        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
198          .add.reset(20, 30)
199          .early(2),
200        sequence(26, 27, 36, 37),
201      );
202    });
203    it('can Restart() structure', () => {
204      sameFirst(
205        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
206          .add.restart(20, 30)
207          .early(2),
208        sequence(21, 22, 31, 32),
209      );
210    });
211    it('can Squeeze() structure', () => {
212      sameFirst(
213        sequence(1, [2, 3]).add.squeeze(sequence(10, 20, 30)),
214        sequence(
215          [11, 21, 31],
216          [
217            [12, 22, 32],
218            [13, 23, 33],
219          ],
220        ),
221      );
222    });
223    it('can SqueezeOut() structure', () => {
224      sameFirst(
225        sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
226        sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
227      );
228    });
229    it('can add object patterns', () => {
230      sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
231    });
232  });
233  describe('keep()', () => {
234    it('can structure In()', () => {
235      expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
236      expect(pure(3).keep.in(pure(4)).query(st(0, 1))[0].value).toBe(3);
237    });
238    it('can structure Out()', () => {
239      sameFirst(sequence(1, 2).keep.out(4), sequence(1, 2).struct(true));
240    });
241    it('can Mix() structure', () => {
242      expect(sequence(1, 2).keep.mix(silence, 5, silence).firstCycle()).toStrictEqual([
243        new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
244        new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
245      ]);
246    });
247    it('can Reset() structure', () => {
248      sameFirst(
249        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
250          .keep.reset(20, 30)
251          .early(2),
252        sequence(6, 7, 6, 7),
253      );
254    });
255    it('can Restart() structure', () => {
256      sameFirst(
257        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
258          .keep.restart(20, 30)
259          .early(2),
260        sequence(1, 2, 1, 2),
261      );
262    });
263    it('can Squeeze() structure', () => {
264      sameFirst(
265        sequence(1, [2, 3]).keep.squeeze(sequence(10, 20, 30)),
266        sequence(
267          [1, 1, 1],
268          [
269            [2, 2, 2],
270            [3, 3, 3],
271          ],
272        ),
273      );
274    });
275    it('can SqueezeOut() structure', () => {
276      sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
277    });
278  });
279  describe('keepif()', () => {
280    it('can structure In()', () => {
281      sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
282      sameFirst(sequence(3, 4).keepif.in(true, false), sequence(3, silence));
283    });
284    it('can structure Out()', () => {
285      sameFirst(pure(1).keepif.out(true, false), sequence(1, silence));
286    });
287    it('can Mix() structure', () => {
288      expect(sequence(1, 2).keepif.mix(false, true, false).firstCycle()).toStrictEqual([
289        new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
290        new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
291      ]);
292    });
293    it('can Reset() structure', () => {
294      sameFirst(
295        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
296          .keepif.reset(false, true)
297          .early(2),
298        sequence(silence, silence, 6, 7),
299      );
300    });
301    it('can Restart() structure', () => {
302      sameFirst(
303        slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
304          .keepif.restart(false, true)
305          .early(2),
306        sequence(silence, silence, 1, 2),
307      );
308    });
309    it('can Squeeze() structure', () => {
310      sameFirst(
311        sequence(1, [2, 3]).keepif.squeeze(sequence(true, true, false)),
312        sequence(
313          [1, 1, silence],
314          [
315            [2, 2, silence],
316            [3, 3, silence],
317          ],
318        ),
319      );
320    });
321    it('can SqueezeOut() structure', () => {
322      sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
323    });
324  });
325  describe('sub()', () => {
326    it('Can subtract things', () => {
327      expect(pure(3).sub(pure(4)).query(st(0, 1))[0].value).toBe(-1);
328    });
329  });
330  describe('mul()', () => {
331    it('Can multiply things', () => {
332      expect(pure(3).mul(pure(2)).firstCycle()[0].value).toBe(6);
333    });
334  });
335  describe('div()', () => {
336    it('Can divide things', () => {
337      expect(pure(3).div(pure(2)).firstCycle()[0].value).toBe(1.5);
338    });
339  });
340  describe('set()', () => {
341    it('Can set things in objects', () => {
342      expect(
343        pure({ a: 4, b: 6 })
344          .set(pure({ c: 7 }))
345          .firstCycle()[0].value,
346      ).toStrictEqual({
347        a: 4,
348        b: 6,
349        c: 7,
350      });
351
352      sameFirst(
353        sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
354        sequence({ a: 4, b: 2, c: 5 }).fast(3),
355      );
356    });
357    it('Can set things with plain values', () => {
358      sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
359    });
360    describe('setOut()', () => {
361      it('Can set things with structure from second pattern', () => {
362        sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
363      });
364    });
365    describe('setSqueeze()', () => {
366      it('Can squeeze one pattern inside the haps of another', () => {
367        sameFirst(
368          sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
369          sequence(
370            ['a', 'b', 'c'],
371            [
372              ['a', 'b', 'c'],
373              ['a', 'b', 'c'],
374            ],
375          ),
376        );
377        sameFirst(
378          sequence(1, [2, 3]).set.squeeze('a', 'b', 'c'),
379          sequence(
380            ['a', 'b', 'c'],
381            [
382              ['a', 'b', 'c'],
383              ['a', 'b', 'c'],
384            ],
385          ),
386        );
387      });
388    });
389  });
390  describe('stack()', () => {
391    it('Can stack things', () => {
392      expect(
393        stack(pure('a'), pure('b'), pure('c'))
394          .firstCycle()
395          .map((h) => h.value),
396      ).toStrictEqual(['a', 'b', 'c']);
397    });
398    it('Can stack subpatterns', () => {
399      sameFirst(stack('a', ['b', 'c']), stack('a', sequence('b', 'c')));
400    });
401  });
402  describe('_fast()', () => {
403    it('Makes things faster', () => {
404      expect(pure('a')._fast(2).firstCycle().length).toBe(2);
405    });
406  });
407  describe('_fastGap()', () => {
408    it('Makes things faster, with a gap', () => {
409      expect(sequence('a', 'b', 'c')._fastGap(2).firstCycle()).toStrictEqual(
410        sequence(['a', 'b', 'c'], silence).firstCycle(),
411      );
412      expect(sequence('a', 'b', 'c')._fastGap(3).firstCycle()).toStrictEqual(
413        sequence(['a', 'b', 'c'], silence, silence).firstCycle(),
414      );
415    });
416    it('Makes things faster, with a gap, when speeded up further', () => {
417      expect(sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle()).toStrictEqual(
418        sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
419      );
420    });
421    it('copes with breaking up events across cycles', () => {
422      expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([
423        hap(ts(0, 1), ts(0, 0.5), 'a'),
424        hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
425      ]);
426    });
427  });
428  describe('_compressSpan()', () => {
429    it('Can squash cycles of a pattern into a given timespan', () => {
430      expect(pure('a')._compressSpan(ts(0.25, 0.5)).firstCycle()).toStrictEqual(
431        sequence(silence, 'a', silence, silence).firstCycle(),
432      );
433    });
434  });
435  describe('fast()', () => {
436    it('Makes things faster', () => {
437      expect(pure('a').fast(2).firstCycle().length).toBe(2);
438    });
439    it('Makes things faster, with a pattern of factors', () => {
440      expect(pure('a').fast(sequence(1, 4)).firstCycle().length).toBe(3);
441      expect(pure('a').fast(sequence(1, 4)).firstCycle()).toStrictEqual(
442        stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
443      );
444    });
445    it('defaults to accepting sequences', () => {
446      expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual(
447        sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(),
448      );
449    });
450    it('works as a static function', () => {
451      expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
452        fast(sequence(1, 2), sequence(1, 2, 3)).firstCycle(),
453      );
454    });
455    it('works as a curried static function', () => {
456      expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
457        fast(sequence(1, 2))(sequence(1, 2, 3)).firstCycle(),
458      );
459    });
460  });
461
462  describe('_slow()', () => {
463    it('Makes things slower', () => {
464      expect(pure('a')._slow(2).firstCycle()[0]).toStrictEqual(hap(ts(0, 2), ts(0, 1), 'a'));
465
466      const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
467
468      expect(pat.query(st(0, 1))[1]).toStrictEqual(hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
469
470      // the following test fails
471      //  assert.deepStrictEqual(
472      //   pat.query(ts(1,2))[1], undefined
473      // )
474      // expecting [c3 eb3] [c3 ~]
475      // what happens [c3 eb3] [c3 eb3]
476      // notable examples:
477      // mini('[c3 g3]/2 eb3') always plays [c3 eb3]
478      // mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
479    });
480    it('Supports zero-length queries', () => {
481      expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
482    });
483  });
484  describe('slow()', () => {
485    it('Supports zero-length queries', () => {
486      expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
487        steady('a').setContext({}).queryArc(0, 0),
488      );
489    });
490  });
491  describe('inside', () => {
492    it('can rev inside a cycle', () => {
493      sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev), sequence('b', 'a', 'd', 'c'));
494    });
495  });
496  describe('outside', () => {
497    it('can rev outside a cycle', () => {
498      sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev), sequence('d', 'c'));
499    });
500  });
501  describe('_filterValues()', () => {
502    it('Filters true', () => {
503      expect(
504        pure(true)
505          .filterValues((x) => x)
506          .firstCycle().length,
507      ).toBe(1);
508    });
509  });
510  describe('when()', () => {
511    it('Always faster', () => {
512      expect(
513        pure('a')
514          .when(pure(true), (x) => x._fast(2))
515          .firstCycle().length,
516      ).toBe(2);
517    });
518    it('Never faster', () => {
519      expect(
520        pure('a')
521          .when(pure(false), (x) => x._fast(2))
522          .firstCycle().length,
523      ).toBe(1);
524    });
525    it('Can alternate', () => {
526      expect(
527        pure(10)
528          .when(slowcat(true, false), (x) => x.add(3))
529          .fast(4)
530          .sortHapsByPart()
531          .firstCycle(),
532      ).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
533    });
534  });
535  describe('fastcat()', () => {
536    it('Can concatenate two things', () => {
537      expect(
538        fastcat(pure('a'), pure('b'))
539          .firstCycle()
540          .map((x) => x.value),
541      ).toStrictEqual(['a', 'b']);
542    });
543  });
544  describe('fastcat()', () => {
545    it('Can go into negative time', () => {
546      sameFirst(fastcat('a', 'b', 'c').late(1000000), fastcat('a', 'b', 'c'));
547    });
548  });
549  describe('slowcat()', () => {
550    it('Can be empty', () => {
551      expect(slowcat().firstCycle()).toStrictEqual([]);
552    });
553    it('Can concatenate things slowly', () => {
554      expect(
555        slowcat('a', 'b')
556          .firstCycle()
557          .map((x) => x.value),
558      ).toStrictEqual(['a']);
559
560      expect(
561        slowcat('a', 'b')
562          ._early(1)
563          .firstCycle()
564          .map((x) => x.value),
565      ).toStrictEqual(['b']);
566
567      expect(
568        slowcat('a', slowcat('b', 'c'))
569          ._early(1)
570          .firstCycle()
571          .map((x) => x.value),
572      ).toStrictEqual(['b']);
573
574      expect(
575        slowcat('a', slowcat('b', 'c'))
576          ._early(3)
577          .firstCycle()
578          .map((x) => x.value),
579      ).toStrictEqual(['c']);
580    });
581    it('Can cat subpatterns', () => {
582      sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
583    });
584  });
585  describe('slowcatPrime()', () => {
586    it('Can be empty', () => {
587      expect(slowcatPrime().firstCycle()).toStrictEqual([]);
588    });
589    it('Can slowcat patterns swapping back and forth skipping the expected notes', () => {
590      expect(
591        slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
592          .fast(4)
593          .firstCycle()
594          .map((a) => a.value),
595      ).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
596    });
597    it('Can go into negative time', () => {
598      expect(
599        slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
600          .fast(4)
601          .late(8)
602          .firstCycle()
603          .map((a) => a.value),
604      ).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
605    });
606  });
607  describe('arp()', () => {
608    it('It wraps around with both positive and negative indices', () => {
609      expect(
610        stack('a', 'b', 'c')
611          .arp(fastcat(-3, -2, -1, 0, 1, 2, 3, 4))
612          .firstCycle()
613          .map((a) => a.value),
614      ).toStrictEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']);
615    });
616  });
617  describe('rev()', () => {
618    it('Can reverse things', () => {
619      expect(
620        fastcat('a', 'b', 'c')
621          .rev()
622          .firstCycle()
623          .sort((a, b) => a.part.begin.sub(b.part.begin))
624          .map((a) => a.value),
625      ).toStrictEqual(['c', 'b', 'a']);
626    });
627    it('Does not reverse the order of cycles', () => {
628      expect(fastcat('a', 'b', 'c', 'd').slow(2).rev().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
629        fastcat('b', 'a', 'd', 'c').firstCycle(),
630      );
631    });
632  });
633  describe('revv()', () => {
634    it('Does reverse the order of cycles', () => {
635      expect(fastcat('a', 'b', 'c', 'd').slow(2).revv().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
636        fastcat('d', 'c', 'b', 'a').firstCycle(),
637      );
638    });
639  });
640  describe('sequence()', () => {
641    it('Can work like fastcat', () => {
642      expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
643    });
644  });
645  describe('palindrome()', () => {
646    it('Can create palindrome', () => {
647      expect(
648        fastcat('a', 'b', 'c')
649          .palindrome()
650          .fast(2)
651          .firstCycle()
652          .sort((a, b) => a.part.begin.sub(b.part.begin))
653          .map((a) => a.value),
654      ).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']);
655    });
656  });
657  describe('polyrhythm()', () => {
658    it('Can layer up cycles', () => {
659      expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
660        stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
661      );
662    });
663  });
664  describe('polymeter()', () => {
665    it('Can layer up cycles, stepwise, with lists', () => {
666      expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
667        stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
668      );
669    });
670  });
671
672  describe('firstOf()', () => {
673    it('Can apply a function every 3rd time', () => {
674      expect(
675        pure('a')
676          .firstOf(3, (x) => x._fast(2))
677          ._fast(3)
678          .firstCycle(),
679      ).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
680    });
681    it('Works as a toplevel function', () => {
682      expect(firstOf(3, fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
683        sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
684      );
685    });
686    it('Works as a toplevel function, with a patterned first argument', () => {
687      expect(firstOf(pure(3), fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
688        sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
689      );
690    });
691    it('works with currying', () => {
692      expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
693        sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
694      );
695      expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual(
696        sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
697      );
698      expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
699        sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
700      );
701      expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
702        sequence(5, 6, 7, 3, 4, 5).firstCycle(),
703      );
704    });
705  });
706  describe('brak()', () => {
707    it('Can make something a bit breakbeaty', () => {
708      sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
709    });
710  });
711  describe('timeCat()', () => {
712    it('Can concatenate patterns with different relative durations', () => {
713      expect(sequence('a', ['a', 'a']).firstCycle()).toStrictEqual(
714        timeCat([1, 'a'], [0.5, 'a'], [0.5, 'a']).firstCycle(),
715      );
716    });
717  });
718  describe('struct()', () => {
719    it('Can restructure a discrete pattern', () => {
720      expect(
721        sequence('a', 'b')
722          .struct(sequence(true, true, true))
723          .firstCycle(),
724      ).toStrictEqual([
725        hap(ts(0, third), ts(0, third), 'a'),
726        hap(ts(third, twothirds), ts(third, 0.5), 'a'),
727        hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
728        hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
729      ]);
730
731      expect(
732        pure('a')
733          .struct(sequence(true, [true, false], true))
734          .firstCycle(),
735      ).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
736
737      expect(
738        pure('a')
739          .struct(sequence(true, [true, false], true).invert())
740          .firstCycle(),
741      ).toStrictEqual(sequence(silence, [silence, 'a'], silence).firstCycle());
742
743      expect(
744        pure('a')
745          .struct(sequence(true, [true, silence], true))
746          .firstCycle(),
747      ).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
748    });
749    it('Can structure a continuous pattern', () => {
750      expect(steady('a').struct(true, [true, true]).firstCycle()).toStrictEqual(sequence('a', ['a', 'a']).firstCycle());
751    });
752  });
753  describe('mask()', () => {
754    it('Can fragment a pattern', () => {
755      expect(
756        sequence('a', 'b')
757          .mask(sequence(true, true, true))
758          .firstCycle(),
759      ).toStrictEqual([
760        hap(ts(0, 0.5), ts(0, third), 'a'),
761        hap(ts(0, 0.5), ts(third, 0.5), 'a'),
762        hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
763        hap(ts(0.5, 1), ts(twothirds, 1), 'b'),
764      ]);
765    });
766    it('Can mask off parts of a pattern', () => {
767      expect(sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle()).toStrictEqual(
768        sequence(['a', 'b'], silence).firstCycle(),
769      );
770
771      expect(sequence('a').mask(sequence(true, false)).firstCycle()).toStrictEqual([hap(ts(0, 1), ts(0, 0.5), 'a')]);
772    });
773  });
774  describe('invert()', () => {
775    it('Can invert a binary pattern', () => {
776      expect(sequence(true, false, [true, false]).invert().firstCycle()).toStrictEqual(
777        sequence(false, true, [false, true]).firstCycle(),
778      );
779    });
780  });
781  describe('_setContext()', () => {
782    it('Can set the hap context', () => {
783      expect(
784        pure('a')
785          .setContext([
786            [
787              [0, 1],
788              [1, 2],
789            ],
790          ])
791          .firstCycle(true),
792      ).toStrictEqual([
793        hap(ts(0, 1), ts(0, 1), 'a', [
794          [
795            [0, 1],
796            [1, 2],
797          ],
798        ]),
799      ]);
800    });
801  });
802  describe('_withContext()', () => {
803    it('Can update the hap context', () => {
804      expect(
805        pure('a')
806          .setContext([
807            [
808              [0, 1],
809              [1, 2],
810            ],
811          ])
812          .withContext((c) => [
813            ...c,
814            [
815              [3, 4],
816              [3, 4],
817            ],
818          ])
819          .firstCycle(true),
820      ).toStrictEqual([
821        hap(ts(0, 1), ts(0, 1), 'a', [
822          [
823            [0, 1],
824            [1, 2],
825          ],
826          [
827            [3, 4],
828            [3, 4],
829          ],
830        ]),
831      ]);
832    });
833  });
834  describe('apply', () => {
835    it('Can apply a function', () => {
836      expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
837    }),
838      it('Can apply a pattern of functions', () => {
839        expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
840        expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual(
841          sequence('a', 'b').fast(2, 3).firstCycle(),
842        );
843      });
844  });
845  describe('layer', () => {
846    it('Can layer up multiple functions', () => {
847      expect(
848        sequence(1, 2, 3)
849          .layer(fast(2), (pat) => pat.add(3, 4))
850          .firstCycle(),
851      ).toStrictEqual(stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle());
852    });
853  });
854  describe('early', () => {
855    it('Can shift a hap earlier', () => {
856      expect(pure(30)._late(0.25).query(st(1, 2))).toStrictEqual([
857        hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
858        hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
859      ]);
860    });
861    it('Can shift a hap earlier, into negative time', () => {
862      expect(pure(30)._late(0.25).query(st(0, 1))).toStrictEqual([
863        hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
864        hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
865      ]);
866    });
867  });
868  describe('off', () => {
869    it('Can offset a transformed pattern from the original', () => {
870      expect(pure(30).off(0.25, add(2)).firstCycle()).toStrictEqual(
871        stack(pure(30), pure(30).late(0.25).add(2)).firstCycle(),
872      );
873    });
874  });
875  describe('jux', () => {
876    it('Can juxtapose', () => {
877      expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
878        stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
879          .sortHapsByPart()
880          .firstCycle(),
881      );
882    });
883  });
884  describe('juxBy', () => {
885    it('Can juxtapose by half', () => {
886      expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
887        stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
888          .sortHapsByPart()
889          .firstCycle(),
890      );
891    });
892  });
893  describe('_squeezeJoin', () => {
894    it('Can squeeze', () => {
895      expect(
896        sequence('a', ['a', 'a'])
897          .fmap((a) => fastcat('b', 'c'))
898          .squeezeJoin()
899          .firstCycle(),
900      ).toStrictEqual(
901        sequence(
902          ['b', 'c'],
903          [
904            ['b', 'c'],
905            ['b', 'c'],
906          ],
907        ).firstCycle(),
908      );
909    });
910    it('Squeezes to the correct cycle', () => {
911      expect(
912        pure(time.struct(true))
913          .squeezeJoin()
914          .queryArc(3, 4)
915          .map((x) => x.value),
916      ).toStrictEqual([Fraction(3)]);
917    });
918  });
919  describe('ply', () => {
920    it('Can ply(3)', () => {
921      expect(sequence('a', ['b', 'c']).ply(3).firstCycle()).toStrictEqual(
922        sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
923      );
924    });
925    it('Doesnt drop haps in the 9th cycle', () => {
926      // fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
927      expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
928    });
929  });
930  describe('striate', () => {
931    it('Can striate(2)', () => {
932      sameFirst(
933        sequence({ sound: 'a' }).striate(2),
934        sequence({ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0.5, end: 1 }),
935      );
936    });
937  });
938  describe('chop', () => {
939    it('Can _chop(2)', () => {
940      expect(sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle()).toStrictEqual(
941        sequence(
942          { sound: 'a', begin: 0, end: 0.5 },
943          { sound: 'a', begin: 0.5, end: 1 },
944          { sound: 'b', begin: 0, end: 0.5 },
945          { sound: 'b', begin: 0.5, end: 1 },
946        ).firstCycle(),
947      );
948    });
949    it('Can chop(2,3)', () => {
950      expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual(
951        sequence(
952          [
953            { sound: 'a', begin: 0, end: 0.5 },
954            { sound: 'a', begin: 0.5, end: 1 },
955          ],
956          [
957            { sound: 'a', begin: 0, end: 1 / 3 },
958            { sound: 'a', begin: 1 / 3, end: 2 / 3 },
959            { sound: 'a', begin: 2 / 3, end: 1 },
960          ],
961        )
962          .sortHapsByPart()
963          .firstCycle(),
964      );
965    });
966    it('Can chop chops', () => {
967      expect(pure({ s: 'bev' }).chop(2).chop(2).firstCycle()).toStrictEqual(pure({ s: 'bev' }).chop(4).firstCycle());
968    });
969  });
970  describe('range', () => {
971    it('Can be patterned', () => {
972      expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
973    });
974  });
975  describe('range2', () => {
976    it('Can change the range of a bipolar pattern', () => {
977      expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
978        sequence(1000, 1025, 1050, 1075).firstCycle(),
979      );
980    });
981  });
982  describe('run', () => {
983    it('Can run', () => {
984      expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
985    });
986    it('Can go into negative time', () => {
987      expect(run(4).late(1).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
988    });
989  });
990  describe('randrun', () => {
991    it('Can randrun', () => {
992      expect(randrun(4).firstCycle()).toStrictEqual(sequence(2, 1, 3, 0).firstCycle());
993    });
994    it('Can go into negative time', () => {
995      expect(randrun(4).late(1).firstCycle()).toStrictEqual(sequence(1, 2, 0, 3).firstCycle());
996    });
997  });
998  describe('binaryN', () => {
999    it('Can make a binary pattern from a decimal', () => {
1000      expect(binaryN(55532).firstCycle()).toStrictEqual(
1001        sequence(1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0).firstCycle(),
1002      );
1003    });
1004    it('Can make a binary pattern from patterned inputs', () => {
1005      expect(binaryN(pure(0x1337), pure(14)).firstCycle()).toStrictEqual(
1006        sequence(0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1).firstCycle(),
1007      );
1008    });
1009  });
1010  describe('ribbon', () => {
1011    it('Can ribbon', () => {
1012      expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
1013        sequence(2, 3, 4, 5).firstCycle(),
1014      );
1015    });
1016  });
1017  describe('linger', () => {
1018    it('Can linger on the first quarter of a cycle', () => {
1019      expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
1020        sequence(0, 1, 0, 1, 0, 1, 0, 1).firstCycle(),
1021      );
1022    });
1023  });
1024  describe('alignments', () => {
1025    it('Can squeeze arguments', () => {
1026      expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
1027    });
1028  });
1029  describe('defragmentHaps', () => {
1030    it('Can merge two touching haps with same whole and value', () => {
1031      expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
1032    });
1033    it('Doesnt merge two overlapping haps', () => {
1034      expect(
1035        stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1))
1036          .defragmentHaps()
1037          .firstCycle().length,
1038      ).toStrictEqual(2);
1039    });
1040    it('Doesnt merge two touching haps with different values', () => {
1041      expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
1042    });
1043    it('Doesnt merge two touching haps with different wholes', () => {
1044      expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
1045    });
1046  });
1047  describe('press', () => {
1048    it('Can syncopate events', () => {
1049      sameFirst(sequence('a', 'b', 'c', 'd').press(), sequence(silence, 'a', silence, 'b', silence, 'c', silence, 'd'));
1050    });
1051  });
1052  describe('hurry', () => {
1053    it('Can speed up patterns and sounds', () => {
1054      sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
1055    });
1056  });
1057  /*describe('composable functions', () => {
1058    it('Can compose functions', () => {
1059      sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
1060    });
1061    it('Can compose by method chaining operators with controls', () => {
1062      sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
1063    });
1064    it('Can compose by method chaining operators and alignments with controls', () => {
1065      sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
1066      //      sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
1067    });
1068  });
1069  describe('weave', () => {
1070    it('Can distribute patterns along a pattern', () => {
1071      sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
1072    });
1073  });
1074  */
1075  describe('slice', () => {
1076    it('Can slice a sample', () => {
1077      sameFirst(
1078        s('break').slice(4, sequence(0, 1, 2, 3)),
1079        sequence(
1080          { begin: 0, end: 0.25, s: 'break', _slices: 4 },
1081          { begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
1082          { begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
1083          { begin: 0.75, end: 1, s: 'break', _slices: 4 },
1084        ),
1085      );
1086    });
1087  });
1088  describe('splice', () => {
1089    it('Can splice a sample', () => {
1090      sameFirst(
1091        s('break').splice(4, sequence(0, 1, 2, 3)),
1092        sequence(
1093          { begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
1094          { begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
1095          { begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
1096          { begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
1097        ),
1098      );
1099    });
1100  });
1101  describe('chunk', () => {
1102    it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
1103      expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
1104        10, 1, 0, 11, 12, 3, 2, 13,
1105      ]);
1106    });
1107  });
1108  describe('fastChunk', () => {
1109    it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
1110      expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
1111        10, 1, 2, 13, 10, 1, 2, 13,
1112      ]);
1113    });
1114  });
1115  describe('repeatCycles', () => {
1116    it('Repeats each cycle of the source pattern the given number of times', () => {
1117      expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
1118    });
1119  });
1120  describe('inhabit', () => {
1121    it('Can pattern named patterns', () => {
1122      expect(
1123        sameFirst(
1124          sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }),
1125          sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
1126        ),
1127      );
1128    });
1129    it('Can pattern indexed patterns', () => {
1130      expect(
1131        sameFirst(
1132          sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]),
1133          sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
1134        ),
1135      );
1136    });
1137  });
1138  describe('pick', () => {
1139    it('Can pattern named patterns', () => {
1140      expect(
1141        sameFirst(
1142          sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }),
1143          sequence(1, 20, 3, stack(4, 40)),
1144        ),
1145      );
1146    });
1147    it('Can pattern indexed patterns', () => {
1148      expect(
1149        sameFirst(
1150          sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
1151          sequence(1, 20, 3, stack(4, 40)),
1152        ),
1153      );
1154    });
1155    it('Clamps indexes', () => {
1156      expect(
1157        sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)),
1158      );
1159    });
1160    it('Is backwards compatible', () => {
1161      expect(
1162        sameFirst(
1163          pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)),
1164          pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]),
1165        ),
1166      );
1167    });
1168  });
1169  describe('pickmod', () => {
1170    it('Wraps indexes', () => {
1171      expect(
1172        sameFirst(
1173          sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
1174          sequence(1, 20, 3, 40),
1175        ),
1176      );
1177    });
1178  });
1179  describe('_steps', () => {
1180    it('Is correctly preserved/calculated through transformations', () => {
1181      expect(sequence(0, 1, 2, 3).linger(4)._steps).toStrictEqual(Fraction(4));
1182      expect(sequence(0, 1, 2, 3).iter(4)._steps).toStrictEqual(Fraction(4));
1183      expect(sequence(0, 1, 2, 3).fast(4)._steps).toStrictEqual(Fraction(4));
1184      expect(sequence(0, 1, 2, 3).hurry(4)._steps).toStrictEqual(Fraction(4));
1185      expect(sequence(0, 1, 2, 3).rev()._steps).toStrictEqual(Fraction(4));
1186      expect(sequence(1).segment(10)._steps).toStrictEqual(Fraction(10));
1187      expect(sequence(1, 0, 1).invert()._steps).toStrictEqual(Fraction(3));
1188      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4)._steps).toStrictEqual(Fraction(8));
1189      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4)._steps).toStrictEqual(Fraction(8));
1190      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3))._steps).toStrictEqual(
1191        Fraction(4),
1192      );
1193      expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3))._steps).toStrictEqual(
1194        Fraction(4),
1195      );
1196      expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4)._steps).toStrictEqual(Fraction(12));
1197      expect(
1198        pure((x) => x + 1)
1199          .setSteps(3)
1200          .appBoth(pure(1).setSteps(2))._steps,
1201      ).toStrictEqual(Fraction(6));
1202      expect(
1203        pure((x) => x + 1)
1204          .setSteps(undefined)
1205          .appBoth(pure(1).setSteps(2))._steps,
1206      ).toStrictEqual(Fraction(2));
1207      expect(
1208        pure((x) => x + 1)
1209          .setSteps(3)
1210          .appBoth(pure(1).setSteps(undefined))._steps,
1211      ).toStrictEqual(Fraction(3));
1212      expect(stack(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(6));
1213      expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setSteps(undefined))._steps).toStrictEqual(Fraction(3));
1214      expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4))._steps).toStrictEqual(Fraction(4));
1215      expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3));
1216      // maybe this should double when they are either all even or all odd
1217      expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3));
1218      expect(fastcat(0, 1).ply(3)._steps).toStrictEqual(Fraction(6));
1219      expect(fastcat(0, 1).setSteps(undefined).ply(3)._steps).toStrictEqual(undefined);
1220      expect(fastcat(0, 1).fast(3)._steps).toStrictEqual(Fraction(2));
1221      expect(fastcat(0, 1).setSteps(undefined).fast(3)._steps).toStrictEqual(undefined);
1222    });
1223  });
1224  describe('stepcat', () => {
1225    it('can cat', () => {
1226      expect(sameFirst(stepcat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
1227      expect(sameFirst(stepcat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
1228    });
1229    it('calculates undefined steps as the average', () => {
1230      expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
1231    });
1232    it('works with auto-reified values', () => {
1233      expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim'))));
1234    });
1235  });
1236  describe('shrink', () => {
1237    it('can shrink', () => {
1238      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)));
1239    });
1240    it('can shrink backwards', () => {
1241      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)));
1242    });
1243  });
1244  describe('grow', () => {
1245    it('can grow', () => {
1246      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)));
1247    });
1248    it('can grow backwards', () => {
1249      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)));
1250    });
1251  });
1252  describe('take and drop', () => {
1253    it('can take from the left', () => {
1254      expect(sameFirst(sequence(0, 1, 2, 3, 4).take(2), sequence(0, 1)));
1255    });
1256    it('can drop from the left', () => {
1257      expect(sameFirst(sequence(0, 1, 2, 3, 4).drop(2), sequence(2, 3, 4)));
1258    });
1259    it('can take from the right', () => {
1260      expect(sameFirst(sequence(0, 1, 2, 3, 4).take(-2), sequence(3, 4)));
1261    });
1262    it('can drop from the right', () => {
1263      expect(sameFirst(sequence(0, 1, 2, 3, 4).drop(-2), sequence(0, 1, 2)));
1264    });
1265    it('can drop nothing', () => {
1266      expect(sameFirst(pure('a').drop(0), pure('a')));
1267    });
1268    it('can drop nothing, repeatedly', () => {
1269      expect(sameFirst(pure('a').drop(0, 0), fastcat('a', 'a')));
1270      for (var i = 0; i < 100; ++i) {
1271        expect(sameFirst(pure('a').drop(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
1272      }
1273    });
1274  });
1275  describe('expand', () => {
1276    it('can expand four things in half', () => {
1277      expect(
1278        sameFirst(sequence(0, 1, 2, 3).expand(1, 0.5), stepcat(sequence(0, 1, 2, 3), sequence(0, 1, 2, 3).expand(0.5))),
1279      );
1280    });
1281    it('can expand five things in half', () => {
1282      expect(
1283        sameFirst(
1284          sequence(0, 1, 2, 3, 4).expand(1, 0.5),
1285          stepcat(sequence(0, 1, 2, 3, 4), sequence(0, 1, 2, 3, 4).expand(0.5)),
1286        ),
1287      );
1288    });
1289  });
1290  describe('stepJoin', () => {
1291    it('can join a pattern with steps of 2', () => {
1292      expect(
1293        sameFirst(
1294          sequence(pure(pure('a')), pure(pure('b').setSteps(2))).stepJoin(),
1295          stepcat(pure('a'), pure('b').setSteps(2)),
1296        ),
1297      );
1298    });
1299    it('can join a pattern with steps of 0.5', () => {
1300      expect(
1301        sameFirst(
1302          sequence(pure(pure('a')), pure(pure('b').setSteps(0.5))).stepJoin(),
1303          stepcat(pure('a'), pure('b').setSteps(0.5)),
1304        ),
1305      );
1306    });
1307  });
1308  describe('loopAt', () => {
1309    it('maintains steps', () => {
1310      expect(s('bev').chop(8).loopAt(2)._steps).toStrictEqual(Fraction(4));
1311    });
1312  });
1313  describe('bite', () => {
1314    it('works with uneven patterns', () => {
1315      sameFirst(
1316        fastcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5))
1317          .bite(2, stepcat(pure(0), pure(1).expand(2)))
1318          .fast(5),
1319        stepcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5).expand(2)).fast(5),
1320      );
1321    });
1322  });
1323  describe('unjoin', () => {
1324    it('destructures a pattern into subcycles', () => {
1325      sameFirst(
1326        fastcat('a', 'b', 'c', 'd')
1327          .unjoin(fastcat(true, fastcat(true, true)))
1328          .fmap(fast(2))
1329          .join(),
1330        fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
1331      );
1332    });
1333  });
1334  describe('into', () => {
1335    it('applies a function to subcycles of a pattern', () => {
1336      sameFirst(
1337        fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
1338        fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
1339      );
1340    });
1341  });
1342  describe('chunkinto', () => {
1343    it('chunks into subcycles', () => {
1344      sameFirst(
1345        fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
1346        fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
1347      );
1348    });
1349  });
1350  describe('chunkbackinto', () => {
1351    it('chunks into subcycles backwards', () => {
1352      sameFirst(
1353        fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
1354        fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
1355      );
1356    });
1357  });
1358  describe('log', () => {
1359    it('logs to console', () => {
1360      const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
1361      const pattern = pure('a').log();
1362      const haps = pattern.queryArc(0, 1);
1363
1364      // Force a trigger
1365      haps.forEach((hap) => {
1366        hap.context?.onTrigger?.(hap);
1367      });
1368
1369      expect(mockConsoleLog).toHaveBeenCalledWith(
1370        '%c[hap] 0/1 → 1/1: a',
1371        'background-color: black;color:white;border-radius:15px',
1372      );
1373      mockConsoleLog.mockRestore();
1374    });
1375  });
1376  describe('logValues', () => {
1377    it('logs values to console', () => {
1378      const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
1379      const pattern = pure('a').note('c#').logValues();
1380      const haps = pattern.queryArc(0, 1);
1381
1382      // Force a trigger
1383      haps.forEach((hap) => {
1384        hap.context?.onTrigger?.(hap);
1385      });
1386
1387      expect(mockConsoleLog).toHaveBeenCalledWith(
1388        '%c[hap] value:a note:c#',
1389        'background-color: black;color:white;border-radius:15px',
1390      );
1391      mockConsoleLog.mockRestore();
1392    });
1393  });
1394});