jevstrudel.git / packages / tonal / test / tonal.test.mjs
1/*
2tonal.test.mjs - <short description TODO>
3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/test/tonal.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
7// import { strict as assert } from 'assert';
8
9import '../tonal.mjs'; // need to import this to add prototypes
10import { pure, n, seq, note, noteToMidi } from '@strudel/core';
11import { describe, it, expect } from 'vitest';
12import { mini } from '../../mini/mini.mjs';
13
14describe('tonal', () => {
15  describe('scaleTranspose', () => {
16    it('transposes notes by scale degrees', () => {
17      expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
18    });
19  });
20  describe('scale', () => {
21    it('converts plain values', () => {
22      expect(
23        seq(0, 1, 2)
24          .scale('C major')
25          .note()
26          .firstCycleValues.map((h) => h.note),
27      ).toEqual(['C3', 'D3', 'E3']);
28    });
29    it('converts n values', () => {
30      expect(
31        n(seq(0, 1, 2))
32          .scale('C major')
33          .firstCycleValues.map((h) => h.note),
34      ).toEqual(['C3', 'D3', 'E3']);
35    });
36    it('converts n values (mini notation)', () => {
37      expect(
38        n(seq(0, 1, 2))
39          .scale('C:major')
40          .firstCycleValues.map((h) => h.note),
41      ).toEqual(['C3', 'D3', 'E3']);
42    });
43    it('converts n values (no tonic)', () => {
44      expect(
45        n(seq(0, 1, 2))
46          .scale('major')
47          .firstCycleValues.map((h) => h.note),
48      ).toEqual(['C3', 'D3', 'E3']);
49    });
50    it('converts n values (explicit mini notation)', () => {
51      expect(
52        n(seq(0, 1, 2))
53          .scale(mini('C:major'))
54          .firstCycleValues.map((h) => h.note),
55      ).toEqual(['C3', 'D3', 'E3']);
56    });
57    it('converts decorated n values', () => {
58      expect(
59        n(seq('0b', '1#', '-2', '3##', '4bb'))
60          .scale('C major')
61          .firstCycleValues.map((h) => h.note),
62      ).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
63    });
64    it('snaps notes (upwards) to scale', () => {
65      const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
66      const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
67
68      expect(
69        note(seq(inputNotes))
70          .scale('C major')
71          .firstCycleValues.map((h) => h.note),
72      ).toEqual(expectedNotes);
73    });
74    it('snaps notes to the correct octave', () => {
75      const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
76      const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
77
78      expect(
79        note(seq(inputNotes))
80          .scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
81          .firstCycleValues.map((h) => h.note),
82      ).toEqual(expectedNotes);
83    });
84    it('handles scale names provided with colons', () => {
85      const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
86      const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
87
88      expect(
89        note(seq(inputNotes))
90          .scale('F#:pentatonic') // F#, G#, A#, C#, and D#
91          .firstCycleValues.map((h) => h.note),
92      ).toEqual(expectedNotes);
93    });
94  });
95  describe('transpose', () => {
96    it('transposes note numbers with interval numbers', () => {
97      expect(
98        note(seq(40, 40, 40))
99          .transpose(0, 1, 2)
100          .firstCycleValues.map((h) => h.note),
101      ).toEqual([40, 41, 42]);
102      expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
103    });
104    it('transposes note numbers with interval strings', () => {
105      expect(
106        note(seq(40, 40, 40))
107          .transpose('1P', '2M', '3m')
108          .firstCycleValues.map((h) => h.note),
109      ).toEqual([40, 42, 43]);
110      expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
111    });
112    it('transposes note strings with interval numbers', () => {
113      expect(
114        note(seq('c', 'c', 'c'))
115          .transpose(0, 1, 2)
116          .firstCycleValues.map((h) => h.note),
117      ).toEqual(['C', 'Db', 'D']);
118      expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
119    });
120    it('transposes note strings with interval strings', () => {
121      expect(
122        note(seq('c', 'c', 'c'))
123          .transpose('1P', '2M', '3m')
124          .firstCycleValues.map((h) => h.note),
125      ).toEqual(['C', 'D', 'Eb']);
126      expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
127    });
128  });
129});