mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
Merge pull request 'Feat: Enhance scale function to quantize notes to a named scale' (#1492) from glossing/strudel:glossing/scale-for-notes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1492
This commit is contained in:
@@ -114,7 +114,7 @@ export const { n } = registerControl('n');
|
||||
*
|
||||
* - a letter (a-g or A-G)
|
||||
* - optional accidentals (b or #)
|
||||
* - optional octave number (0-9). Defaults to 3
|
||||
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
||||
*
|
||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||
*
|
||||
@@ -127,6 +127,8 @@ export const { n } = registerControl('n');
|
||||
* note("c4 a4 f4 e4")
|
||||
* @example
|
||||
* note("60 69 65 64")
|
||||
* @example
|
||||
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
|
||||
*/
|
||||
export const { note } = registerControl(['note', 'n']);
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
|
||||
|
||||
// returns true if the given string is a note
|
||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -7,80 +7,130 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
// import { strict as assert } from 'assert';
|
||||
|
||||
import '../tonal.mjs'; // need to import this to add prototypes
|
||||
import { pure, n, seq, note } from '@strudel/core';
|
||||
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mini } from '../../mini/mini.mjs';
|
||||
|
||||
describe('tonal', () => {
|
||||
it('Should run tonal functions ', () => {
|
||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||
describe('scaleTranspose', () => {
|
||||
it('transposes notes by scale degrees', () => {
|
||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||
});
|
||||
});
|
||||
it('scale with plain values', () => {
|
||||
expect(
|
||||
seq(0, 1, 2)
|
||||
.scale('C major')
|
||||
.note()
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
describe('scale', () => {
|
||||
it('converts plain values', () => {
|
||||
expect(
|
||||
seq(0, 1, 2)
|
||||
.scale('C major')
|
||||
.note()
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values (mini notation)', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C:major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values (no tonic)', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values (explicit mini notation)', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale(mini('C:major'))
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts decorated n values', () => {
|
||||
expect(
|
||||
n(seq('0b', '1#', '-2', '3##', '4bb'))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
|
||||
});
|
||||
it('produces silence for mixed sharps and flats', () => {
|
||||
expect(
|
||||
n(seq('0b#', '1#b', '2#b#'))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([]);
|
||||
});
|
||||
it('snaps notes (upwards) to scale', () => {
|
||||
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
|
||||
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
|
||||
|
||||
expect(
|
||||
note(seq(inputNotes))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(expectedNotes);
|
||||
});
|
||||
it('snaps notes to the correct octave', () => {
|
||||
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
|
||||
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
|
||||
|
||||
expect(
|
||||
note(seq(inputNotes))
|
||||
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(expectedNotes);
|
||||
});
|
||||
it('handles scale names provided with colons', () => {
|
||||
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
|
||||
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
|
||||
|
||||
expect(
|
||||
note(seq(inputNotes))
|
||||
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(expectedNotes);
|
||||
});
|
||||
});
|
||||
it('scale with n values', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale with colon', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C:major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale without tonic', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale with mininotation colon', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale(mini('C:major'))
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('transposes note numbers with interval numbers', () => {
|
||||
expect(
|
||||
note(seq(40, 40, 40))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 41, 42]);
|
||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
||||
});
|
||||
it('transposes note numbers with interval strings', () => {
|
||||
expect(
|
||||
note(seq(40, 40, 40))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 42, 43]);
|
||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
||||
});
|
||||
it('transposes note strings with interval numbers', () => {
|
||||
expect(
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'Db', 'D']);
|
||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
||||
});
|
||||
it('transposes note strings with interval strings', () => {
|
||||
expect(
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'D', 'Eb']);
|
||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||
describe('transpose', () => {
|
||||
it('transposes note numbers with interval numbers', () => {
|
||||
expect(
|
||||
note(seq(40, 40, 40))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 41, 42]);
|
||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
||||
});
|
||||
it('transposes note numbers with interval strings', () => {
|
||||
expect(
|
||||
note(seq(40, 40, 40))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 42, 43]);
|
||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
||||
});
|
||||
it('transposes note strings with interval numbers', () => {
|
||||
expect(
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'Db', 'D']);
|
||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
||||
});
|
||||
it('transposes note strings with interval strings', () => {
|
||||
expect(
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'D', 'Eb']);
|
||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+101
-53
@@ -6,19 +6,28 @@ 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/core';
|
||||
import { stepInNamedScale } from './tonleiter.mjs';
|
||||
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||
import { noteToMidi } from '../core/util.mjs';
|
||||
|
||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||
|
||||
function scaleStep(step, scale) {
|
||||
scale = scale.replaceAll(':', ' ');
|
||||
step = Math.ceil(step);
|
||||
let { intervals, tonic, empty } = Scale.get(scale);
|
||||
if ((empty && isNote(scale)) || (empty && !tonic)) {
|
||||
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
|
||||
function getScale(scaleName) {
|
||||
scaleName = scaleName.replaceAll(':', ' ');
|
||||
const scale = Scale.get(scaleName);
|
||||
const { tonic, empty } = scale;
|
||||
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
|
||||
throw new Error(
|
||||
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
|
||||
);
|
||||
} else if (empty) {
|
||||
throw new Error(`invalid scale "${scale}"`);
|
||||
throw new Error(`Invalid scale name "${scaleName}"`);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
function scaleStep(step, scale) {
|
||||
step = Math.ceil(step);
|
||||
let { intervals, tonic } = getScale(scale);
|
||||
tonic = tonic || 'C';
|
||||
const { pc, oct = 3 } = Note.get(tonic);
|
||||
const octaveOffset = Math.floor(step / intervals.length);
|
||||
@@ -30,8 +39,7 @@ function scaleStep(step, scale) {
|
||||
// transpose note inside scale by offset steps
|
||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||
function scaleOffset(scale, offset, note) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
let { notes } = Scale.get(`${tonic} ${scaleName}`);
|
||||
let { notes } = getScale(scale);
|
||||
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
||||
offset = Number(offset);
|
||||
if (isNaN(offset)) {
|
||||
@@ -120,10 +128,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
||||
const interval = !isNaN(Number(intervalOrSemitones))
|
||||
? Interval.fromSemitones(intervalOrSemitones)
|
||||
: String(intervalOrSemitones);
|
||||
// TODO: move simplify to player to preserve enharmonics
|
||||
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
|
||||
// TODO: check if this is still relevant..
|
||||
const targetNote = Note.simplify(Note.transpose(note, interval));
|
||||
const targetNote = Note.transpose(note, interval);
|
||||
if (typeof hap.value === 'object') {
|
||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||
}
|
||||
@@ -171,8 +176,59 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
||||
},
|
||||
);
|
||||
|
||||
// Converts a step value, which is a number optionally decorated with sharps and flats,
|
||||
// to a number and an `offset` number of semitones
|
||||
function _convertStepToNumberAndOffset(step) {
|
||||
let asNumber = Number(step);
|
||||
let offset = 0;
|
||||
if (isNaN(asNumber)) {
|
||||
step = String(step);
|
||||
// Check to see if the step matches the expected format:
|
||||
// - A number (possibly negative)
|
||||
// - Some number of sharps or flats (but not both)
|
||||
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
|
||||
}
|
||||
asNumber = Number(match[1]);
|
||||
// These decorations will determine the semitone offset based on the number of
|
||||
// sharps or flats
|
||||
const decorations = match[2] || '';
|
||||
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
|
||||
}
|
||||
return [asNumber, offset];
|
||||
}
|
||||
|
||||
let scaleToMidisAndNotes = {};
|
||||
// Finds the nearest scale note to `note`
|
||||
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
||||
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
|
||||
if (scaleToMidisAndNotes[scaleName] === undefined) {
|
||||
const { intervals, tonic } = getScale(scaleName);
|
||||
const { pc } = Note.get(tonic);
|
||||
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
|
||||
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
|
||||
const sMidi = sNotes.map(noteToMidi);
|
||||
// Cache
|
||||
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
|
||||
}
|
||||
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
|
||||
const rootMidi = scaleMidis[0];
|
||||
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
|
||||
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
|
||||
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
|
||||
const noteMatch = scaleNotes[noteIdx];
|
||||
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
|
||||
*
|
||||
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
||||
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
|
||||
*
|
||||
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||
*
|
||||
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||
*
|
||||
@@ -191,6 +247,12 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
||||
* n(rand.range(0,12).segment(8))
|
||||
* .scale("C:ritusen")
|
||||
* .s("piano")
|
||||
* @example
|
||||
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
|
||||
* .scale("C:<major minor>/2")
|
||||
* .s("piano")
|
||||
* @example
|
||||
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
||||
*/
|
||||
|
||||
export const scale = register(
|
||||
@@ -204,49 +266,35 @@ export const scale = register(
|
||||
pat
|
||||
.fmap((value) => {
|
||||
const isObject = typeof value === 'object';
|
||||
let step = isObject ? value.n : value;
|
||||
if (isObject) {
|
||||
// The case where the note has been defined via `n` or `pure`
|
||||
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
|
||||
const step = isObject ? (value.n ?? value.value) : value;
|
||||
delete value.n; // remove n so it won't cause trouble
|
||||
}
|
||||
if (isNote(step)) {
|
||||
// legacy..
|
||||
return pure(step);
|
||||
}
|
||||
let asNumber = Number(step);
|
||||
let semitones = 0;
|
||||
if (isNaN(asNumber)) {
|
||||
step = String(step);
|
||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
||||
logger(
|
||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
||||
'error',
|
||||
);
|
||||
if (isNote(step)) {
|
||||
// legacy..
|
||||
return pure(step);
|
||||
}
|
||||
try {
|
||||
const [number, offset] = _convertStepToNumberAndOffset(step);
|
||||
let note;
|
||||
if (isObject && value.anchor) {
|
||||
note = stepInNamedScale(number, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(number, scale);
|
||||
}
|
||||
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
return silence;
|
||||
}
|
||||
const isharp = step.indexOf('#');
|
||||
if (isharp >= 0) {
|
||||
asNumber = Number(step.substring(0, isharp));
|
||||
semitones = step.length - isharp;
|
||||
} else {
|
||||
const iflat = step.indexOf('b');
|
||||
asNumber = Number(step.substring(0, iflat));
|
||||
semitones = iflat - step.length;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
let note;
|
||||
if (isObject && value.anchor) {
|
||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(asNumber, scale);
|
||||
}
|
||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
value = silence;
|
||||
// The case where the note has been defined via `note`
|
||||
else {
|
||||
const note = _getNearestScaleNote(scale, value.note);
|
||||
return pure(isObject ? { ...value, note } : note);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.outerJoin()
|
||||
// legacy:
|
||||
|
||||
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
|
||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||
|
||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||
let [root, scaleName] = Scale.tokenize(scale);
|
||||
const [root, scaleName] = Scale.tokenize(scale);
|
||||
const rootMidi = x2midi(root);
|
||||
const rootChroma = midi2chroma(rootMidi);
|
||||
if (!scaleSteps[scaleName]) {
|
||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
||||
const { intervals } = Scale.get(`C ${scaleName}`);
|
||||
// cache result
|
||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||
}
|
||||
@@ -222,6 +222,7 @@ export const Note = {
|
||||
};
|
||||
|
||||
// TODO: support octave numbers
|
||||
// Example: Note("Bb3").transpose("c3")
|
||||
export function transpose(note, step) {
|
||||
// example: E, 3
|
||||
const stepNumber = Step.tokenize(step)[1]; // 3
|
||||
@@ -235,5 +236,3 @@ export function transpose(note, step) {
|
||||
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('');
|
||||
}
|
||||
|
||||
//Note("Bb3").transpose("c3")
|
||||
|
||||
@@ -3644,6 +3644,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | color:red note:0 ]",
|
||||
"[ 1/4 → 1/2 | color:red note:1 ]",
|
||||
"[ 1/2 → 3/4 | note:E2 ]",
|
||||
"[ 3/4 → 1/1 | note:F2 ]",
|
||||
"[ 1/1 → 5/4 | note:G2 ]",
|
||||
@@ -3652,6 +3654,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
||||
"[ 7/4 → 2/1 | note:C3 ]",
|
||||
"[ 2/1 → 9/4 | note:D3 ]",
|
||||
"[ 9/4 → 5/2 | note:D2 ]",
|
||||
"[ 5/2 → 11/4 | color:red note:2 ]",
|
||||
"[ 11/4 → 3/1 | color:red note:3 ]",
|
||||
"[ 3/1 → 13/4 | note:G2 ]",
|
||||
"[ 13/4 → 7/2 | note:A2 ]",
|
||||
"[ 7/2 → 15/4 | note:B2 ]",
|
||||
@@ -6560,6 +6564,27 @@ exports[`runs examples > example "note" example index 2 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "note" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:fbb1 s:saw ]",
|
||||
"[ 1/4 → 1/2 | note:a#0 s:saw ]",
|
||||
"[ 1/2 → 3/4 | note:cbbb-1 s:saw ]",
|
||||
"[ 3/4 → 1/1 | note:e##-2 s:saw ]",
|
||||
"[ 1/1 → 5/4 | note:fbb1 s:saw ]",
|
||||
"[ 5/4 → 3/2 | note:a#0 s:saw ]",
|
||||
"[ 3/2 → 7/4 | note:cbbb-1 s:saw ]",
|
||||
"[ 7/4 → 2/1 | note:e##-2 s:saw ]",
|
||||
"[ 2/1 → 9/4 | note:fbb1 s:saw ]",
|
||||
"[ 9/4 → 5/2 | note:a#0 s:saw ]",
|
||||
"[ 5/2 → 11/4 | note:cbbb-1 s:saw ]",
|
||||
"[ 11/4 → 3/1 | note:e##-2 s:saw ]",
|
||||
"[ 3/1 → 13/4 | note:fbb1 s:saw ]",
|
||||
"[ 13/4 → 7/2 | note:a#0 s:saw ]",
|
||||
"[ 7/2 → 15/4 | note:cbbb-1 s:saw ]",
|
||||
"[ 15/4 → 4/1 | note:e##-2 s:saw ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||
@@ -8848,6 +8873,108 @@ exports[`runs examples > example "scale" example index 2 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scale" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C3 s:piano ]",
|
||||
"[ 0/1 → 1/4 | note:B3 s:piano ]",
|
||||
"[ 1/4 → 3/8 | note:Gb2 s:piano ]",
|
||||
"[ 3/8 → 1/2 | note:F2 s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:A2 s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:D4 s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:G3 s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:C3 s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:C4 s:piano ]",
|
||||
"[ 5/4 → 11/8 | note:Gb2 s:piano ]",
|
||||
"[ 11/8 → 3/2 | note:E2 s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:A2 s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:Eb4 s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:F#3 s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:C3 s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:B3 s:piano ]",
|
||||
"[ 9/4 → 19/8 | note:Gb2 s:piano ]",
|
||||
"[ 19/8 → 5/2 | note:F2 s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:Ab2 s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:D4 s:piano ]",
|
||||
"[ 11/4 → 3/1 | note:G3 s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:C3 s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:C4 s:piano ]",
|
||||
"[ 13/4 → 27/8 | note:Gb2 s:piano ]",
|
||||
"[ 27/8 → 7/2 | note:E2 s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:Ab2 s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:Eb4 s:piano ]",
|
||||
"[ 15/4 → 4/1 | note:F#3 s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scale" example index 4 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | note:Gb1 ]",
|
||||
"[ 1/16 → 1/8 | note:Gb1 ]",
|
||||
"[ 1/8 → 3/16 | note:Gb1 ]",
|
||||
"[ 3/16 → 1/4 | note:Gb1 ]",
|
||||
"[ 1/4 → 5/16 | note:Gb1 ]",
|
||||
"[ 5/16 → 3/8 | note:Gb1 ]",
|
||||
"[ 3/8 → 7/16 | note:Gb1 ]",
|
||||
"[ 7/16 → 1/2 | note:Gb1 ]",
|
||||
"[ 1/2 → 9/16 | note:Gb1 ]",
|
||||
"[ 9/16 → 5/8 | note:Gb1 ]",
|
||||
"[ 5/8 → 11/16 | note:Gb1 ]",
|
||||
"[ 11/16 → 3/4 | note:Gb1 ]",
|
||||
"[ 3/4 → 13/16 | note:Gb1 ]",
|
||||
"[ 13/16 → 7/8 | note:Gb1 ]",
|
||||
"[ 7/8 → 15/16 | note:Gb1 ]",
|
||||
"[ 15/16 → 1/1 | note:Gb1 ]",
|
||||
"[ 1/1 → 17/16 | note:Cb3 ]",
|
||||
"[ 17/16 → 9/8 | note:Cb3 ]",
|
||||
"[ 9/8 → 19/16 | note:Cb3 ]",
|
||||
"[ 19/16 → 5/4 | note:Cb3 ]",
|
||||
"[ 5/4 → 21/16 | note:Cb3 ]",
|
||||
"[ 21/16 → 11/8 | note:Cb3 ]",
|
||||
"[ 11/8 → 23/16 | note:Cb3 ]",
|
||||
"[ 23/16 → 3/2 | note:Cb3 ]",
|
||||
"[ 3/2 → 25/16 | note:Cb3 ]",
|
||||
"[ 25/16 → 13/8 | note:Cb3 ]",
|
||||
"[ 13/8 → 27/16 | note:Cb3 ]",
|
||||
"[ 27/16 → 7/4 | note:Cb3 ]",
|
||||
"[ 7/4 → 29/16 | note:Cb3 ]",
|
||||
"[ 29/16 → 15/8 | note:Cb3 ]",
|
||||
"[ 15/8 → 31/16 | note:Cb3 ]",
|
||||
"[ 31/16 → 2/1 | note:Cb3 ]",
|
||||
"[ 2/1 → 33/16 | note:Eb4 ]",
|
||||
"[ 33/16 → 17/8 | note:Eb4 ]",
|
||||
"[ 17/8 → 35/16 | note:Eb4 ]",
|
||||
"[ 35/16 → 9/4 | note:Eb4 ]",
|
||||
"[ 9/4 → 37/16 | note:Eb4 ]",
|
||||
"[ 37/16 → 19/8 | note:Eb4 ]",
|
||||
"[ 19/8 → 39/16 | note:Eb4 ]",
|
||||
"[ 39/16 → 5/2 | note:Eb4 ]",
|
||||
"[ 5/2 → 41/16 | note:Eb4 ]",
|
||||
"[ 41/16 → 21/8 | note:Eb4 ]",
|
||||
"[ 21/8 → 43/16 | note:Eb4 ]",
|
||||
"[ 43/16 → 11/4 | note:Eb4 ]",
|
||||
"[ 11/4 → 45/16 | note:Eb4 ]",
|
||||
"[ 45/16 → 23/8 | note:Eb4 ]",
|
||||
"[ 23/8 → 47/16 | note:Eb4 ]",
|
||||
"[ 47/16 → 3/1 | note:Eb4 ]",
|
||||
"[ 3/1 → 49/16 | note:Db2 ]",
|
||||
"[ 49/16 → 25/8 | note:Db2 ]",
|
||||
"[ 25/8 → 51/16 | note:Db2 ]",
|
||||
"[ 51/16 → 13/4 | note:Db2 ]",
|
||||
"[ 13/4 → 53/16 | note:Db2 ]",
|
||||
"[ 53/16 → 27/8 | note:Db2 ]",
|
||||
"[ 27/8 → 55/16 | note:Db2 ]",
|
||||
"[ 55/16 → 7/2 | note:Db2 ]",
|
||||
"[ 7/2 → 57/16 | note:Db2 ]",
|
||||
"[ 57/16 → 29/8 | note:Db2 ]",
|
||||
"[ 29/8 → 59/16 | note:Db2 ]",
|
||||
"[ 59/16 → 15/4 | note:Db2 ]",
|
||||
"[ 15/4 → 61/16 | note:Db2 ]",
|
||||
"[ 61/16 → 31/8 | note:Db2 ]",
|
||||
"[ 31/8 → 63/16 | note:Db2 ]",
|
||||
"[ 63/16 → 4/1 | note:Db2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:C3 ]",
|
||||
|
||||
Reference in New Issue
Block a user