transpose: support alls combinations of numbers and strings for notes and intervals

This commit is contained in:
Felix Roos 2024-04-07 01:25:47 +02:00
parent 558e9e36ff
commit 3d06528393
2 changed files with 61 additions and 10 deletions

View File

@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert'; // import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq } from '@strudel/core'; import { pure, n, seq, note } from '@strudel/core';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs'; import { mini } from '../../mini/mini.mjs';
@ -44,4 +44,36 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note), .firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']); ).toEqual(['C3', 'D3', 'E3']);
}); });
it('transposes note numbers with interval numbers', () => {
expect(
note(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(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('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('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']);
});
}); });

View File

@ -96,18 +96,37 @@ function scaleOffset(scale, offset, note) {
export const transpose = register('transpose', function (intervalOrSemitones, pat) { export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => { return pat.withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones)) const note = hap.value.note ?? hap.value;
? Interval.fromSemitones(intervalOrSemitones /* as number */) if (typeof note === 'number') {
: String(intervalOrSemitones); // note is a number, so just add the number semitones of the interval
if (typeof hap.value === 'number') { let semitones;
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval; if (typeof intervalOrSemitones === 'number') {
return hap.withValue(() => hap.value + semitones); semitones = intervalOrSemitones;
} else if (typeof intervalOrSemitones === 'string') {
semitones = Interval.semitones(intervalOrSemitones) || 0;
}
const targetNote = note + semitones;
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
return hap.withValue(() => targetNote);
} }
if (typeof hap.value === 'object') if (typeof note !== 'string' || !isNote(note)) {
return hap.withValue(() => ({ ...hap.value, note: Note.simplify(Note.transpose(hap.value.note, interval)) })); logger(`[tonal] transpose: not a note "${note}"`, 'warning');
return hap;
}
// note is a string, so we might be able to preserve harmonics if interval is a string as well
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones)
: String(intervalOrSemitones);
// TODO: move simplify to player to preserve enharmonics // TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval))); // TODO: check if this is still relevant..
const targetNote = Note.simplify(Note.transpose(note, interval));
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
return hap.withValue(() => targetNote);
}); });
}); });