diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 49395bff..a9f15617 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -573,6 +573,12 @@ const generic_params = [ // TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano // ['velocity'], ['voice'], // TODO: synth param + + // voicings + ['chord'], // https://github.com/tidalcycles/strudel/issues/506 + ['voiceBelow', 'voicebelow'], // https://github.com/tidalcycles/strudel/issues/506 + ['voiceMax', 'voicemax'], // https://github.com/tidalcycles/strudel/issues/506 + /** * Sets the level of reverb. * diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index cfca5289..866d1cbc 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -55,6 +55,8 @@ describe('tonleiter', () => { expect(pc2chroma('D')).toBe(2); expect(pc2chroma('Db')).toBe(1); expect(pc2chroma('Dbb')).toBe(0); + //lowercase + // expect(pc2chroma('c')).toBe(0); // TODO }); test('rotateChroma', () => { expect(rotateChroma(0, 1)).toBe(1); @@ -80,6 +82,7 @@ describe('tonleiter', () => { expect(note2pc('C5')).toBe('C'); // expect(note2pc('C52')).toBe('C'); // <- 2 digits fail expect(note2pc('Bb3')).toBe('Bb'); + //expect(note2pc('F')).toBe('F'); // <- fails }); test('note2oct', () => { expect(note2oct('C5')).toBe(5); @@ -103,10 +106,12 @@ describe('tonleiter', () => { const voicingDictionary = { m7: [ '3 7 10 14', // b3 5 b7 9 - '10 14 5 19', // b7 9 b3 5 + '10 14 15 19', // b7 9 b3 5 ], }; expect(voiceBelow('Bb4', 'Em7', voicingDictionary)).toEqual(['G3', 'B3', 'D4', 'Gb4']); expect(voiceBelow('D5', 'Cm7', voicingDictionary)).toEqual(['Eb4', 'G4', 'Bb4', 'D5']); + expect(voiceBelow('G5', 'Cm7', voicingDictionary)).toEqual(['Bb4', 'D5', 'Eb5', 'G5']); + // expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']); }); }); diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 1e9cdeda..6203a699 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -1,3 +1,5 @@ +import { isNote, isNoteWithOctave } from '@strudel.cycles/core'; + // https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515 const flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; @@ -30,6 +32,27 @@ export const note2midi = (note) => { const [pc, oct] = [note2pc(note), note2oct(note)]; return pc2chroma(pc) + oct * 12 + 12; }; +export const note2chroma = (note) => { + return pc2chroma(note2pc(note)); +}; + +// TODO: test +export const midi2chroma = (midi) => midi % 12; + +// TODO: test and use in voicing function +export const x2chroma = (x) => { + if (isNoteWithOctave(x)) { + return note2chroma(x); + } + if (isNote(x)) { + //pc + return pc2chroma(x); + } + if (typeof x === 'number') { + // expect midi + return midi2chroma(x); + } +}; // duplicate: util.mjs (does not support sharp flag) export const midi2note = (midi, sharp = false) => { diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 07d1229a..8f877394 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -5,6 +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 { voiceBelow, note2midi } from './tonleiter.mjs'; import _voicings from 'chord-voicings'; const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup @@ -129,7 +130,28 @@ export const voicings = register('voicings', function (dictionary, pat) { */ export const rootNotes = register('rootNotes', function (octave, pat) { return pat.fmap((value) => { + value = value.chord || value; const root = value.match(/^([a-gA-G][b#]?).*$/)[1]; return root + octave; }); }); + +export const voicing = register('voicing', function (dictionary, pat) { + return pat + .fmap((value) => { + let { voiceMax: max, voiceBelow: below, chord, ...rest } = value; + let top = max || below; + top = top?.note || top || 'c5'; + if (typeof dictionary === 'string') { + dictionary = voicingRegistry[dictionary]?.dictionary; + } + let notes = voiceBelow(top, chord, dictionary); + if (below) { + notes = notes.filter((n) => note2midi(n) !== note2midi(top)); + } + return stack(...notes) + .note() + .set(rest); + }) + .outerJoin(); +});