mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 05:38:35 +00:00
scales can now be anchored
This commit is contained in:
parent
f37652edb3
commit
b27f177138
@ -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'));
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user