From 3d065283932ba5c9a0d9f1b6a530c48f7bac7b4e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 Apr 2024 01:25:47 +0200 Subject: [PATCH] transpose: support alls combinations of numbers and strings for notes and intervals --- packages/tonal/test/tonal.test.mjs | 34 ++++++++++++++++++++++++++- packages/tonal/tonal.mjs | 37 ++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 2b829ecb..8dd0e186 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -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 '../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 { mini } from '../../mini/mini.mjs'; @@ -44,4 +44,36 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).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']); + }); }); diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 16e8fe1c..a2e01092 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -96,18 +96,37 @@ function scaleOffset(scale, offset, note) { export const transpose = register('transpose', function (intervalOrSemitones, pat) { return pat.withHap((hap) => { - const interval = !isNaN(Number(intervalOrSemitones)) - ? Interval.fromSemitones(intervalOrSemitones /* as number */) - : String(intervalOrSemitones); - if (typeof hap.value === 'number') { - const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval; - return hap.withValue(() => hap.value + semitones); + const note = hap.value.note ?? hap.value; + if (typeof note === 'number') { + // note is a number, so just add the number semitones of the interval + let semitones; + if (typeof intervalOrSemitones === 'number') { + 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') - return hap.withValue(() => ({ ...hap.value, note: Note.simplify(Note.transpose(hap.value.note, interval)) })); + if (typeof note !== 'string' || !isNote(note)) { + 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 // 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); }); });