From 0506e4a635fa9763617d51fc6d12b9bbf7ea96fe Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 Jan 2024 22:15:00 +0100 Subject: [PATCH] work on transpose: - support octaves - still some bugs - add experimental emitNotes flag to stepInNamedScale --- packages/tonal/test/tonleiter.test.mjs | 16 +++++++- packages/tonal/tonleiter.mjs | 54 ++++++++++++++++++-------- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index e1a693579..b724c5416 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -25,6 +25,7 @@ import { describe('tonleiter', () => { test('Step ', () => { + expect(Step.tokenize('b7')).toEqual(['b', 7]); expect(Step.tokenize('#11')).toEqual(['#', 11]); expect(Step.tokenize('b13')).toEqual(['b', 13]); expect(Step.tokenize('bb6')).toEqual(['bb', 6]); @@ -36,14 +37,25 @@ describe('tonleiter', () => { expect(Step.accidentals('#11')).toEqual(1); }); test('Note', () => { - expect(Note.tokenize('C##')).toEqual(['C', '##']); - expect(Note.tokenize('Bb')).toEqual(['B', 'b']); + expect(Note.tokenize('C3')).toEqual(['C', '', 3]); + expect(Note.tokenize('C##')).toEqual(['C', '##', undefined]); + expect(Note.tokenize('Bb')).toEqual(['B', 'b', undefined]); expect(Note.accidentals('C#')).toEqual(1); expect(Note.accidentals('C##')).toEqual(2); expect(Note.accidentals('Eb')).toEqual(-1); expect(Note.accidentals('Bbb')).toEqual(-2); }); test('transpose', () => { + expect(transpose('Bb4', '4')).toEqual('Eb5'); // fails -> E###########5 + expect(transpose('Bb4', '6')).toEqual('G5'); + + expect(transpose('D3', 'b7')).toEqual('C4'); + expect(transpose('C3', 'b7')).toEqual('Bb3'); + + expect(transpose('C', 'b7')).toEqual('Bb'); + expect(transpose('D', 'b7')).toEqual('C'); + expect(transpose('E', 'b7')).toEqual('D'); + expect(transpose('E', '7')).toEqual('D#'); expect(transpose('F#', '3')).toEqual('A#'); expect(transpose('C', '3')).toEqual('E'); expect(transpose('D', '3')).toEqual('F#'); diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index c8f4bdef7..b2b9255ab 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -101,11 +101,14 @@ export function nearestNumberIndex(target, numbers, preferHigher) { let scaleSteps = {}; // [scaleName]: semitones[] export function stepInNamedScale(step, scale, anchor, preferHigher) { + let emitNotes = false; // true is experimental let [root, scaleName] = Scale.tokenize(scale); const rootMidi = x2midi(root); const rootChroma = midi2chroma(rootMidi); + let intervals; + // TODO: don't use Scale.get, just read from a map { [scaleName]: intervals } + intervals = Scale.get(`C ${scaleName}`).intervals; if (!scaleSteps[scaleName]) { - let { intervals } = Scale.get(`C ${scaleName}`); // cache result scaleSteps[scaleName] = intervals.map(step2semitones); } @@ -113,19 +116,31 @@ export function stepInNamedScale(step, scale, anchor, preferHigher) { if (!steps) { return null; } - let transpose = rootMidi; + let offset = rootMidi; if (anchor) { anchor = x2midi(anchor, 3); const anchorChroma = midi2chroma(anchor); const anchorDiff = _mod(anchorChroma - rootChroma, 12); const zeroIndex = nearestNumberIndex(anchorDiff, steps, preferHigher); step = step + zeroIndex; - transpose = anchor - anchorDiff; + offset = anchor - anchorDiff; } - const octOffset = Math.floor(step / steps.length) * 12; + let octaves = Math.floor(step / steps.length); step = _mod(step, steps.length); - const targetMidi = steps[step] + transpose; - return targetMidi + octOffset; + if (emitNotes) { + // TODO: anchor octave currently has no effect + // this branch is for emitting notes + const interval = interval2step(intervals[step]); + let [pc, acc, oct = 3] = tokenizeNote(root); + // oct += octaves; + const rootWithOctave = pc + acc + oct; + if (anchor) { + // octaves = offset / 12 - oct; + } + let targetNote = transpose(rootWithOctave, interval, octaves); + return targetNote; + } + return steps[step] + offset + octaves * 12; } // different ways to resolve the note to compare the anchor to (see renderVoicing) @@ -182,6 +197,9 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below' const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7]; const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B']; const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; +const intervalSteps = { P: '', M: '', m: 'b', A: '#', d: 'b' }; + +export const interval2step = (interval) => intervalSteps[interval.slice(-1)] + interval.slice(0, -1); export const accidentalOffset = (accidentals) => { return accidentals.split('#').length - accidentals.split('b').length; @@ -214,26 +232,28 @@ export const Step = { export const Note = { // TODO: support octave numbers tokenize(note) { - return [note[0], note.slice(1)]; + return tokenizeNote(note); }, accidentals(note) { return accidentalOffset(this.tokenize(note)[1]); }, }; -// TODO: support octave numbers -export function transpose(note, step) { +export function transpose(note, step, octaveOffset = 0) { // example: E, 3 - const stepNumber = Step.tokenize(step)[1]; // 3 - const noteLetter = Note.tokenize(note)[0]; // E - const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2" - const targetNote = noteLetters[(noteIndex + stepNumber - 1) % 8]; // G "G is a third above E" - const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C" - const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C" + const stepNumber = Step.tokenize(step)[1]; // 3 / 7 + const [noteLetter, acc, noteOctave] = Note.tokenize(note); // E / D + const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2" / 1 + const targetNoteIndex = noteIndex + stepNumber - 1; + const targetNote = noteLetters[targetNoteIndex % 7]; // G "G is a third above E" / "C " + const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C" / 2 + const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C" / 0 const indexOffset = targetIndex - rootIndex; // 3 (E to G is normally a 3 semitones) const stepIndex = steps.indexOf(stepNumber); // 4 ("3" is normally 4 semitones) - const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E" - return [targetNote, offsetAccidentals].join(''); + const accidentalOffset = Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset; + const offsetAccidentals = accidentalString(accidentalOffset % 12); // "we need to add a # to to the G to make it a major third from E" + const targetOctave = noteOctave ? noteOctave + Math.floor(targetNoteIndex / 7) + octaveOffset : ''; + return [targetNote, offsetAccidentals].join('') + targetOctave; } //Note("Bb3").transpose("c3")