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});