jevstrudel.git / packages / core / test / util.test.mjs
1/*
2util.test.mjs - Tests for the core 'util' module
3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/util.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 { pure } from '../pattern.mjs';
8import {
9  _mod,
10  compose,
11  flatten,
12  fractionalArgs,
13  freqToMidi,
14  getFrequency,
15  getPlayableNoteValue,
16  isNote,
17  midiToFreq,
18  noteToMidi,
19  numeralArgs,
20  parseFractional,
21  parseNumeral,
22  rotate,
23  splitAt,
24  tokenizeNote,
25  zipWith,
26} from '../util.mjs';
27import { describe, expect, it } from 'vitest';
28
29describe('isNote', () => {
30  it('should recognize notes without accidentals', () => {
31    'C3 D3 E3 F3 G3 A3 B3 C4 D5 c5 d5 e5'.split(' ').forEach((note) => {
32      expect(isNote(note)).toBe(true);
33    });
34  });
35  it('should recognize notes with accidentals', () => {
36    'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
37      expect(isNote(note)).toBe(true);
38    });
39  });
40  it('should recognize notes without octave', () => {
41    expect(isNote('C')).toBe(true);
42    expect(isNote('Bb')).toBe(true);
43  });
44  it('should not recognize invalid notes', () => {
45    expect(isNote('H5')).toBe(false);
46    expect(isNote('X')).toBe(false);
47    expect(isNote(1)).toBe(false);
48  });
49});
50
51describe('isNote', () => {
52  it('should tokenize notes without accidentals', () => {
53    expect(tokenizeNote('C3')).toStrictEqual(['C', '', 3]);
54    expect(tokenizeNote('D3')).toStrictEqual(['D', '', 3]);
55    expect(tokenizeNote('E3')).toStrictEqual(['E', '', 3]);
56    expect(tokenizeNote('F3')).toStrictEqual(['F', '', 3]);
57    expect(tokenizeNote('G3')).toStrictEqual(['G', '', 3]);
58    expect(tokenizeNote('A3')).toStrictEqual(['A', '', 3]);
59    expect(tokenizeNote('B3')).toStrictEqual(['B', '', 3]);
60    expect(tokenizeNote('C4')).toStrictEqual(['C', '', 4]);
61    expect(tokenizeNote('D5')).toStrictEqual(['D', '', 5]);
62  });
63  it('should tokenize notes with accidentals', () => {
64    expect(tokenizeNote('C#3')).toStrictEqual(['C', '#', 3]);
65    expect(tokenizeNote('D##3')).toStrictEqual(['D', '##', 3]);
66    expect(tokenizeNote('Eb3')).toStrictEqual(['E', 'b', 3]);
67    expect(tokenizeNote('Fbb3')).toStrictEqual(['F', 'bb', 3]);
68    expect(tokenizeNote('Bb5')).toStrictEqual(['B', 'b', 5]);
69  });
70  it('should tokenize notes without octave', () => {
71    expect(tokenizeNote('C')).toStrictEqual(['C', '', undefined]);
72    expect(tokenizeNote('C#')).toStrictEqual(['C', '#', undefined]);
73    expect(tokenizeNote('Bb')).toStrictEqual(['B', 'b', undefined]);
74    expect(tokenizeNote('Bbb')).toStrictEqual(['B', 'bb', undefined]);
75  });
76  it('should not tokenize invalid notes', () => {
77    expect(tokenizeNote('X')).toStrictEqual([]);
78    expect(tokenizeNote('asfasf')).toStrictEqual([]);
79    expect(tokenizeNote(123)).toStrictEqual([]);
80  });
81});
82describe('noteToMidi', () => {
83  it('should turn notes into midi', () => {
84    expect(noteToMidi('A4')).toEqual(69);
85    expect(noteToMidi('C4')).toEqual(60);
86    expect(noteToMidi('Db4')).toEqual(61);
87    expect(noteToMidi('C3')).toEqual(48);
88    expect(noteToMidi('Cb3')).toEqual(47);
89    expect(noteToMidi('Cbb3')).toEqual(46);
90    expect(noteToMidi('C#3')).toEqual(49);
91    expect(noteToMidi('C#3')).toEqual(49);
92    expect(noteToMidi('C##3')).toEqual(50);
93  });
94  it('should throw an error when given a non-note', () => {
95    expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
96    expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
97  });
98});
99describe('midiToFreq', () => {
100  it('should turn midi into frequency', () => {
101    expect(midiToFreq(69)).toEqual(440);
102    expect(midiToFreq(57)).toEqual(220);
103  });
104});
105describe('freqToMidi', () => {
106  it('should turn frequency into midi', () => {
107    expect(freqToMidi(440)).toEqual(69);
108    expect(freqToMidi(220)).toEqual(57);
109  });
110});
111describe('getFrequency', () => {
112  const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
113  it('should turn note into frequency', () => {
114    expect(getFrequency(happify('a4'))).toEqual(440);
115    expect(getFrequency(happify('a3'))).toEqual(220);
116  });
117  it('should turn midi into frequency', () => {
118    expect(getFrequency(happify(69, { type: 'midi' }))).toEqual(440);
119    expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
120  });
121  it('should return frequencies unchanged', () => {
122    expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
123    expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
124  });
125  it('should turn object with a "freq" property into frequency', () => {
126    expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
127    expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
128  });
129  it('should throw an error when given a non-note', () => {
130    expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
131    expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
132  });
133});
134
135describe('_mod', () => {
136  it('should work like regular modulo with positive numbers', () => {
137    expect(_mod(0, 3)).toEqual(0);
138    expect(_mod(1, 3)).toEqual(1);
139    expect(_mod(2, 3)).toEqual(2);
140    expect(_mod(3, 3)).toEqual(0);
141    expect(_mod(4, 3)).toEqual(1);
142    expect(_mod(4, 2)).toEqual(0);
143  });
144  it('should work with negative numbers', () => {
145    expect(_mod(-1, 3)).toEqual(2);
146    expect(_mod(-2, 3)).toEqual(1);
147    expect(_mod(-3, 3)).toEqual(0);
148    expect(_mod(-4, 3)).toEqual(2);
149    expect(_mod(-5, 3)).toEqual(1);
150    expect(_mod(-3, 2)).toEqual(1);
151  });
152});
153
154describe('compose', () => {
155  const add1 = (a) => a + 1;
156  it('should compose', () => {
157    expect(compose(add1, add1)(0)).toEqual(2);
158    expect(compose(add1)(0)).toEqual(1);
159  });
160  const addS = (s) => (a) => a + s;
161  it('should compose left to right', () => {
162    expect(compose(addS('a'), addS('b'))('')).toEqual('ab');
163    expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
164  });
165});
166
167describe('getPlayableNoteValue', () => {
168  const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
169  it('should return object "note" property', () => {
170    expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
171  });
172  it('should return object "n" property', () => {
173    expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
174  });
175  it('should return object "value" property', () => {
176    expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
177  });
178  it('should turn midi into frequency', () => {
179    expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
180  });
181  it('should return frequency value', () => {
182    expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
183  });
184  it('should throw an error if value is not an object, number, or string', () => {
185    expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
186    expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
187  });
188});
189
190describe('parseNumeral', () => {
191  it('should parse numbers as is', () => {
192    expect(parseNumeral(4)).toBe(4);
193    expect(parseNumeral(0)).toBe(0);
194    expect(parseNumeral(20)).toBe(20);
195    expect(parseNumeral('20')).toBe(20);
196    expect(parseNumeral(1.5)).toBe(1.5);
197  });
198  it('should parse notes', () => {
199    expect(parseNumeral('c')).toBe(48);
200    expect(parseNumeral('c4')).toBe(60);
201    expect(parseNumeral('c#4')).toBe(61);
202    expect(parseNumeral('db4')).toBe(61);
203  });
204  it('should throw an error for unknown strings', () => {
205    expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
206  });
207});
208
209describe('parseFractional', () => {
210  it('should parse numbers as is', () => {
211    expect(parseFractional(4)).toBe(4);
212    expect(parseFractional(0)).toBe(0);
213    expect(parseFractional(20)).toBe(20);
214    expect(parseFractional('20')).toBe(20);
215    expect(parseFractional(1.5)).toBe(1.5);
216  });
217  it('should parse fractional shorthands values', () => {
218    expect(parseFractional('w')).toBe(1);
219    expect(parseFractional('h')).toBe(0.5);
220    expect(parseFractional('q')).toBe(0.25);
221    expect(parseFractional('e')).toBe(0.125);
222  });
223  it('should throw an error for unknown strings', () => {
224    expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
225  });
226});
227
228describe('numeralArgs', () => {
229  it('should convert function arguments to numbers', () => {
230    const add = numeralArgs((a, b) => a + b);
231    expect(add('c4', 2)).toBe(62);
232  });
233});
234describe('fractionalArgs', () => {
235  it('should convert function arguments to numbers', () => {
236    const add = fractionalArgs((a, b) => a + b);
237    expect(add('q', 2)).toBe(2.25);
238  });
239});
240
241describe('rotate', () => {
242  it('should rotate array to the left', () => {
243    expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]);
244    expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]);
245    expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]);
246    expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]);
247    expect(rotate([0], 3)).toStrictEqual([0]);
248    expect(rotate([], 3)).toStrictEqual([]);
249  });
250});
251
252describe('flatten', () => {
253  it('should flatten array by one level', () => {
254    expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]);
255    expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]);
256    expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]);
257    expect(flatten([0])).toStrictEqual([0]);
258    expect(flatten([])).toStrictEqual([]);
259  });
260});
261
262describe('splitAt', () => {
263  it('should split array into two', () => {
264    expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([
265      [0, 1],
266      [2, 3],
267    ]);
268    expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]);
269    expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]);
270    expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]);
271  });
272});
273
274describe('zipWith', () => {
275  it('should use the function to combine the two arrays element-wise', () => {
276    expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]);
277    expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]);
278    expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]);
279    expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]);
280    expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]);
281  });
282});