slab/menuband: PDF score round-trip with sample-accurate playback master
Drag a Menu Band PDF onto the menubar piano (or back onto the score area) and the synth replays the original performance — sample- accurate timing, full sustain, with the auto-played keys lit RED to distinguish "score is playing me back" from "I'm pressing keys." Embeds two payloads in the PDF Info dictionary: • Subject = MusicXML for the staff visual. • Keywords = ["MenuBandFormat:1", "events:<json>"], where the json is [{midi, on, off}] in seconds-from-first-onset. Held notes export as off:null and Swift clamps them to the latest released note's release at extract time, so sustain matches what the user actually held until they grabbed the drag. Timing: • Sheet.noteOn(pitch, t) accepts a Swift-supplied CACurrentMediaTime, bypassing WebKit bridge's variable 5–50ms delivery latency. Onsets are anchored to the input event, not when JS happens to receive the message. • Replay schedules note-on/off via DispatchQueue.main .asyncAfter at .nanoseconds() precision (was rounding to ms). • MenuBandPopover.refreshHeldNotes records `display + octaveShift × 12` so the played pitch (what the user heard) round-trips, not the menubar-clamped C4–B5 display window. • buildMusicXML emits a <sound tempo="X"/> directive in measure 1 — derived from beatInterval when the metronome's running, or the median (real_gap / notated_seconds_at_120bpm) ratio when free-playing — so Verovio's renderToMIDI replays at the inferred tempo (legacy fallback path for pre-events PDFs). UX polish: • Drag-out clears the staff once the drop completes (NSDraggingSource endedAt) so the next take starts fresh. • Drag-in clears the local notes buffer first so the imported score replaces (not overlays) what was on screen. • Menubar drop target also accepts .pdf, dispatched to a new handlePDFFileDrop that opens the popover and hands off to SheetMusicView.loadAndPlay. Format-spec backstory: the original /MenuBandMusicXML + /MenuBandFormat custom Info-dict keys round-tripped on disk but PDFKit serialised them as PDF strings (parens) instead of PDF names (slash-prefixed). CoreGraphics's stricter parser rejected the whole dict on read with "found non-name key while building dictionary" → documentAttributes returned empty. Switched to Subject + Keywords (predefined PDFDocumentAttribute keys that PDFKit serialises correctly) and kept a raw-bytes legacy fallback for PDFs from the broken format.