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,195 @@
|
||||
import { parsePitch } from "@/lib/pitch";
|
||||
|
||||
export type HoleState = "open" | "closed" | "half";
|
||||
|
||||
export type Fingering = {
|
||||
thumb: HoleState;
|
||||
/** Holes 1, 2, 3, 4, 5, 6a, 6b, 7a, 7b. */
|
||||
holes: HoleState[];
|
||||
bell: boolean;
|
||||
};
|
||||
|
||||
export type FingeringSystem = "baroque" | "german";
|
||||
|
||||
export const RANGE_LABEL = "C5–D7";
|
||||
export const LOW_MIDI = 72;
|
||||
export const HIGH_MIDI = 98;
|
||||
|
||||
const HOLE_IDS = ["1", "2", "3", "4", "5", "6a", "6b", "7a", "7b"] as const;
|
||||
|
||||
type ThumbCode = "0" | "x" | "-";
|
||||
type Row = [pitch: string, thumb: ThumbCode, holes: string, bell?: boolean];
|
||||
|
||||
/**
|
||||
* First-choice Baroque (English) fingerings from the Dolmetsch Online chart
|
||||
* for recorders in C (Brian Blood). Octave 1 is concert C5.
|
||||
* A listed hole is closed. 6a without 6b, or 7a without 7b, is the half-hole.
|
||||
* Thumb 0 is closed, x is pinched, - is open.
|
||||
* Alternates on that chart (second B, all-closed D, and so on) are not encoded.
|
||||
*/
|
||||
const BAROQUE_ROWS: Row[] = [
|
||||
["C5", "0", "123456a6b7a7b"],
|
||||
["C#5", "0", "123456a6b7a"],
|
||||
["D5", "0", "123456a6b"],
|
||||
["Eb5", "0", "123456a"],
|
||||
["E5", "0", "12345"],
|
||||
["F5", "0", "12346a6b7a7b"],
|
||||
["F#5", "0", "12356a6b"],
|
||||
["G5", "0", "123"],
|
||||
["Ab5", "0", "12456a"],
|
||||
["A5", "0", "12"],
|
||||
["Bb5", "0", "134"],
|
||||
["B5", "0", "1"],
|
||||
["C6", "0", "2"],
|
||||
["C#6", "-", "12"],
|
||||
["D6", "-", "2"],
|
||||
["Eb6", "-", "23456a6b"],
|
||||
["E6", "x", "12345"],
|
||||
["F6", "x", "12346a6b"],
|
||||
["F#6", "x", "1235"],
|
||||
["G6", "x", "123"],
|
||||
["Ab6", "x", "124"],
|
||||
["A6", "x", "12"],
|
||||
["Bb6", "x", "1256a6b"],
|
||||
["B6", "x", "1245"],
|
||||
["C7", "x", "145"],
|
||||
["C#7", "x", "1346a6b", true],
|
||||
["D7", "x", "1346a6b"],
|
||||
];
|
||||
|
||||
/**
|
||||
* German differs on F, F♯, and B♭.
|
||||
* Low F is Dolmetsch’s German fingering: 0 123 4 (holes 5–7 open).
|
||||
* F♯ is thumb, 1, 2, 3, 5, 6, and half-hole 7.
|
||||
* B♭ is thumb, 1, 3, 4, 6.
|
||||
* Those two accidentals are the school German pattern described by
|
||||
* Zach VanderGraaff (Dynamic Music Room). The octave above keeps the same
|
||||
* front holes and uses the Dolmetsch second-octave thumb.
|
||||
*/
|
||||
const GERMAN_ROWS: Row[] = [
|
||||
["F5", "0", "1234"],
|
||||
["F#5", "0", "12356a6b7a"],
|
||||
["Bb5", "0", "1346a6b"],
|
||||
["F6", "x", "1234"],
|
||||
["F#6", "x", "12356a6b7a"],
|
||||
["Bb6", "x", "1346a6b"],
|
||||
];
|
||||
|
||||
export const CHART_NOTES = [
|
||||
"C5",
|
||||
"C#5",
|
||||
"D5",
|
||||
"Eb5",
|
||||
"E5",
|
||||
"F5",
|
||||
"F#5",
|
||||
"G5",
|
||||
"Ab5",
|
||||
"A5",
|
||||
"Bb5",
|
||||
"B5",
|
||||
"C6",
|
||||
"C#6",
|
||||
"D6",
|
||||
"Eb6",
|
||||
"E6",
|
||||
"F6",
|
||||
"F#6",
|
||||
"G6",
|
||||
"Ab6",
|
||||
"A6",
|
||||
"Bb6",
|
||||
"B6",
|
||||
"C7",
|
||||
"C#7",
|
||||
"D7",
|
||||
] as const;
|
||||
|
||||
function closedSet(spec: string): Set<string> {
|
||||
const closed = new Set<string>();
|
||||
let index = 0;
|
||||
while (index < spec.length) {
|
||||
const pair = spec.slice(index, index + 2);
|
||||
if (pair === "6a" || pair === "6b" || pair === "7a" || pair === "7b") {
|
||||
closed.add(pair);
|
||||
index += 2;
|
||||
} else {
|
||||
closed.add(spec[index]);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return closed;
|
||||
}
|
||||
|
||||
function make(thumb: ThumbCode, spec: string, bell = false): Fingering {
|
||||
const closed = closedSet(spec);
|
||||
return {
|
||||
thumb: thumb === "0" ? "closed" : thumb === "x" ? "half" : "open",
|
||||
holes: HOLE_IDS.map((id) => (closed.has(id) ? "closed" : "open")),
|
||||
bell,
|
||||
};
|
||||
}
|
||||
|
||||
function table(rows: Row[]): Map<number, Fingering> {
|
||||
const map = new Map<number, Fingering>();
|
||||
for (const [pitch, thumb, holes, bell] of rows) {
|
||||
const parsed = parsePitch(pitch);
|
||||
if (!parsed) throw new Error(`Bad chart pitch ${pitch}`);
|
||||
map.set(parsed.midi, make(thumb, holes, bell ?? false));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const BAROQUE = table(BAROQUE_ROWS);
|
||||
const GERMAN = table(GERMAN_ROWS);
|
||||
|
||||
export function fingeringFor(pitch: string, system: FingeringSystem): Fingering | null {
|
||||
const parsed = parsePitch(pitch);
|
||||
if (!parsed || parsed.midi < LOW_MIDI || parsed.midi > HIGH_MIDI) return null;
|
||||
if (system === "german") {
|
||||
const german = GERMAN.get(parsed.midi);
|
||||
if (german) return german;
|
||||
}
|
||||
return BAROQUE.get(parsed.midi) ?? null;
|
||||
}
|
||||
|
||||
export function fingeringCode(fingering: Fingering): string {
|
||||
const thumb = fingering.thumb === "closed" ? "0" : fingering.thumb === "half" ? "x" : "-";
|
||||
const closed = HOLE_IDS.filter((_, index) => fingering.holes[index] === "closed");
|
||||
const bell = fingering.bell ? " bell" : "";
|
||||
return `${thumb} ${closed.join(" ")}${bell}`;
|
||||
}
|
||||
|
||||
export function fingeringMarks(fingering: Fingering): string[] {
|
||||
const marks: string[] = [];
|
||||
if (fingering.thumb === "half") marks.push("pinch");
|
||||
const [hole1, hole2, hole3, hole4, hole5, hole6a, hole6b, hole7a, hole7b] = fingering.holes;
|
||||
if (hole6a !== hole6b) marks.push("half 6");
|
||||
if (hole7a !== hole7b) marks.push("half 7");
|
||||
const lowerRightClosed = hole6a === "closed" || hole6b === "closed" || hole7a === "closed" || hole7b === "closed";
|
||||
if (hole4 === "closed" && hole5 === "open" && lowerRightClosed) marks.push("fork");
|
||||
if (hole1 === "closed" && hole2 === "open" && hole3 === "closed") marks.push("fork");
|
||||
if (fingering.bell) marks.push("cover bell");
|
||||
return marks;
|
||||
}
|
||||
|
||||
export function fingeringProse(fingering: Fingering): string {
|
||||
const thumb =
|
||||
fingering.thumb === "closed"
|
||||
? "thumb closed"
|
||||
: fingering.thumb === "half"
|
||||
? "thumb pinched"
|
||||
: "thumb open";
|
||||
const names = ["1", "2", "3", "4", "5", "6a", "6b", "7a", "7b"];
|
||||
const closed = names.filter((_, index) => fingering.holes[index] === "closed");
|
||||
const holes = closed.length > 0 ? `holes ${closed.join(" ")} closed` : "no front holes closed";
|
||||
const bell = fingering.bell ? ", cover the bell" : "";
|
||||
return `${thumb}, ${holes}${bell}`;
|
||||
}
|
||||
|
||||
export function germanDiffers(pitch: string): boolean {
|
||||
const baroque = fingeringFor(pitch, "baroque");
|
||||
const german = fingeringFor(pitch, "german");
|
||||
if (!baroque || !german) return false;
|
||||
return fingeringCode(baroque) !== fingeringCode(german);
|
||||
}
|
||||
Reference in new issue
Block a user