Skip to content

Commit

Permalink
work on transpose:
Browse files Browse the repository at this point in the history
- support  octaves
- still some bugs
- add experimental emitNotes flag to stepInNamedScale
  • Loading branch information
felixroos committed Jan 7, 2024
1 parent b52cb19 commit 0506e4a
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 19 deletions.
16 changes: 14 additions & 2 deletions packages/tonal/test/tonleiter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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#');
Expand Down
54 changes: 37 additions & 17 deletions packages/tonal/tonleiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,31 +101,46 @@ 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);
}
const steps = scaleSteps[scaleName];
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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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")

0 comments on commit 0506e4a

Please sign in to comment.