Add new tests, organize old tests, fix issue with note

This commit is contained in:
Aria
2025-08-06 12:37:55 -05:00
parent 7046c1f546
commit a4c040e101
3 changed files with 120 additions and 87 deletions
+102 -69
View File
@@ -7,80 +7,113 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq, note } from '@strudel/core';
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
describe('tonal', () => {
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
describe('scaleTranspose', () => {
it('transposes notes by scale degrees', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
});
it('scale with plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
describe('scale', () => {
it('converts plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (no tonic)', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (explicit mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts decorated n values', () => {
expect(
n(seq('0b', '1#', '-2', '3##', '4bb'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
});
it('produces silence for mixed sharps and flats', () => {
expect(
n(seq('0b#', '1#b', '2#b#'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['', '', '']);
});
it('snaps notes (upwards) to scale', () => {
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
let expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
// Notes are converted to midi by scale
expectedNotes = expectedNotes.map((note) => noteToMidi(note));
expect(
note(seq(inputNotes))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
});
it('scale with n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale without tonic', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
describe('transpose', () => {
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
});
});
+16 -16
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
import { stepInNamedScale, scaleToChromas } from './tonleiter.mjs';
import { noteToMidi } from '../core/util.mjs'
import { noteToMidi } from '../core/util.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -175,25 +175,18 @@ export const { scaleTranspose, scaleTrans, strans } = register(
// Converts a step value, which is a number optionally decorated with sharps and flats,
// to a number and an `offset` number of semitones
function _convertStepToNumberAndOffset(step) {
if (isNote(step)) {
// legacy..
return pure(step);
}
let asNumber = Number(step);
let offset = 0;
if (isNaN(asNumber)) {
step = String(step);
// Check to see if the step matches the expected format:
// - A number (possibly negative)
// - Some number of sharps or flats (but not both)
// - A number (possibly negative)
// - Some number of sharps or flats (but not both)
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
if (!match) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
logger(`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, 'error');
return [silence, 0];
}
asNumber = Number(match[1]);
// These decorations will determine the semitone offset based on the number of
@@ -210,10 +203,13 @@ function _getNearestScaleNote(scaleName, note) {
const octave = (midiNote / 12) >> 0;
const targetChroma = midiNote % 12;
const scaleChromas = scaleToChromas(scaleName);
return scaleChromas.reduce((prev, curr) => {
// Include equality so ties are broken upwards
return Math.abs(curr - targetChroma) <= Math.abs(prev - targetChroma) ? curr : prev;
}) + octave * 12;
return (
scaleChromas.reduce((prev, curr) => {
// Include equality so ties are broken upwards
return Math.abs(curr - targetChroma) <= Math.abs(prev - targetChroma) ? curr : prev;
}) +
octave * 12
);
}
/**
@@ -262,6 +258,10 @@ export const scale = register(
if ((isObject && 'n' in value) || !isObject) {
let step = isObject ? value.n : value;
delete value.n; // remove n so it won't cause trouble
if (isNote(step)) {
// legacy..
return pure(step);
}
let [number, offset] = _convertStepToNumberAndOffset(step);
try {
let note;
+2 -2
View File
@@ -238,7 +238,7 @@ export function transpose(note, step) {
}
// Converts a `scaleName` into a corresponding list of chromas between 0 and 12
export function scaleToChromas (scaleName) {
export function scaleToChromas(scaleName) {
if (Array.isArray(scaleName)) {
scaleName = scaleName.flat().join(' ');
}
@@ -247,5 +247,5 @@ export function scaleToChromas (scaleName) {
const chroma = rootMidi % 12;
const intervals = Scale.get(name).intervals;
const scaleSteps = intervals.map(Interval.semitones);
return scaleSteps.map(s => (s + chroma) % 12);
return scaleSteps.map((s) => (s + chroma) % 12);
}