diff --git a/README.md b/README.md index 9e98649..6719290 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ The usual way to run it is one Linux AppImage. That file contains the window, th ## Run the AppImage ```bash -chmod +x Recorder-Tab-Converter-0.1.1.AppImage -./Recorder-Tab-Converter-0.1.1.AppImage +chmod +x Recorder-Tab-Converter-0.1.2.AppImage +./Recorder-Tab-Converter-0.1.2.AppImage ``` If the file will not start because FUSE is missing: ```bash -APPIMAGE_EXTRACT_AND_RUN=1 ./Recorder-Tab-Converter-0.1.1.AppImage +APPIMAGE_EXTRACT_AND_RUN=1 ./Recorder-Tab-Converter-0.1.2.AppImage ``` Open **Twinkle, Twinkle, Little Star** or **Ode to Joy** to see tabs immediately. Or drop a PNG, JPG, or WEBP of a page. The note list under the tab is editable. Fix a bad pitch or duration and the fingerings update. If a photo cannot be read, the app says so and does not invent notes. @@ -30,7 +30,7 @@ From a checkout, one command: That installs Node packages, downloads a relocatable Python plus oemer and its ONNX models, builds the app, and writes: ```text -dist/Recorder-Tab-Converter-0.1.1.AppImage +dist/Recorder-Tab-Converter-0.1.2.AppImage ``` The U-Net checkpoint from oemer 0.1.8 has ConvTranspose pads `[0, 0, -1, -1]`. The build rewrites those nodes to pads `[0, 0, 0, 0]` and `output_padding` `[1, 1]` before packaging. That is the same output size, and it is what current ONNX Runtime will load. The rewrite does not change fingerings. @@ -51,6 +51,8 @@ The dev server listens on [http://127.0.0.1:3847](http://127.0.0.1:3847). Photo Baroque (English) fingerings are the first choice on the [Dolmetsch Online chart for recorders in C](https://dolmetsch.com/textfingeringchart.htm) (Brian Blood). The soprano is treated in concert pitch: it sounds as written, low C is C5, and the chart stops at D7. Double holes 6 and 7, forked F, and the second-octave thumb pinch are included. Alternate fingerings on that chart are not shown. Notes outside C5–D7 are flagged instead of guessed. +Each chart is a narrow recorder drawn foot-up: holes 7 and 6 at the top, then 5 through 1, then the thumb, with the mouthpiece at the bottom. A filled hole is closed. The lead is a horizontal row of those charts, grouped by bar. An arrow after a chart is one more beat. A ×2 mark appears only when the music has a repeat. + German mode changes three notes: - **F** — Dolmetsch’s German fingering, thumb plus holes 1–4 (`0 123 4`). diff --git a/package-lock.json b/package-lock.json index c50ce3d..df76635 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "recorder-tab-converter", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "recorder-tab-converter", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "@base-ui/react": "^1.8.0", "class-variance-authority": "^0.7.1", diff --git a/package.json b/package.json index d939f4b..d1c7c34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "recorder-tab-converter", - "version": "0.1.1", + "version": "0.1.2", "private": true, "description": "Turn a sheet-music image into soprano recorder fingering tabs, with rhythm kept.", "main": "electron/main.cjs", diff --git a/src/components/converter.tsx b/src/components/converter.tsx index de30498..3c9022f 100644 --- a/src/components/converter.tsx +++ b/src/components/converter.tsx @@ -114,8 +114,9 @@ export function Converter() {

Soprano in C · concert pitch

Recorder Tab Converter

- Turn a lead melody into fingering tabs a soprano player can read: thumb and holes, the note - name, and the rhythm, lined up in bars. Chords keep the top note. A second staff is ignored. + Turn a lead melody into fingering tabs a soprano player can read. Each chart has the foot at + the top and the mouthpiece at the bottom, with the rhythm lined up in bars. Chords keep the + top note. A second staff is ignored.

