import { useEffect, useRef, useState, type RefObject } from "react"; import clsx from "clsx"; import { IconChevron, IconDownload } from "./icons"; /** * Output / "job" actions, shown below the piano roll: live transcription * progress, exporting the result (MIDI or a stereo mix), and starting over * with another file. Distinct from the playback bar above the roll, which is * about exploring the result. * * The progress bar's fill width and "Xs / Ys" label are driven imperatively * from App's per-frame loop (via the refs) so the smoothing never triggers * React re-renders — same pattern as the playback clock. */ export function OutputBar(props: { transcribing: boolean; progressFillRef: RefObject; progressLabelRef: RefObject; midiUrl: string | null; midiFilename: string; /** Raw MIDI blob + source audio, re-uploaded to /auralize for the mix. */ midiBlob: Blob | null; currentFile: File | null; onTranscribeAnother: () => void; }) { const { transcribing, progressFillRef, progressLabelRef, midiUrl, midiFilename, midiBlob, currentFile, onTranscribeAnother, } = props; const [synthesizing, setSynthesizing] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const menuRef = useRef(null); const ready = midiUrl !== null; // Dismiss the download menu on outside click / Escape. useEffect(() => { if (!menuOpen) return; const onDown = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) setMenuOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); }; document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); }; }, [menuOpen]); function download() { if (midiUrl === null) return; const a = document.createElement("a"); a.href = midiUrl; a.download = midiFilename; a.click(); } // Renders the transcription server-side with FluidSynth. "synth" downloads // just the synthesized MIDI (mono); "mix" blends it with the original audio // (L = original, R = synthesis) for easy A/B comparison. async function downloadWav(mode: "synth" | "mix") { if (midiBlob === null || currentFile === null) return; setSynthesizing(true); try { const form = new FormData(); form.append("mode", mode); form.append("midi", midiBlob, "transcription.mid"); if (mode === "mix") form.append("audio", currentFile); const resp = await fetch("/auralize", { method: "POST", body: form }); if (!resp.ok) { const text = await resp.text(); let detail = text; try { detail = JSON.parse(text).detail ?? text; } catch { // not JSON — keep the raw body } throw new Error(detail); } const wavBlob = await resp.blob(); const url = URL.createObjectURL(wavBlob); const a = document.createElement("a"); a.href = url; const stem = currentFile.name.replace(/\.[^.]+$/, "") || "transcription"; a.download = stem + (mode === "mix" ? "_mix.wav" : "_transcription.wav"); a.click(); URL.revokeObjectURL(url); } catch (e) { alert("Couldn't create the audio file: " + (e as Error).message); } finally { setSynthesizing(false); } } const menuItem = "block w-full rounded-none border-0 bg-transparent px-3 py-2 text-left text-[13px] font-normal text-content hover:bg-[#20212b]"; return (
{transcribing && (
estimating…
)}
{menuOpen && (
)}
); }