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:
froos
2025-09-15 23:36:30 +02:00
7 changed files with 356 additions and 130 deletions
+3 -1
View File
@@ -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']);
+2 -2
View File
@@ -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 [];
}
+1 -1
View File
@@ -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 [];
}
+119 -69
View File
@@ -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
View File
@@ -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:
+3 -4
View File
@@ -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")
+127
View File
@@ -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 ]",