"use client"; import { NoteEditor } from "@/components/note-editor"; import { ScoreViews } from "@/components/tab-score"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import type { FingeringSystem } from "@/lib/fingering"; import { SAMPLES } from "@/lib/samples"; import { blankScore, type Score } from "@/lib/score"; import { Loader2, Music2, Upload } from "lucide-react"; import { useEffect, useId, useState } from "react"; type Status = "empty" | "loading" | "ready" | "error"; type RecognizerInfo = { available: boolean; name: string; detail: string; }; export function Converter() { const inputId = useId(); const [score, setScore] = useState(null); const [system, setSystem] = useState("baroque"); const [status, setStatus] = useState("empty"); const [error, setError] = useState(null); const [warnings, setWarnings] = useState([]); const [recognizer, setRecognizer] = useState(null); const [dragOver, setDragOver] = useState(false); useEffect(() => { let cancelled = false; fetch("/api/recognize") .then((response) => response.json()) .then((info: RecognizerInfo) => { if (!cancelled) setRecognizer(info); }) .catch(() => { if (!cancelled) { setRecognizer({ available: false, name: "oemer", detail: "Could not check the local reader. Samples and the editor still work.", }); } }); return () => { cancelled = true; }; }, []); function loadScore(next: Score) { setScore(structuredClone(next)); setWarnings([]); setError(null); setStatus("ready"); } async function onFile(file: File) { setError(null); const allowed = ["image/png", "image/jpeg", "image/webp"].includes(file.type) || /\.(png|jpe?g|webp)$/i.test(file.name); if (file.size === 0) { setError("That file is empty. No notes were invented."); setScore((current) => current ?? blankScore()); setStatus("error"); return; } if (!allowed) { setError("Use a PNG, JPG, or WEBP photo. That file is not one of those. No notes were invented."); setScore((current) => current ?? blankScore()); setStatus("error"); return; } if (file.size > 8 * 1024 * 1024) { setError("That image is over 8 MB. Crop the line or export a smaller JPEG."); setScore((current) => current ?? blankScore()); setStatus("error"); return; } setStatus("loading"); try { const body = new FormData(); body.set("image", file); const response = await fetch("/api/recognize", { method: "POST", body }); const data = (await response.json()) as { error?: string; score?: Score; warnings?: string[]; }; if (!response.ok || data.error || !data.score) { setError(data.error ?? "This image could not be read. No notes were invented."); setWarnings([]); setScore((current) => current ?? blankScore()); setStatus("error"); return; } setScore(data.score); setWarnings(data.warnings ?? []); setStatus("ready"); } catch { setError("The reader did not answer. No notes were invented. You can still enter the line by hand."); setScore((current) => current ?? blankScore()); setStatus("error"); } } return (

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.

{ event.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(event) => { event.preventDefault(); setDragOver(false); const file = event.dataTransfer.files[0]; if (file) void onFile(file); }} >

Photo or scan

{recognizer?.detail ?? "Checking the local reader…"}

{ const file = event.target.files?.[0]; event.target.value = ""; if (file) void onFile(file); }} />
{status === "loading" ? (

oemer is reading the page on this computer. A dense photo can take a few minutes. Nothing is uploaded.

) : (

Drop a PNG, JPG, or WEBP here, or open a sample below if you want to see the tabs before a photo works.

)}
{error ? ( The page was not read {error} ) : null} {warnings.length > 0 ? ( What the reader kept
    {warnings.map((warning) => (
  • {warning}
  • ))}
) : null}
{SAMPLES.map((sample) => ( ))}
{score ? ( <>

{score.title}

{score.attribution}

Fingering

{ const picked = values[values.length - 1]; if (picked === "baroque" || picked === "german") setSystem(picked); }} aria-label="Fingering system" > Baroque German

Baroque is the Dolmetsch chart and the default. German only changes F, F♯, and B♭.

Note list

Pitch, rest, and duration. Change anything the photo got wrong. The tabs follow this list, including the bar lines.

) : (

No line loaded yet

Open Twinkle or Ode to Joy to see fingerings and rhythm immediately, or start an empty bar and type the notes yourself.

)}
); }