From 81b142704c110e0a68a0f80754442a7e009b1f48 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 12 Jul 2023 22:55:34 +0200 Subject: [PATCH] change call signature of renderVoicing + pull logic in from voicings function --- packages/tonal/test/tonleiter.test.mjs | 14 +++++++++----- packages/tonal/tonleiter.mjs | 17 ++++++++++------- packages/tonal/voicings.mjs | 12 ++++-------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index e5d44917..661ef827 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -116,16 +116,20 @@ describe('tonleiter', () => { expect(scaleStep([60, 63, 67], -4)).toBe(43); }); test('renderVoicing', () => { - const voicingDictionary = { + const dictionary = { m7: [ '3 7 10 14', // b3 5 b7 9 '10 14 15 19', // b7 9 b3 5 ], }; - expect(renderVoicing('Em7', 'Bb4', voicingDictionary)).toEqual(['G3', 'B3', 'D4', 'Gb4']); - expect(renderVoicing('Cm7', 'D5', voicingDictionary)).toEqual(['Eb4', 'G4', 'Bb4', 'D5']); - expect(renderVoicing('Cm7', 'G5', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']); - expect(renderVoicing('Cm7', 'g5', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']); + expect(renderVoicing({ chord: 'Em7', anchor: 'Bb4', dictionary })).toEqual(['G3', 'B3', 'D4', 'Gb4']); + expect(renderVoicing({ chord: 'Cm7', anchor: 'D5', dictionary })).toEqual(['Eb4', 'G4', 'Bb4', 'D5']); + expect(renderVoicing({ chord: 'Cm7', anchor: 'G5', dictionary })).toEqual(['Bb4', 'D5', 'Eb5', 'G5']); + expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary })).toEqual(['Bb4', 'D5', 'Eb5', 'G5']); + expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, n: 0 })).toEqual([70]); // Bb4 + expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, n: 1 })).toEqual([74]); // D5 + expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, n: 4 })).toEqual([82]); // Bb5 + expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, offset: 1 })).toEqual(['Eb5', 'G5', 'Bb5', 'D6']); // expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']); // TODO: test with offset }); diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index ee5a4d16..01a6206f 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -42,7 +42,7 @@ export const note2chroma = (note) => { export const midi2chroma = (midi) => midi % 12; // TODO: test and use in voicing function -export const x2chroma = (x) => { +export const pitch2chroma = (x) => { if (isNoteWithOctave(x)) { return note2chroma(x); } @@ -87,19 +87,19 @@ export function scaleStep(notes, offset) { return notes[offset % notes.length] + octOffset; } -export function renderVoicing(chord, anchor, voicingDictionary, offset = 0, n) { +export function renderVoicing({ chord, anchor, dictionary, offset = 0, n, mode }) { + anchor = anchor?.note || anchor || 'c5'; const [root, symbol] = tokenizeChord(chord); - const maxPc = note2pc(anchor); - const maxChroma = pc2chroma(maxPc); + const anchorChroma = pitch2chroma(anchor); const rootChroma = pc2chroma(root); - const voicings = voicingDictionary[symbol].map((voicing) => + const voicings = dictionary[symbol].map((voicing) => (typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones), ); let minDistance, bestIndex; // calculate distances up from voicing top notes let chromaDiffs = voicings.map((v, i) => { - const diff = _mod(maxChroma - v.slice(-1)[0] - rootChroma, 12); + const diff = _mod(anchorChroma - v.slice(-1)[0] - rootChroma, 12); if (minDistance === undefined || diff < minDistance) { minDistance = diff; bestIndex = i; @@ -114,10 +114,13 @@ export function renderVoicing(chord, anchor, voicingDictionary, offset = 0, n) { const topMidi = maxMidi - chromaDiffs[indexWithOffset] + octDiff; const voicingMidi = voicing.map((v) => topMidi - voicing[voicing.length - 1] + v); - const notes = voicingMidi.map((n) => midi2note(n)); + let notes = voicingMidi.map((n) => midi2note(n)); if (n !== undefined) { return [scaleStep(notes, n)]; } + if (mode === 'below') { + notes = notes.filter((n) => x2midi(n) !== note2midi(anchor)); + } return notes; } diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 79722d01..1713dddf 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { stack, register } from '@strudel.cycles/core'; -import { renderVoicing, note2midi, x2midi } from './tonleiter.mjs'; +import { renderVoicing } from './tonleiter.mjs'; import _voicings from 'chord-voicings'; const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup @@ -139,16 +139,12 @@ export const rootNotes = register('rootNotes', function (octave, pat) { export const voicing = register('voicing', function (pat) { return pat .fmap((value) => { - // TODO: default dictionary? - let { anchor, mode, offset, chord, n, dictionary, ...rest } = value; - anchor = anchor?.note || anchor || 'c5'; + let { dictionary, ...rest } = value; if (typeof dictionary === 'string') { dictionary = voicingRegistry[dictionary]?.dictionary; } - let notes = renderVoicing(chord, anchor, dictionary, offset, n); - if (mode === 'below') { - notes = notes.filter((n) => x2midi(n) !== note2midi(anchor)); - } + let notes = renderVoicing({ ...value, dictionary }); + return stack(...notes) .note() .set(rest);