Compare commits

..

1 Commits

Author SHA1 Message Date
Felix Roos 0506e4a635 work on transpose:
- support  octaves
- still some bugs
- add experimental emitNotes flag to stepInNamedScale
2024-01-07 22:15:00 +01:00
6 changed files with 54 additions and 65 deletions
-38
View File
@@ -1,38 +0,0 @@
// import { SelectionRange } from '@codemirror/state';
import { RectangleMarker } from '@codemirror/view';
import { layer } from '@codemirror/view';
// currently stuck with: https://discuss.codemirror.net/t/line-background-layer/7666
export const backlayer = layer({
update: (update) => {
return update.docChanged;
},
markers: (view) => {
const offsetTop = 14; // how to know this number ? .view.documentTop is scroll relative..
const offsetLeft = 4; // how to know this number ?
return view.viewportLineBlocks.map((block) => {
const { left } = view.coordsAtPos(block.to);
return new RectangleMarker('cm-backlayer', offsetLeft, block.top + offsetTop, left, block.height);
});
},
});
/* const len = view.state.doc.length;
const markers = RectangleMarker.forRange(
view,
'cm-backlayer',
SelectionRange.fromJSON({
from: 0,
to: len,
anchor: 0,
head: len,
empty: true,
assoc: -1,
bidiLevel: null,
}),
);
console.log('markser', markers);
return markers;
*/
-2
View File
@@ -21,7 +21,6 @@ import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { backlayer } from './backlayer.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -77,7 +76,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
history(),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
backlayer,
Prec.highest(
keymap.of([
{
-1
View File
@@ -3,4 +3,3 @@ export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
export * from './themes.mjs';
export * from './backlayer.mjs';
+14 -2
View File
@@ -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#');
+37 -17
View File
@@ -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")
+3 -5
View File
@@ -10,11 +10,6 @@
--gutterForeground: #8a919966;
}
.cm-backlayer {
background: #00ff0050;
/* background: var(--lineBackground) */
}
.darken::before {
content: ' ';
position: fixed;
@@ -35,6 +30,9 @@
#code .cm-content {
padding-bottom: 50vh;
}
#code .cm-line > * {
background: var(--lineBackground);
}
#code .cm-editor {
background-color: transparent !important;