Merge branch 'main' into feature/autocomplete-sound-names

This commit is contained in:
Dr Mathew R Pocock 2025-09-16 02:13:26 +02:00
commit 71dd45cd4b
12 changed files with 383 additions and 130 deletions

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']);

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 [];
}

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 [];
}

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']);
});
});
});

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:

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")

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 ]",

View file

@ -0,0 +1,5 @@
The FontStruction “TIC-80 wide font”
(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed
under a Creative Commons CC0 Public Domain Dedication license
(http://creativecommons.org/publicdomain/zero/1.0/).
[ancestry]

View file

@ -0,0 +1,16 @@
The font file in this archive was created using Fontstruct the free, online
font-building tool.
This font was created by “nesbox”.
This font has a homepage where this archive and other versions may be found:
https://fontstruct.com/fontstructions/show/1388526
[ancestry]
Try Fontstruct at https://fontstruct.com
Its easy and its fun.
Fontstruct is copyright ©2017-2025 Rob Meek
LEGAL NOTICE:
In using this font you must comply with the licensing terms described in the
file “license.txt” included with this archive.
If you redistribute the font file in this archive, it must be accompanied by all
the other files from this archive, including this one.

Binary file not shown.

View file

@ -74,6 +74,7 @@ const fontFamilyOptions = {
FiraCode: 'FiraCode',
'FiraCode-SemiBold': 'FiraCode SemiBold',
teletext: 'teletext',
tic80: 'tic80',
mode7: 'mode7',
BigBlueTerminal: 'BigBlueTerminal',
x3270: 'x3270',

View file

@ -50,6 +50,11 @@
src: url('/fonts/teletext/EuropeanTeletext.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'tic80';
src: url('/fonts/tic80/tic-80-wide-font.otf');
size-adjust: 60%;
}
@font-face {
font-family: 'mode7';
src: url('/fonts/mode7/MODE7GX3.TTF');