Drag Baroque charts onto a line, reorder them, set a duration, or remove a note. The AppImage no longer packages oemer, ONNX, or the models. Version 0.2.0. Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
22 files changed
+569
-2231
No files matched your search
+52
-97
@@ -1,12 +1,18 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fingeringCode, fingeringFor, fingeringMarks, CHART_NOTES } from "@/lib/fingering";
|
||||
import { parseMusicXml } from "@/lib/musicxml";
|
||||
import { DURATION_PRESETS } from "@/lib/duration";
|
||||
import {
|
||||
cloneLine,
|
||||
createNote,
|
||||
dropIndexFromPoint,
|
||||
insertNote,
|
||||
LINE_EXAMPLES,
|
||||
moveNote,
|
||||
removeNote,
|
||||
setDuration,
|
||||
} from "@/lib/line";
|
||||
import { parsePitch } from "@/lib/pitch";
|
||||
import { barStatus, expectedQuarters, extraBeatArrows, leadRows } from "@/lib/score";
|
||||
import { odeToJoy, twinkle } from "@/lib/samples";
|
||||
import { formatTextTab } from "@/lib/text-tab";
|
||||
import { extraBeatArrows } from "@/lib/score";
|
||||
|
||||
const BAROQUE: Record<string, string> = {
|
||||
C5: "0 1 2 3 4 5 6a 6b 7a 7b",
|
||||
@@ -63,7 +69,7 @@ describe("soprano chart", () => {
|
||||
expect(fingeringCode(fingeringFor("Db5", "baroque")!)).toBe(BAROQUE["C#5"]);
|
||||
});
|
||||
|
||||
it("applies only the cited German changes", () => {
|
||||
it("keeps the cited German changes beside the Baroque chart", () => {
|
||||
expect(fingeringCode(fingeringFor("F5", "german")!)).toBe("0 1 2 3 4");
|
||||
expect(fingeringCode(fingeringFor("F#5", "german")!)).toBe("0 1 2 3 5 6a 6b 7a");
|
||||
expect(fingeringCode(fingeringFor("Bb5", "german")!)).toBe("0 1 3 4 6a 6b");
|
||||
@@ -73,98 +79,47 @@ describe("soprano chart", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sample melodies", () => {
|
||||
it("keeps Twinkle in 4/4 with forked F and full bars", () => {
|
||||
expect(twinkle.measures).toHaveLength(12);
|
||||
expect(twinkle.measures[0].events.map((event) => event.pitch)).toEqual(["C5", "C5", "G5", "G5"]);
|
||||
expect(twinkle.measures[2].events[0].pitch).toBe("F5");
|
||||
for (const measure of twinkle.measures) {
|
||||
expect(barStatus(measure, twinkle.timeSignature).ok).toBe(true);
|
||||
}
|
||||
expect(formatTextTab(twinkle, "baroque")).toContain("0 1 2 3 4 6a 6b 7a 7b");
|
||||
describe("fingering line", () => {
|
||||
const quarter = DURATION_PRESETS.find((preset) => preset.label === "quarter")!;
|
||||
const half = DURATION_PRESETS.find((preset) => preset.label === "half")!;
|
||||
|
||||
it("inserts, reorders, removes, and changes duration without touching other notes", () => {
|
||||
const c = createNote("C5", quarter, "c");
|
||||
const e = createNote("E5", quarter, "e");
|
||||
const g = createNote("G5", quarter, "g");
|
||||
const inserted = insertNote(insertNote([c], 1, g), 1, e);
|
||||
expect(inserted.map((note) => note.pitch)).toEqual(["C5", "E5", "G5"]);
|
||||
expect(moveNote(inserted, "c", 3).map((note) => note.pitch)).toEqual(["E5", "G5", "C5"]);
|
||||
expect(moveNote(inserted, "g", 0).map((note) => note.pitch)).toEqual(["G5", "C5", "E5"]);
|
||||
expect(moveNote(inserted, "e", 1).map((note) => note.id)).toEqual(["c", "e", "g"]);
|
||||
const held = setDuration(inserted, "e", half);
|
||||
expect(held.find((note) => note.id === "e")).toMatchObject({ quarters: 2, label: "half" });
|
||||
expect(held.find((note) => note.id === "c")?.quarters).toBe(1);
|
||||
expect(removeNote(held, "e").map((note) => note.pitch)).toEqual(["C5", "G5"]);
|
||||
});
|
||||
|
||||
it("places a drop before the note whose center is to the right of the pointer", () => {
|
||||
const line = { left: 0, right: 400, top: 0, bottom: 200 };
|
||||
expect(dropIndexFromPoint(20, 40, line, [50, 150, 250])).toBe(0);
|
||||
expect(dropIndexFromPoint(80, 40, line, [50, 150, 250])).toBe(1);
|
||||
expect(dropIndexFromPoint(300, 40, line, [50, 150, 250])).toBe(3);
|
||||
expect(dropIndexFromPoint(20, 400, line, [50])).toBe(-1);
|
||||
expect(dropIndexFromPoint(10, 10, line, [])).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps Ode to Joy dotted rhythm inside the bar", () => {
|
||||
expect(expectedQuarters(odeToJoy.timeSignature)).toBe(4);
|
||||
const bar = odeToJoy.measures[3];
|
||||
expect(bar.events.map((event) => event.label)).toEqual(["dotted quarter", "eighth", "half"]);
|
||||
expect(bar.events.reduce((sum, event) => sum + event.quarters, 0)).toBe(4);
|
||||
for (const measure of odeToJoy.measures) {
|
||||
expect(barStatus(measure, odeToJoy.timeSignature).ok).toBe(true);
|
||||
it("offers two short editable examples and an empty start", () => {
|
||||
expect(LINE_EXAMPLES).toHaveLength(2);
|
||||
for (const example of LINE_EXAMPLES) {
|
||||
expect(example.notes.length).toBeGreaterThan(0);
|
||||
expect(example.notes.length).toBeLessThanOrEqual(8);
|
||||
for (const note of example.notes) {
|
||||
expect(fingeringFor(note.pitch, "baroque")).not.toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("lead-line MusicXML", () => {
|
||||
const xml = readFileSync(path.join(__dirname, "fixtures", "lead.musicxml"), "utf8");
|
||||
const parsed = parseMusicXml(xml);
|
||||
|
||||
it("keeps timing, the top note, and the top staff", () => {
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) return;
|
||||
expect(parsed.score.title).toBe("Probe");
|
||||
expect(parsed.score.timeSignature).toEqual({ beats: 4, beatType: 4 });
|
||||
const [first, second, third, fourth] = parsed.score.measures;
|
||||
expect(first.events.map((event) => [event.kind, event.pitch, event.label])).toEqual([
|
||||
["note", "E5", "quarter"],
|
||||
["rest", null, "quarter"],
|
||||
["note", "G5", "dotted quarter"],
|
||||
["note", "A4", "eighth"],
|
||||
]);
|
||||
expect(second.events.map((event) => [event.pitch, event.label, event.tied])).toEqual([
|
||||
["E5", "half", false],
|
||||
[null, "half", false],
|
||||
]);
|
||||
expect(third.events.map((event) => event.pitch)).toEqual(["G5"]);
|
||||
expect(fourth.events.map((event) => event.pitch)).toEqual(["C6", "D6"]);
|
||||
expect(parsed.warnings.join(" ")).toMatch(/first part/);
|
||||
expect(parsed.warnings.join(" ")).toMatch(/lower staves/);
|
||||
expect(parsed.warnings.join(" ")).toMatch(/highest note/);
|
||||
expect(parsed.warnings.join(" ")).toMatch(/Grace notes/);
|
||||
expect(parsed.score.measures.some((measure) => measure.repeatTimes || measure.repeatForward)).toBe(false);
|
||||
});
|
||||
|
||||
it("shows a repeat count only when the barline repeats", () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<score-partwise version="4.0">
|
||||
<part-list><score-part id="P1"><part-name>Lead</part-name></score-part></part-list>
|
||||
<part id="P1">
|
||||
<measure number="1">
|
||||
<barline location="left"><repeat direction="forward"/></barline>
|
||||
<attributes><divisions>1</divisions><time><beats>4</beats><beat-type>4</beat-type></time></attributes>
|
||||
<note><pitch><step>C</step><octave>5</octave></pitch><duration>1</duration><type>quarter</type></note>
|
||||
<note><rest/><duration>3</duration><type>half</type><dot/></note>
|
||||
</measure>
|
||||
<measure number="2">
|
||||
<note><pitch><step>D</step><octave>5</octave></pitch><duration>4</duration><type>whole</type></note>
|
||||
<barline location="right"><repeat direction="backward" times="2"/></barline>
|
||||
</measure>
|
||||
</part>
|
||||
</score-partwise>`;
|
||||
const parsed = parseMusicXml(xml);
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) return;
|
||||
expect(parsed.score.measures[0].repeatForward).toBe(true);
|
||||
expect(parsed.score.measures[0].repeatTimes).toBeUndefined();
|
||||
expect(parsed.score.measures[1].repeatTimes).toBe(2);
|
||||
expect(parsed.score.measures[1].events.map((event) => event.label)).toEqual(["whole"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lead layout", () => {
|
||||
it("counts extra beats without adding an arrow for a dotted quarter", () => {
|
||||
const fourFour = { beats: 4, beatType: 4 };
|
||||
expect(extraBeatArrows(1, fourFour)).toBe(0);
|
||||
expect(extraBeatArrows(0.5, fourFour)).toBe(0);
|
||||
expect(extraBeatArrows(1.5, fourFour)).toBe(0);
|
||||
expect(extraBeatArrows(2, fourFour)).toBe(1);
|
||||
expect(extraBeatArrows(3, fourFour)).toBe(2);
|
||||
expect(extraBeatArrows(4, fourFour)).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps Twinkle in three rows and does not invent a repeat", () => {
|
||||
expect(leadRows(twinkle.measures)).toHaveLength(3);
|
||||
expect(twinkle.measures.some((measure) => measure.repeatTimes)).toBe(false);
|
||||
expect(odeToJoy.measures.some((measure) => measure.repeatTimes)).toBe(false);
|
||||
const copy = cloneLine([...LINE_EXAMPLES[0].notes]);
|
||||
expect(copy.map((note) => note.pitch)).toEqual(LINE_EXAMPLES[0].notes.map((note) => note.pitch));
|
||||
expect(copy[0].id).not.toBe(LINE_EXAMPLES[0].notes[0].id);
|
||||
expect(extraBeatArrows(LINE_EXAMPLES[0].notes[6].quarters, { beats: 4, beatType: 4 })).toBe(1);
|
||||
expect(extraBeatArrows(LINE_EXAMPLES[1].notes[0].quarters, { beats: 4, beatType: 4 })).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user