Add a soprano recorder tab converter as a Linux AppImage.
The app turns a lead line into Baroque fingering tabs with rhythm kept, ships Twinkle and Ode to Joy, and reads photos locally with oemer when the bundled Python is present. Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
55 files changed
+17536
No files matched your search
@@ -0,0 +1,97 @@
|
||||
import { formatQuarterLengths, quartersMatch } from "@/lib/duration";
|
||||
|
||||
export type TimeSignature = {
|
||||
beats: number;
|
||||
beatType: number;
|
||||
};
|
||||
|
||||
export type ScoreEvent = {
|
||||
id: string;
|
||||
kind: "note" | "rest";
|
||||
pitch: string | null;
|
||||
quarters: number;
|
||||
label: string;
|
||||
tied: boolean;
|
||||
};
|
||||
|
||||
export type Measure = {
|
||||
id: string;
|
||||
events: ScoreEvent[];
|
||||
};
|
||||
|
||||
export type ScoreOrigin = "sample" | "upload" | "blank";
|
||||
|
||||
export type Score = {
|
||||
title: string;
|
||||
attribution: string;
|
||||
timeSignature: TimeSignature;
|
||||
measures: Measure[];
|
||||
origin: ScoreOrigin;
|
||||
};
|
||||
|
||||
export const TIME_SIGNATURES: TimeSignature[] = [
|
||||
{ beats: 2, beatType: 4 },
|
||||
{ beats: 3, beatType: 4 },
|
||||
{ beats: 4, beatType: 4 },
|
||||
{ beats: 6, beatType: 8 },
|
||||
{ beats: 3, beatType: 8 },
|
||||
{ beats: 2, beatType: 2 },
|
||||
];
|
||||
|
||||
export function newId(prefix: string): string {
|
||||
return `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
export function signatureKey(timeSignature: TimeSignature): string {
|
||||
return `${timeSignature.beats}/${timeSignature.beatType}`;
|
||||
}
|
||||
|
||||
export function parseSignature(key: string): TimeSignature {
|
||||
const [beats, beatType] = key.split("/").map(Number);
|
||||
return { beats, beatType };
|
||||
}
|
||||
|
||||
export function expectedQuarters(timeSignature: TimeSignature): number {
|
||||
return (timeSignature.beats * 4) / timeSignature.beatType;
|
||||
}
|
||||
|
||||
export function barStatus(
|
||||
measure: Measure,
|
||||
timeSignature: TimeSignature,
|
||||
): { ok: boolean; text: string } {
|
||||
const sum = measure.events.reduce((total, event) => total + event.quarters, 0);
|
||||
const expected = expectedQuarters(timeSignature);
|
||||
const signature = signatureKey(timeSignature);
|
||||
if (quartersMatch(sum, expected)) {
|
||||
return { ok: true, text: `${signature} · ${formatQuarterLengths(sum)}` };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
text: `${formatQuarterLengths(sum)} in this bar; ${signature} holds ${formatQuarterLengths(expected)}.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function startBeats(events: ScoreEvent[], timeSignature: TimeSignature): number[] {
|
||||
const quartersPerBeat = 4 / timeSignature.beatType;
|
||||
let cursor = 0;
|
||||
return events.map((event) => {
|
||||
const beat = cursor / quartersPerBeat + 1;
|
||||
cursor += event.quarters;
|
||||
return beat;
|
||||
});
|
||||
}
|
||||
|
||||
export function formatBeat(beat: number): string {
|
||||
const rounded = Math.round(beat * 100) / 100;
|
||||
return Number.isInteger(rounded) ? String(rounded) : String(rounded);
|
||||
}
|
||||
|
||||
export function blankScore(): Score {
|
||||
return {
|
||||
title: "Untitled line",
|
||||
attribution: "Hand-entered lead line. Add notes, then read the tabs.",
|
||||
timeSignature: { beats: 4, beatType: 4 },
|
||||
origin: "blank",
|
||||
measures: [{ id: newId("m"), events: [] }],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user