From 4d25e3b90b3aa51efdffb64519871aa996bc5f27 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 3 Jan 2024 00:23:28 +0100 Subject: [PATCH 1/5] hotfix: Zach B-B --- website/src/components/Showcase.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/Showcase.jsx b/website/src/components/Showcase.jsx index 2cd0317e..bbdf7785 100644 --- a/website/src/components/Showcase.jsx +++ b/website/src/components/Showcase.jsx @@ -71,7 +71,7 @@ let _videos = [ params: 'start=1278', }, { - title: 'Zach B @ (Algo|Afro) Futures 2023', + title: 'Zach B-B @ (Algo|Afro) Futures 2023', id: 'zUoZvkZ3J7Q', params: 'start=2547', }, From 27049eb706687f5bfabac5ba30bcc5abfaed353b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 4 Jan 2024 11:02:13 +0100 Subject: [PATCH 2/5] add root mode for voicings + allow numbers as anchor --- packages/tonal/tonleiter.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index e2d24b27..afccf259 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -60,12 +60,12 @@ export const step2semitones = (x) => { return Interval.semitones(x); }; -export const x2midi = (x) => { +export const x2midi = (x, defaultOctave) => { if (typeof x === 'number') { return x; } if (typeof x === 'string') { - return noteToMidi(x); + return noteToMidi(x, defaultOctave); } }; @@ -88,13 +88,14 @@ let modeTarget = { below: (v) => v.slice(-1)[0], duck: (v) => v.slice(-1)[0], above: (v) => v[0], + root: (v) => v[0], }; export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) { const [root, symbol] = tokenizeChord(chord); const rootChroma = pc2chroma(root); - anchor = anchor?.note || anchor; - const anchorChroma = pitch2chroma(anchor); + anchor = x2midi(anchor?.note || anchor, 4); + const anchorChroma = midi2chroma(anchor); const voicings = dictionary[symbol].map((voicing) => (typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones), ); @@ -110,18 +111,21 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below' } return diff; }); + if (mode === 'root') { + bestIndex = 0; + } const octDiff = Math.ceil(offset / voicings.length) * 12; const indexWithOffset = _mod(bestIndex + offset, voicings.length); const voicing = voicings[indexWithOffset]; const targetStep = modeTarget[mode](voicing); - const anchorMidi = noteToMidi(anchor, 4) - chromaDiffs[indexWithOffset] + octDiff; + const anchorMidi = anchor - chromaDiffs[indexWithOffset] + octDiff; const voicingMidi = voicing.map((v) => anchorMidi - targetStep + v); let notes = voicingMidi.map((n) => midi2note(n)); if (mode === 'duck') { - notes = notes.filter((_, i) => voicingMidi[i] !== noteToMidi(anchor)); + notes = notes.filter((_, i) => voicingMidi[i] !== anchor); } if (n !== undefined) { return [scaleStep(notes, n, octaves)]; From b27f177138a5342e67482f7b7ffa1d6d01af5e06 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 5 Jan 2024 13:03:40 +0100 Subject: [PATCH 3/5] scales can now be anchored --- packages/tonal/test/tonleiter.test.mjs | 33 ++++++++++++++++++ packages/tonal/tonal.mjs | 8 ++++- packages/tonal/tonleiter.mjs | 47 +++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/tonal/test/tonleiter.test.mjs b/packages/tonal/test/tonleiter.test.mjs index 6c28f37a..e1a69357 100644 --- a/packages/tonal/test/tonleiter.test.mjs +++ b/packages/tonal/test/tonleiter.test.mjs @@ -18,6 +18,9 @@ import { midi2note, renderVoicing, scaleStep, + stepInNamedScale, + nearestNumberIndex, + note2midi, } from '../tonleiter.mjs'; describe('tonleiter', () => { @@ -149,4 +152,34 @@ describe('tonleiter', () => { // expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']); // TODO: test with offset }); + test('nearestNumber', () => { + expect(nearestNumberIndex(0, [0, 2, 4, 5])).toEqual(0); + expect(nearestNumberIndex(1, [0, 2, 4, 5])).toEqual(0); + expect(nearestNumberIndex(1, [0, 2, 4, 5], true)).toEqual(1); + expect(nearestNumberIndex(2, [0, 2, 4, 5])).toEqual(1); + expect(nearestNumberIndex(2, [0, 2, 4, 5]), true).toEqual(1); + expect(nearestNumberIndex(3, [0, 2, 4, 5])).toEqual(1); + expect(nearestNumberIndex(3, [0, 2, 4, 5], true)).toEqual(2); + expect(nearestNumberIndex(4, [0, 2, 4, 5])).toEqual(2); + }); + test('stepInNamedScale', () => { + expect(stepInNamedScale(1, 'D major')).toEqual(note2midi('E3')); + expect(stepInNamedScale(2, 'E major')).toEqual(note2midi('G#3')); + expect(stepInNamedScale(0, 'D major', 'E3')).toEqual(note2midi('E3')); + expect(stepInNamedScale(0, 'D major', 'E4')).toEqual(note2midi('E4')); + expect(stepInNamedScale(0, 'D major', 'Eb4')).toEqual(note2midi('D4')); + expect(stepInNamedScale(0, 'D major', 'F4')).toEqual(note2midi('E4')); + expect(stepInNamedScale(0, 'D major', 'F#4')).toEqual(note2midi('F#4')); + expect(stepInNamedScale(0, 'D major', 'G4')).toEqual(note2midi('G4')); + + expect(stepInNamedScale(0, 'F major', 'F4')).toEqual(note2midi('F4')); + expect(stepInNamedScale(0, 'F major', 'G4')).toEqual(note2midi('G4')); + expect(stepInNamedScale(0, 'F major', 'A4')).toEqual(note2midi('A4')); + expect(stepInNamedScale(0, 'F major', 'Bb4')).toEqual(note2midi('Bb4')); + expect(stepInNamedScale(0, 'F major', 'C4')).toEqual(note2midi('C4')); + + expect(stepInNamedScale(1, 'F major', 'C4')).toEqual(note2midi('D4')); + expect(stepInNamedScale(1, 'F major', 'C3')).toEqual(note2midi('D3')); + expect(stepInNamedScale(1, 'F minor', 'D4')).toEqual(note2midi('Eb4')); + }); }); diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 76bda827..6b831d47 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { Note, Interval, Scale } from '@tonaljs/tonal'; import { register, _mod, silence, logger, pure, isNote } from '@strudel.cycles/core'; +import { stepInNamedScale } from './tonleiter.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; @@ -184,7 +185,12 @@ export const scale = register('scale', function (scale, pat) { return silence; } try { - const note = scaleStep(asNumber, scale); + let note; + if (value.anchor) { + note = stepInNamedScale(asNumber, scale, value.anchor); + } else { + note = scaleStep(asNumber, scale); + } value = pure(isObject ? { ...value, note } : note); } catch (err) { logger(`[tonal] ${err.message}`, 'error'); diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index afccf259..c8f4bdef 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -1,5 +1,5 @@ import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel.cycles/core'; -import { Interval } from '@tonaljs/tonal'; +import { Interval, Scale } from '@tonaljs/tonal'; // https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515 @@ -29,6 +29,7 @@ export function tokenizeChord(chord) { } export const note2pc = (note) => note.match(/^[A-G][#b]?/i)[0]; export const note2oct = (note) => tokenizeNote(note)[2]; +export const note2midi = noteToMidi; export const note2chroma = (note) => { return pc2chroma(note2pc(note)); @@ -83,6 +84,50 @@ export function scaleStep(notes, offset, octaves = 1) { return notes[offset] + octOffset; } +export function nearestNumberIndex(target, numbers, preferHigher) { + let bestIndex = 0, + bestDiff = Infinity; + numbers.forEach((s, i) => { + const diff = Math.abs(s - target); + // preferHigher only works if numbers are sorted in ascending order! + if ((!preferHigher && diff < bestDiff) || (preferHigher && diff <= bestDiff)) { + bestIndex = i; + bestDiff = diff; + } + }); + return bestIndex; +} + +let scaleSteps = {}; // [scaleName]: semitones[] + +export function stepInNamedScale(step, scale, anchor, preferHigher) { + let [root, scaleName] = Scale.tokenize(scale); + const rootMidi = x2midi(root); + const rootChroma = midi2chroma(rootMidi); + 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; + 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; + } + const octOffset = Math.floor(step / steps.length) * 12; + step = _mod(step, steps.length); + const targetMidi = steps[step] + transpose; + return targetMidi + octOffset; +} + // different ways to resolve the note to compare the anchor to (see renderVoicing) let modeTarget = { below: (v) => v.slice(-1)[0], From bcee6632af4792da03ee0cd154c08c4dd5a41e2e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 6 Jan 2024 15:21:58 +0100 Subject: [PATCH 4/5] fix: invisible selection on vim + emacs mode --- packages/codemirror/codemirror.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 8a887036..0d095a4e 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -4,7 +4,14 @@ import { history } from '@codemirror/commands'; import { javascript } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; -import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view'; +import { + EditorView, + highlightActiveLineGutter, + highlightActiveLine, + keymap, + lineNumbers, + drawSelection, +} from '@codemirror/view'; import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core'; import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { isTooltipEnabled } from './tooltip.mjs'; @@ -68,6 +75,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo syntaxHighlighting(defaultHighlightStyle), history(), EditorView.updateListener.of((v) => onChange(v)), + drawSelection({ cursorBlinkRate: 0 }), Prec.highest( keymap.of([ { From 7157634db03db7b673a059b494e8ebcfd26e6e5d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 9 Jan 2024 22:34:16 +0100 Subject: [PATCH 5/5] fix: autocomplete / tooltip code example bug --- packages/codemirror/autocomplete.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index c9ec3d7a..203ab855 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -3,6 +3,12 @@ import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; +function plaintext(str) { + const div = document.createElement('div'); + div.innerText = str; + return div.innerHTML; +} + const getDocLabel = (doc) => doc.name || doc.longname; const getInnerText = (html) => { var div = document.createElement('div'); @@ -21,7 +27,7 @@ ${doc.description} )}
- ${doc.examples?.map((example) => `
${example}
`)} + ${doc.examples?.map((example) => `
${plaintext(example)}
`)}
`[0]; /*