diff --git a/src/components/fingering-diagram.tsx b/src/components/fingering-diagram.tsx index f65dca6..62509e7 100644 --- a/src/components/fingering-diagram.tsx +++ b/src/components/fingering-diagram.tsx @@ -6,6 +6,13 @@ import { type HoleState = Fingering["holes"][number]; +const TUBE = "#9deaf3"; +const INK = "#1a1a1a"; + +/** + * Foot at the top, mouthpiece at the bottom. + * The drawing is the reverse of a held recorder. Hole states are not changed. + */ function Hole({ cx, cy, @@ -19,15 +26,15 @@ function Hole({ }) { return ( - - {state === "closed" ? : null} + + {state === "closed" ? : null} {state === "half" ? ( ) : null} - + ); } @@ -35,69 +42,77 @@ function Hole({ export function FingeringDiagram({ fingering, pitch, - size = "lg", + size = "lead", }: { fingering: Fingering | null; pitch: string; - size?: "lg" | "sm"; + size?: "lead" | "sm"; }) { - const width = size === "lg" ? 112 : 88; + const width = size === "lead" ? 44 : 40; + const height = Math.round(width * (188 / 48)); if (!fingering) { return (
- Outside C5–D7. No fingering. + No chart
); } const [h1, h2, h3, h4, h5, h6a, h6b, h7a, h7b] = fingering.holes; - const label = `${pitch}. ${fingeringProse(fingering)}. ${fingeringMarks(fingering).join(", ")}`; + const label = `${pitch}. Foot at the top, mouthpiece at the bottom. ${fingeringProse(fingering)}. ${fingeringMarks(fingering).join(", ")}`; return ( - - - - - T - - - - - - - - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - {fingering.bell ? ( - - bell - - ) : null} + + {fingering.bell ? : null} + + + + + + + + + + + + + + + + ); } -export function RestMark({ label }: { label: string }) { +export function BeatArrow() { + return ( + + + + ); +} + +export function RestMark({ label, width = 44 }: { label: string; width?: number }) { + const height = Math.round(width * (188 / 48)); const kind = label.includes("whole") ? "whole" : label.includes("half") @@ -108,25 +123,25 @@ export function RestMark({ label }: { label: string }) { ? "eighth" : "quarter"; return ( - - - {kind === "whole" ? : null} - {kind === "half" ? : null} + + + {kind === "whole" ? : null} + {kind === "half" ? : null} {kind === "quarter" ? ( ) : null} {kind === "eighth" || kind === "sixteenth" ? ( - - - + + + {kind === "sixteenth" ? ( - + ) : null} ) : null} diff --git a/src/components/tab-score.tsx b/src/components/tab-score.tsx index 9e2107c..2503153 100644 --- a/src/components/tab-score.tsx +++ b/src/components/tab-score.tsx @@ -1,6 +1,6 @@ "use client"; -import { FingeringDiagram, RestMark } from "@/components/fingering-diagram"; +import { BeatArrow, FingeringDiagram, RestMark } from "@/components/fingering-diagram"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -8,11 +8,20 @@ import { CHART_NOTES, fingeringCode, fingeringFor, - fingeringMarks, germanDiffers, type FingeringSystem, } from "@/lib/fingering"; -import { formatBeat, signatureKey, startBeats, type Score } from "@/lib/score"; +import { + extraBeatArrows, + formatBeat, + leadRows, + signatureKey, + startBeats, + type Measure, + type Score, + type ScoreEvent, + type TimeSignature, +} from "@/lib/score"; import { formatTextTab } from "@/lib/text-tab"; import { Copy } from "lucide-react"; import { toast } from "sonner"; @@ -36,8 +45,9 @@ export function ScoreViews({ score, system }: { score: Score; system: FingeringS

- Each cell is wide in proportion to its length, so a half note takes twice the room of a - quarter. Scroll a bar sideways if the fingerings need more room than the screen. + Each note is a narrow recorder with the foot at the top and the mouthpiece at the bottom. + Filled holes are closed. Notes sit in a row, grouped by bar. An arrow is one more beat on + the note before it. The duration is also written under the chart.

{outside > 0 ? (

@@ -46,56 +56,21 @@ export function ScoreViews({ score, system }: { score: Score; system: FingeringS

) : null}
- {score.measures.map((measure, index) => { - const beats = startBeats(measure.events, score.timeSignature); - return ( -
-

Bar {index + 1}

- {measure.events.length === 0 ? ( -

Empty bar. Add a note or a rest in the editor.

- ) : ( -
-
- {measure.events.map((event, eventIndex) => { - const fingering = - event.kind === "note" && event.pitch - ? fingeringFor(event.pitch, system) - : null; - const marks = fingering ? fingeringMarks(fingering) : []; - return ( -
-
- {event.kind === "rest" ? ( - - ) : ( - - )} -
-

- {event.kind === "rest" ? "rest" : event.pitch} -

-

{event.label}

- {marks.length > 0 ? ( -

{marks.join(" · ")}

- ) : null} - {event.tied ?

ties forward

: null} -

beat {formatBeat(beats[eventIndex] ?? 1)}

-
- ); - })} -
-
- )} -
- ); - })} + {leadRows(score.measures).map((row, rowIndex) => ( +
+
+ {row.map((measure) => ( + item.id === measure.id) + 1} + /> + ))} +
+
+ ))}
{score.measures.some((measure) => @@ -131,13 +106,13 @@ export function ScoreViews({ score, system }: { score: Score; system: FingeringS

- Every note from C5 to D7. {system === "baroque" ? "Baroque is showing." : "German changes are badged."} + Every note from C5 to D7, foot at the top. {system === "baroque" ? "Baroque is showing." : "German changes are badged."}

-