1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-pdf-CjPCU7YA.js","assets/vendor-react-three-BCww3P1x.js","assets/vendor-react-Bay9Y7Lb.js","assets/vendor-D0P4XgNj.js","assets/vendor-i18n-BC7ZMXWQ.js","assets/vendor-DqKRdbjz.css"])))=>i.map(i=>d[i]); 2import { _ as __vitePreload } from "./vendor-react-three-BCww3P1x.js"; 3import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js"; 4import { bo as Monitor, ds as ZoomOut, dt as ZoomIn, cA as SlidersHorizontal, ct as BookOpen, bg as Music, cG as Keyboard, aW as X, ce as Ellipsis, bL as ThumbsUp, bM as Clock, bO as Library, b6 as LoaderCircle, d1 as Download, c_ as ChevronLeft, bF as ChevronRight, ck as Layers, fQ as Map$1, gj as Pill, ba as Info, bI as Hash, e2 as Palette, dq as MousePointer2, cQ as GraduationCap } from "./vendor-D0P4XgNj.js"; 5import { u as useSoundfontKeyboard, a as useKeyboardInput, g as getMidiFromKeyboardClick, r as renderSharedKeyboard } from "./SharedKeyboard-B8ktwnUd.js"; 6import "./vendor-i18n-BC7ZMXWQ.js"; 7import "./useMidiKeyboard-DlyEIN8T.js"; 8import "./wasm-synth-adapter-DpNGIIyM.js"; 9import "./vendor-spessasynth-DH8n6o_V.js"; 10const useSheetMusicEngine = (props) => { 11 const { 12 xmlData: initialXmlData, 13 xmlString: initialXmlString, 14 scorePath, 15 quantizedScorePaths, 16 midiId, 17 isMelodyMode, 18 melodySource, 19 title, 20 processedInstruments, 21 trackVisibility, 22 dimensions, 23 layoutMode, 24 scale, 25 quantization, 26 density, 27 showMeasureNumbers, 28 visibleTrackCount, 29 showChords, 30 showRoman, 31 harmonyChordsUrl, 32 harmonyRomanUrl, 33 measureMap = [], 34 sections = [], 35 duration 36 } = props; 37 const [verovioToolkit, setVerovioToolkit] = reactExports.useState(null); 38 const [loading, setLoading] = reactExports.useState(() => { 39 return !!(initialXmlData || initialXmlString || scorePath || quantizedScorePaths); 40 }); 41 const [loadingProgress, setLoadingProgress] = reactExports.useState({ 42 percent: 15, 43 stage: "Initializing score engine..." 44 }); 45 const [isDataLoaded, setIsDataLoaded] = reactExports.useState(false); 46 const [pageCount, setPageCount] = reactExports.useState(0); 47 const [currentPage, setCurrentPage] = reactExports.useState(1); 48 const [svgContent, setSvgContent] = reactExports.useState(""); 49 const [svgHeight, setSvgHeight] = reactExports.useState(0); 50 const [syncPoints, setSyncPoints] = reactExports.useState([]); 51 const [internalXmlData, setInternalXmlData] = reactExports.useState(null); 52 const [harmonyData, setHarmonyData] = reactExports.useState({}); 53 const currentSourceRef = reactExports.useRef(null); 54 const finalXmlDataRef = reactExports.useRef(null); 55 const availableIdsRef = reactExports.useRef(/* @__PURE__ */ new Set()); 56 const measureIdMapRef = reactExports.useRef({}); 57 const lastOptionsRef = reactExports.useRef(""); 58 const lastRenderedPageRef = reactExports.useRef(0); 59 const trackVisibilityStr = JSON.stringify(trackVisibility); 60 reactExports.useEffect(() => { 61 let mounted = true; 62 const injectScript = () => { 63 return new Promise((resolve, reject) => { 64 const existing = document.querySelector('script[src*="verovio"]'); 65 if (window.verovio) { 66 resolve(); 67 return; 68 } 69 if (existing) { 70 existing.addEventListener("load", () => resolve()); 71 existing.addEventListener("error", () => reject(new Error("Verovio script load error"))); 72 if (existing.dataset.loaded === "true") resolve(); 73 return; 74 } 75 const script = document.createElement("script"); 76 script.src = "/libs/verovio/verovio-toolkit-wasm.js"; 77 script.async = true; 78 script.onload = () => { 79 script.dataset.loaded = "true"; 80 resolve(); 81 }; 82 script.onerror = () => reject(new Error("Failed to download verovio-toolkit-wasm.js")); 83 document.head.appendChild(script); 84 }); 85 }; 86 const init = async () => { 87 try { 88 await injectScript(); 89 } catch (e) { 90 console.error("[SheetMusicEngine] Verovio script download failed:", e); 91 if (mounted) setLoading(false); 92 return; 93 } 94 let attempts = 0; 95 while (attempts < 300) { 96 if (!mounted) return; 97 const v = window.verovio; 98 if (v && typeof v === "object") break; 99 await new Promise((r) => setTimeout(r, 100)); 100 attempts++; 101 } 102 const verovio = window.verovio; 103 if (!verovio || typeof verovio !== "object") { 104 console.error("[SheetMusicEngine] Verovio global object not found after timeout. typeof window.verovio:", typeof window.verovio); 105 if (mounted) setLoading(false); 106 return; 107 } 108 if (window.verovioToolkit) { 109 if (mounted) setVerovioToolkit(window.verovioToolkit); 110 return; 111 }
112 try { 113 let module = verovio.module; 114 const ToolkitClass = verovio.toolkit; 115 if (!ToolkitClass) { 116 throw new Error("Verovio toolkit class not found"); 117 } 118 console.log("[SheetMusicEngine] Initializing with module type:", typeof module); 119 if (module && typeof module.then === "function") { 120 console.log("[SheetMusicEngine] Awaiting Verovio module promise..."); 121 module = await module; 122 } else if (typeof module === "function") { 123 console.log("[SheetMusicEngine] Calling Verovio module factory..."); 124 module = await module(); 125 } 126 if (module && !module.calledRun) { 127 await new Promise((resolve) => { 128 const prev = module.onRuntimeInitialized; 129 module.onRuntimeInitialized = () => { 130 if (typeof prev === "function") prev(); 131 resolve(); 132 }; 133 let waitCount = 0; 134 const checkInterval = setInterval(() => { 135 if (module.calledRun || waitCount > 100) { 136 clearInterval(checkInterval); 137 resolve(); 138 } 139 waitCount++; 140 }, 50); 141 }); 142 } 143 let tk = null; 144 let initAttempts = 0; 145 while (initAttempts < 30) { 146 try { 147 tk = new ToolkitClass(); 148 if (tk && typeof tk.getVersion === "function") break; 149 } catch { 150 try { 151 tk = new ToolkitClass(module); 152 if (tk && typeof tk.getVersion === "function") break; 153 } catch { 154 } 155 } 156 await new Promise((r) => setTimeout(r, 100)); 157 initAttempts++; 158 } 159 if (tk && typeof tk.getVersion === "function") { 160 console.log("[SheetMusicEngine] Verovio initialized successfully. Version:", tk.getVersion()); 161 window.verovioToolkit = tk; 162 if (mounted) setVerovioToolkit(tk); 163 } else { 164 throw new Error("Failed to instantiate Verovio toolkit after retries"); 165 } 166 } catch (e) { 167 console.error("[SheetMusicEngine] Failed to init Verovio:", e); 168 let fallbackSuccess = false; 169 if (window.createVerovioModule) { 170 try { 171 console.log("[SheetMusicEngine] Attempting fallback via createVerovioModule..."); 172 const module = await window.createVerovioModule(); 173 const tk = new window.verovio.toolkit(module); 174 if (mounted) { 175 setVerovioToolkit(tk); 176 fallbackSuccess = true; 177 } 178 } catch (fallbackErr) { 179 console.error("[SheetMusicEngine] Fallback failed:", fallbackErr); 180 } 181 } 182 if (!fallbackSuccess && mounted) { 183 setLoading(false); 184 } 185 } 186 }; 187 init(); 188 return () => { 189 mounted = false; 190 }; 191 }, []); 192 const renderPage = reactExports.useCallback((page) => { 193 if (!verovioToolkit || !isDataLoaded) return; 194 try { 195 const svg = verovioToolkit.renderToSVG(page, {}); 196 if (svg) { 197 setSvgContent(svg); 198 const vbMatch = svg.match(/viewBox="[-\d.]+\s+[-\d.]+\s+[-\d.]+\s+([-\d.]+)"/); 199 if (vbMatch) setSvgHeight(parseFloat(vbMatch[1])); 200 lastRenderedPageRef.current = page; 201 } 202 } catch (e) { 203 console.error("[SheetMusicEngine] Render Page Error:", e); 204 } 205 }, [verovioToolkit, isDataLoaded]); 206 reactExports.useEffect(() => { 207 let active = true; 208 const fetchScore = async () => { 209 let url = scorePath; 210 if (isMelodyMode) { 211 if (melodySource === "studio") { 212 url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.melody) || scorePath; 213 } else { 214 url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore_melody) || scorePath; 215 } 216 } else { 217 if (quantization === 900) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.urtext) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || scorePath; 218 else if (quantization === 8) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath; 219 else if (quantization === 16) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath; 220 else if (quantization === 300) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath; 221 else if (quantization === 500) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || scorePath; 222 else if (quantization === 600) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.ly) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath; 223 else if (quantization === 700) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.abc) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath; 224 else if (quantization === 800) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || scorePath; 225 else url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath; 226 } 227 if (!url && !initialXmlString && !initialXmlData) { 228 if (active) {
229 setLoading(false); 230 setIsDataLoaded(false); 231 } 232 return; 233 } 234 if (active) { 235 setLoading(true); 236 setLoadingProgress({ percent: 30, stage: "Downloading sheet music notation..." }); 237 setIsDataLoaded(false); 238 setInternalXmlData(""); 239 } 240 if (url) { 241 try { 242 console.log("[SheetMusicEngine] Fetching score from:", url); 243 const res = await fetch(url); 244 if (!res.ok) throw new Error(`HTTP ${res.status}`); 245 let text = await res.text(); 246 const hasNotes = text.includes("<note") || text.includes(":note"); 247 if (!hasNotes && ((quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) || scorePath)) { 248 const fallbackUrl = url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) || url !== scorePath && scorePath; 249 if (fallbackUrl) { 250 console.warn(`[SheetMusicEngine] Score at ${url} contains no notes. Automatically falling back to: ${fallbackUrl}`); 251 try { 252 const fbRes = await fetch(fallbackUrl); 253 if (fbRes.ok) { 254 text = await fbRes.text(); 255 } 256 } catch (fbErr) { 257 console.error("[SheetMusicEngine] Fallback score fetch failed:", fbErr); 258 } 259 } 260 } 261 if (active) { 262 setLoadingProgress({ percent: 55, stage: "Processing score data..." }); 263 setInternalXmlData(text); 264 } 265 } catch (err) { 266 console.error("[SheetMusicEngine] Failed to fetch score from primary URL:", err); 267 let fallbackText = ""; 268 const fallbacks = [ 269 quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved, 270 scorePath, 271 quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio, 272 quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8, 273 quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16 274 ].filter((u) => u && u !== url); 275 for (const fb of fallbacks) { 276 try { 277 console.log("[SheetMusicEngine] Trying fallback score URL:", fb); 278 const fbRes = await fetch(fb); 279 if (fbRes.ok) { 280 fallbackText = await fbRes.text(); 281 break; 282 } 283 } catch { 284 } 285 } 286 if (active) { 287 if (fallbackText) { 288 setLoadingProgress({ percent: 55, stage: "Processing score data..." }); 289 setInternalXmlData(fallbackText); 290 } else if (initialXmlString) { 291 setInternalXmlData(initialXmlString); 292 } else if (initialXmlData) { 293 setInternalXmlData(new TextDecoder().decode(initialXmlData)); 294 } else { 295 setLoading(false); 296 } 297 } 298 } 299 } else { 300 if (active) { 301 if (initialXmlString) setInternalXmlData(initialXmlString); 302 else if (initialXmlData) setInternalXmlData(new TextDecoder().decode(initialXmlData)); 303 } 304 } 305 }; 306 fetchScore(); 307 return () => { 308 active = false; 309 }; 310 }, [scorePath, quantizedScorePaths, quantization, isMelodyMode, melodySource, initialXmlString, initialXmlData]); 311 reactExports.useEffect(() => { 312 if (!verovioToolkit) return; 313 const loadScore = async () => { 314 const localXmlString = internalXmlData || ""; 315 if (!localXmlString) { 316 return; 317 }
318 setLoading(true); 319 setLoadingProgress({ percent: 65, stage: "Processing track & notation structure..." }); 320 setIsDataLoaded(false); 321 lastOptionsRef.current = ""; 322 lastRenderedPageRef.current = 0; 323 try { 324 await new Promise((r) => setTimeout(r, 0)); 325 const parser = new DOMParser(); 326 const xmlDoc = parser.parseFromString(localXmlString, "text/xml"); 327 let modified = false; 328 const realInstruments = processedInstruments.filter((instr) => { 329 const name = (instr.displayName || instr.name || "").toLowerCase(); 330 return name !== "informational track"; 331 }); 332 const isMEI = !!xmlDoc.querySelector("mei, *|mei"); 333 const allScoreParts = Array.from(xmlDoc.querySelectorAll("score-part")); 334 const allParts = Array.from(xmlDoc.querySelectorAll("part")); 335 if (isMEI) { 336 const scoreDef = xmlDoc.querySelector("scoreDef, *|scoreDef"); 337 const rootStaffGrp = scoreDef ? scoreDef.querySelector("staffGrp, *|staffGrp") || scoreDef : xmlDoc;
338 let partNodes = []; 339 if (rootStaffGrp) { 340 const directChildren = Array.from(rootStaffGrp.children).filter((el) => { 341 const tag = (el.localName || el.tagName).toLowerCase().split(":").pop() || ""; 342 return tag === "staffdef" || tag === "staffgrp"; 343 }); 344 if (directChildren.length > 0) { 345 partNodes = directChildren; 346 } 347 } 348 if (partNodes.length === 0) { 349 const allStaffDefs = Array.from(xmlDoc.querySelectorAll("staffDef, *|staffDef")); 350 const seenN = /* @__PURE__ */ new Set(); 351 partNodes = allStaffDefs.filter((sd) => { 352 const n = sd.getAttribute("n"); 353 if (!n || seenN.has(n)) return false; 354 seenN.add(n); 355 return true; 356 }); 357 } 358 let firstVisibleStaffN = null; 359 partNodes.forEach((partNode, xmlIdx) => { 360 const instr = realInstruments[xmlIdx]; 361 const isVisible = isMelodyMode ? xmlIdx === 0 : !instr ? true : trackVisibility[instr.index] !== false; 362 let displayName = instr ? instr.displayName || instr.name || "" : ""; 363 if (isMelodyMode && xmlIdx === 0) { 364 displayName = melodySource === "studio" ? "Melody (Studio Engraved)" : "Melody (Standard)"; 365 } 366 const isNodeStaffDef = (partNode.localName || partNode.tagName).toLowerCase().split(":").pop() === "staffdef"; 367 const staffDefsInPart = Array.from(partNode.querySelectorAll("staffDef, *|staffDef")); 368 if (isNodeStaffDef && !staffDefsInPart.includes(partNode)) { 369 staffDefsInPart.unshift(partNode); 370 } 371 const partStaffNs = staffDefsInPart.map((sd) => sd.getAttribute("n")).filter(Boolean); 372 if (partStaffNs.length === 0) { 373 partStaffNs.push((xmlIdx + 1).toString()); 374 } 375 if (isVisible && firstVisibleStaffN === null && partStaffNs.length > 0) { 376 firstVisibleStaffN = partStaffNs[0]; 377 } 378 if (!isVisible) { 379 staffDefsInPart.forEach((sd) => sd.remove()); 380 if (!isNodeStaffDef) { 381 partNode.remove(); 382 } 383 partStaffNs.forEach((staffN) => { 384 xmlDoc.querySelectorAll(`staffDef[n="${staffN}"], *|staffDef[n="${staffN}"]`).forEach((el) => el.remove()); 385 xmlDoc.querySelectorAll(`staff[n="${staffN}"], *|staff[n="${staffN}"]`).forEach((el) => el.remove()); 386 }); 387 modified = true; 388 return; 389 } 390 if (displayName) { 391 const updateLabelsOnElement = (el) => { 392 el.setAttribute("label", displayName); 393 el.setAttribute("label.abbr", displayName); 394 const tag = (el.localName || el.tagName).toLowerCase().split(":").pop() || ""; 395 if (tag !== "staffdef") { 396 const labelChildren = Array.from(el.children).filter((c) => { 397 const cTag = (c.localName || c.tagName).toLowerCase().split(":").pop() || ""; 398 return cTag === "label"; 399 }); 400 if (labelChildren.length > 0) { 401 labelChildren.forEach((lbl) => { 402 lbl.textContent = displayName; 403 }); 404 } else { 405 try { 406 const newLbl = xmlDoc.createElementNS ? xmlDoc.createElementNS("http://www.music-encoding.org/ns/mei", "label") : xmlDoc.createElement("label"); 407 newLbl.textContent = displayName; 408 el.insertBefore(newLbl, el.firstChild); 409 } catch { 410 } 411 } 412 const abbrChildren = Array.from(el.children).filter((c) => { 413 const cTag = (c.localName || c.tagName).toLowerCase().split(":").pop() || ""; 414 return cTag === "labelabbr"; 415 });
416 abbrChildren.forEach((lbl) => { 417 lbl.textContent = displayName; 418 }); 419 } 420 }; 421 updateLabelsOnElement(partNode); 422 staffDefsInPart.forEach((sd) => { 423 if (sd !== partNode) { 424 updateLabelsOnElement(sd); 425 } 426 }); 427 modified = true; 428 } 429 }); 430 if (firstVisibleStaffN !== null && firstVisibleStaffN !== "1") { 431 const allHarms = xmlDoc.querySelectorAll("harm, *|harm"); 432 allHarms.forEach((harm) => { 433 if (harm.getAttribute("staff") === "1") { 434 harm.setAttribute("staff", firstVisibleStaffN); 435 modified = true; 436 } 437 }); 438 const allDirs = xmlDoc.querySelectorAll("dir, *|dir"); 439 allDirs.forEach((dir) => { 440 if (dir.getAttribute("staff") === "1") { 441 const text = (dir.textContent || "").trim(); 442 if (text.includes("â") || text.includes("â")) { 443 dir.setAttribute("staff", firstVisibleStaffN); 444 modified = true; 445 } 446 } 447 }); 448 } 449 if (!showChords || !showRoman) { 450 const allHarms = xmlDoc.querySelectorAll("harm, *|harm"); 451 allHarms.forEach((harm) => { 452 const text = (harm.textContent || "").trim(); 453 const typeAttr = harm.getAttribute("type") || harm.getAttribute("label") || ""; 454 const isRoman = typeAttr === "roman" || text.includes("â") || /^[IViv]+[0-9]*$/.test(text); 455 const isChord = typeAttr !== "roman" && (text.includes("â") || /^[A-G][b#]?(m|maj|min|dim|aug|sus|add)?[0-9]*(\/[A-G][b#]?)?$/.test(text)); 456 if (isRoman && !showRoman) { 457 harm.remove(); 458 modified = true; 459 } else if (isChord && !showChords) { 460 harm.remove(); 461 modified = true; 462 } 463 }); 464 const allDirs = xmlDoc.querySelectorAll("dir, *|dir, anchoredText, *|anchoredText"); 465 allDirs.forEach((el) => { 466 const text = (el.textContent || "").trim(); 467 const isRoman = text.includes("â") || /^[IViv]+[0-9]*$/.test(text); 468 const isChord = text.includes("â") || /^[A-G][b#]?(m|maj|min|dim|aug|sus|add)?[0-9]*(\/[A-G][b#]?)?$/.test(text); 469 const isKey = text.includes("â") || text.includes("Key:"); 470 if (isRoman && !showRoman) { 471 el.remove(); 472 modified = true; 473 } else if (isChord && !showChords) { 474 el.remove(); 475 modified = true; 476 } else if (isKey && !showChords && !showRoman) { 477 el.remove(); 478 modified = true; 479 } 480 }); 481 } 482 const tieredDirs = xmlDoc.querySelectorAll("dir, *|dir, anchoredText, *|anchoredText, harm, *|harm"); 483 tieredDirs.forEach((el) => { 484 const text = (el.textContent || "").trim(); 485 if (text.includes("â") || text.includes("[Key:")) { 486 el.setAttribute("place", "above"); 487 el.setAttribute("vgrp", "3000"); 488 modified = true; 489 } else if (text.includes("â")) { 490 el.setAttribute("place", "above"); 491 el.setAttribute("vgrp", "2000"); 492 modified = true; 493 } else if (text.includes("â")) { 494 el.setAttribute("place", "above"); 495 el.setAttribute("vgrp", "1000"); 496 modified = true; 497 } 498 }); 499 xmlDoc.querySelectorAll("verse, *|verse").forEach((verse) => { 500 const layer = verse.closest("layer, *|layer"); 501 const layerN = layer ? layer.getAttribute("n") : null; 502 if (layerN && layerN !== "1") { 503 verse.remove(); 504 modified = true; 505 } 506 }); 507 xmlDoc.querySelectorAll("chord, *|chord").forEach((chord) => { 508 const notes = Array.from(chord.querySelectorAll("note, *|note")); 509 let keptOne = false;
510 notes.forEach((note) => { 511 const verses = note.querySelectorAll("verse, *|verse"); 512 verses.forEach((verse) => { 513 if (keptOne) { 514 verse.remove(); 515 modified = true; 516 } else { 517 keptOne = true; 518 } 519 }); 520 }); 521 }); 522 } else { 523 allScoreParts.forEach((scorePart, xmlIdx) => { 524 const part = allParts[xmlIdx]; 525 const instr = realInstruments[xmlIdx]; 526 const isVisible = isMelodyMode ? xmlIdx === 0 : !instr ? true : trackVisibility[instr.index] !== false; 527 let displayName = instr ? instr.displayName || instr.name || "" : ""; 528 if (isMelodyMode && xmlIdx === 0) { 529 displayName = melodySource === "studio" ? "Melody (Studio Engraved)" : "Melody (Standard)"; 530 } 531 if (!isVisible) { 532 scorePart.remove(); 533 if (part) part.remove(); 534 modified = true; 535 return; 536 } 537 if (part) { 538 part.querySelectorAll("direction").forEach((el) => { 539 const text = (el.textContent || "").trim(); 540 if (!showChords && text.includes("â")) { 541 el.remove(); 542 modified = true; 543 } 544 if (!showRoman && text.includes("â")) { 545 el.remove(); 546 modified = true; 547 } 548 if (!showChords && !showRoman && (text.includes("â") || text.includes("Key:"))) { 549 el.remove(); 550 modified = true; 551 } 552 if (text.includes("â") || text.includes("Key:")) { 553 el.setAttribute("placement", "above"); 554 el.setAttribute("default-y", "100"); 555 modified = true; 556 } else if (text.includes("â")) { 557 el.setAttribute("placement", "above"); 558 el.setAttribute("default-y", "70"); 559 modified = true; 560 } else if (text.includes("â")) { 561 el.setAttribute("placement", "above"); 562 el.setAttribute("default-y", "40"); 563 modified = true; 564 } 565 }); 566 if (!showChords) { 567 part.querySelectorAll("harmony:not([type='roman'])").forEach((el) => { 568 el.remove(); 569 modified = true; 570 }); 571 } 572 if (!showRoman) { 573 part.querySelectorAll("harmony[type='roman']").forEach((el) => { 574 el.remove(); 575 modified = true; 576 }); 577 } 578 const measures = part.querySelectorAll("measure"); 579 measures.forEach((measure) => { 580 let currentTime = 0; 581 const seenLyricOnsets = /* @__PURE__ */ new Set(); 582 let previousNoteOnset = 0; 583 const children = Array.from(measure.childNodes); 584 children.forEach((childNode) => { 585 var _a, _b, _c; 586 if (childNode.nodeType !== 1) return; 587 const el = childNode; 588 const tagName = el.tagName.toLowerCase(); 589 if (tagName === "note") { 590 const isChord = el.querySelector("chord") !== null; 591 let noteOnset = 0; 592 if (isChord) { 593 noteOnset = previousNoteOnset; 594 } else { 595 noteOnset = currentTime; 596 previousNoteOnset = currentTime; 597 } 598 const durationText = (_a = el.querySelector("duration")) == null ? void 0 : _a.textContent; 599 const duration2 = durationText ? parseInt(durationText, 10) : 0; 600 if (!isChord) { 601 currentTime += duration2; 602 } 603 const lyrics = el.querySelectorAll("lyric"); 604 lyrics.forEach((lyric) => { 605 if (seenLyricOnsets.has(noteOnset)) { 606 lyric.remove(); 607 modified = true; 608 } else { 609 seenLyricOnsets.add(noteOnset); 610 } 611 }); 612 } else if (tagName === "backup") { 613 const durationText = (_b = el.querySelector("duration")) == null ? void 0 : _b.textContent; 614 const duration2 = durationText ? parseInt(durationText, 10) : 0; 615 currentTime = Math.max(0, currentTime - duration2); 616 } else if (tagName === "forward") { 617 const durationText = (_c = el.querySelector("duration")) == null ? void 0 : _c.textContent; 618 const duration2 = durationText ? parseInt(durationText, 10) : 0; 619 currentTime += duration2; 620 } 621 }); 622 }); 623 } 624 if (displayName) { 625 let partName = scorePart.querySelector("part-name"); 626 if (!partName) { 627 partName = xmlDoc.createElement("part-name"); 628 scorePart.appendChild(partName); 629 } 630 partName.textContent = displayName; 631 let partAbbr = scorePart.querySelector("part-abbr, part-abbreviation"); 632 if (!partAbbr) { 633 partAbbr = xmlDoc.createElement("part-abbreviation"); 634 scorePart.appendChild(partAbbr); 635 } 636 partAbbr.textContent = displayName; 637 scorePart.querySelectorAll("part-name-display, part-abbreviation-display").forEach((el) => el.remove()); 638 modified = true; 639 } 640 }); 641 } 642 const effectiveIsMelody = isMelodyMode && (melodySource === "standard" || !melodySource); 643 try { 644 const displayTitle = effectiveIsMelody && title ? `${title} (Melody)` : title || "HamieNET.com Score"; 645 const scorePartwise = xmlDoc.querySelector("score-partwise"); 646 const mei = xmlDoc.querySelector("mei"); 647 if (scorePartwise) { 648 let work = xmlDoc.querySelector("work"); 649 if (!work) { 650 work = xmlDoc.createElement("work"); 651 scorePartwise.insertBefore(work, scorePartwise.firstChild); 652 } 653 let workTitle = work.querySelector("work-title"); 654 if (!workTitle) { 655 workTitle = xmlDoc.createElement("work-title"); 656 work.appendChild(workTitle); 657 } 658 workTitle.textContent = displayTitle; 659 modified = true; 660 } else if (mei) { 661 let meiHead = xmlDoc.querySelector("meiHead"); 662 if (!meiHead) { 663 meiHead = xmlDoc.createElement("meiHead"); 664 mei.insertBefore(meiHead, mei.firstChild); 665 } 666 let fileDesc = meiHead.querySelector("fileDesc"); 667 if (!fileDesc) { 668 fileDesc = xmlDoc.createElement("fileDesc"); 669 meiHead.appendChild(fileDesc); 670 } 671 let titleStmt = fileDesc.querySelector("titleStmt"); 672 if (!titleStmt) { 673 titleStmt = xmlDoc.createElement("titleStmt"); 674 fileDesc.appendChild(titleStmt); 675 } 676 let t = titleStmt.querySelector("title"); 677 if (!t) { 678 t = xmlDoc.createElement("title"); 679 titleStmt.appendChild(t); 680 } 681 t.textContent = displayTitle; 682 modified = true; 683 } 684 } catch (e) { 685 } 686 const ids = /* @__PURE__ */ new Set(); 687 const measureIdMap = {}; 688 let docMeasureIndex = 1; 689 xmlDoc.querySelectorAll("[id], [*|id]").forEach((el) => { 690 const id = el.getAttribute("xml:id") || el.getAttribute("id"); 691 if (id) { 692 ids.add(id); 693 if (el.localName === "measure" || el.tagName.toLowerCase().endsWith("measure")) { 694 const rawN = el.getAttribute("n") || el.getAttribute("number"); 695 const n = rawN || (docMeasureIndex++).toString(); 696 if (n) { 697 measureIdMap[n.toString()] = id; 698 } 699 } 700 } 701 }); 702 availableIdsRef.current = ids; 703 measureIdMapRef.current = measureIdMap; 704 const finalData = modified ? new XMLSerializer().serializeToString(xmlDoc) : localXmlString; 705 finalXmlDataRef.current = finalData;
706 setLoadingProgress({ percent: 75, stage: "Engraving sheet music layout..." }); 707 await new Promise((r) => setTimeout(r, 0)); 708 verovioToolkit.setOptions({ 709 header: "none", 710 footer: "none", 711 font: "Bravura" 712 }); 713 const success = verovioToolkit.loadData(finalData); 714 if (success === true || success === 1) { 715 const sourceKey = `${midiId}_${quantization}_${isMelodyMode}_${melodySource}`; 716 currentSourceRef.current = sourceKey; 717 try { 718 const renderedMei = verovioToolkit.getMEI ? verovioToolkit.getMEI({ scoreBased: true }) : null; 719 if (renderedMei) { 720 const meiDoc = parser.parseFromString(renderedMei, "text/xml"); 721 let meiMeasureIndex = 1; 722 meiDoc.querySelectorAll("measure").forEach((el) => { 723 const id = el.getAttribute("xml:id") || el.getAttribute("id"); 724 const rawN = el.getAttribute("n") || el.getAttribute("number"); 725 const n = rawN || (meiMeasureIndex++).toString(); 726 if (id && n) { 727 measureIdMap[n.toString()] = id; 728 ids.add(id); 729 } 730 }); 731 } 732 } catch (e) { 733 console.warn("[SheetMusicEngine] Failed to enrich measure IDs from Verovio MEI:", e); 734 } 735 try { 736 const targetPage = Math.min(Math.max(1, currentPage), verovioToolkit.getPageCount() || 1); 737 const svg = verovioToolkit.renderToSVG(targetPage, {}); 738 if (svg) { 739 setSvgContent(svg); 740 const vbMatch = svg.match(/viewBox="[-\d.]+\s+[-\d.]+\s+[-\d.]+\s+([-\d.]+)"/); 741 if (vbMatch) setSvgHeight(parseFloat(vbMatch[1])); 742 } 743 lastRenderedPageRef.current = targetPage; 744 } catch (e) { 745 } 746 setIsDataLoaded(true); 747 } 748 } catch (e) { 749 console.error("[SheetMusicEngine] Load error:", e); 750 } finally { 751 setLoading(false); 752 } 753 }; 754 loadScore(); 755 }, [verovioToolkit, quantization, scorePath, internalXmlData, isMelodyMode, melodySource, title, processedInstruments, trackVisibilityStr, showChords, showRoman, midiId]); 756 reactExports.useEffect(() => { 757 if (!verovioToolkit || !isDataLoaded || dimensions.width < 50) return; 758 const updateLayout = () => { 759 try { 760 const minSpacingStaff = Math.max(12, Math.round(18 * (100 / Math.max(20, scale)))); 761 const minSpacingSystem = Math.max(16, minSpacingStaff + 8); 762 const calculatedSpacingStaff = Math.max(minSpacingStaff, Math.round(density / 50 * 14)); 763 const calculatedSpacingSystem = Math.max(minSpacingSystem, Math.round(density / 50 * 16)); 764 const calculatedSpacingLinear = Math.max(0, density / 50 * 0.25); 765 const calculatedSpacingNonLinear = Math.max(0, density / 50 * 0.6); 766 let options = { 767 scale, 768 adjustPageHeight: false, 769 adjustPageWidth: false, 770 breaks: "auto", 771 font: "Bravura", 772 mnumInterval: showMeasureNumbers ? 1 : 0, 773 pageMarginLeft: 100, 774 pageMarginRight: 100, 775 pageMarginTop: 100, 776 pageMarginBottom: 100, 777 spacingStaff: calculatedSpacingStaff, 778 spacingSystem: calculatedSpacingSystem, 779 spacingLinear: calculatedSpacingLinear, 780 spacingNonLinear: calculatedSpacingNonLinear 781 }; 782 if (layoutMode === "a4") { 783 options = { 784 ...options, 785 pageWidth: 2100 * (100 / scale), 786 pageHeight: 2970 * (100 / scale), 787 pageMarginTop: 150 * (100 / scale), 788 pageMarginBottom: 50 * (100 / scale), 789 pageMarginLeft: 50 * (100 / scale), 790 pageMarginRight: 50 * (100 / scale), 791 header: "none", 792 footer: "none", 793 justifyVertically: true, 794 usePgHeaderForAll: false 795 }; 796 } else if (layoutMode === "a3") { 797 options = { 798 ...options, 799 pageWidth: 4200 * (100 / scale), 800 pageHeight: 2970 * (100 / scale), 801 pageMarginTop: 150 * (100 / scale), 802 pageMarginBottom: 50 * (100 / scale), 803 pageMarginLeft: 50 * (100 / scale), 804 pageMarginRight: 50 * (100 / scale), 805 header: "none", 806 footer: "none", 807 justifyVertically: true, 808 usePgHeaderForAll: false 809 }; 810 } else if (layoutMode === "screen-paged") { 811 const pWidth = Math.max(100, dimensions.width * (100 / scale)); 812 options = { ...options, footer: "none", header: "none", pageWidth: pWidth, pageHeight: 100, pageMarginTop: 50, pageMarginBottom: 0, pageMarginLeft: 0, pageMarginRight: 0, adjustPageHeight: 1, justifyVertically: false }; 813 } else { 814 options = { ...options, footer: "none", header: "none", pageWidth: 1e5, pageHeight: 100, pageMarginTop: 50, pageMarginBottom: 0, pageMarginLeft: 0, pageMarginRight: 0, breaks: "none", adjustPageHeight: 1, justifyVertically: false }; 815 } 816 const optionsStr = JSON.stringify(options); 817 let layoutChanged = false;
818 if (optionsStr !== lastOptionsRef.current) { 819 verovioToolkit.setOptions(options); 820 if ((layoutMode === "a4" || layoutMode === "a3") && finalXmlDataRef.current) { 821 verovioToolkit.loadData(finalXmlDataRef.current); 822 } else { 823 verovioToolkit.redoLayout(); 824 } 825 lastOptionsRef.current = optionsStr; 826 const newPageCount = verovioToolkit.getPageCount(); 827 setPageCount(newPageCount); 828 layoutChanged = true; 829 } 830 if (layoutChanged || currentPage !== lastRenderedPageRef.current) { 831 const newPageCount = verovioToolkit.getPageCount(); 832 const targetPage = Math.min(Math.max(1, currentPage), newPageCount || 1); 833 const svg = verovioToolkit.renderToSVG(targetPage, {}); 834 if (svg) { 835 setSvgContent(svg); 836 const vbMatch = svg.match(/viewBox="[-\d.]+\s+[-\d.]+\s+[-\d.]+\s+([-\d.]+)"/); 837 if (vbMatch) setSvgHeight(parseFloat(vbMatch[1])); 838 } 839 lastRenderedPageRef.current = currentPage; 840 } 841 } catch (e) { 842 console.error("[SheetMusicEngine] Layout Update Error:", e); 843 } 844 }; 845 updateLayout(); 846 }, [verovioToolkit, isDataLoaded, layoutMode, scale, dimensions.width, dimensions.height, density, showMeasureNumbers, showChords, showRoman, currentPage]); 847 reactExports.useEffect(() => { 848 if (!verovioToolkit || !isDataLoaded) { 849 setSyncPoints([]); 850 return; 851 } 852 const points = []; 853 const measureBoundaries = []; 854 const ids = availableIdsRef.current; 855 const visualMeasureKeys = Object.keys(measureIdMapRef.current); 856 const maxAudioMeasure = measureMap.length > 0 ? Math.max(...measureMap.map((m) => m && m.measure != null ? m.measure : 0)) : 0; 857 const hasVisualRepeats = visualMeasureKeys.length > 0 && maxAudioMeasure > visualMeasureKeys.length || quantization === 700; 858 if (hasVisualRepeats) { 859 let vDuration = 0; 860 try { 861 const rawTm = verovioToolkit.renderToTimemap ? verovioToolkit.renderToTimemap() : null; 862 const tm = typeof rawTm === "string" ? JSON.parse(rawTm) : rawTm; 863 if (Array.isArray(tm) && tm.length > 0) { 864 const lastEvent = tm[tm.length - 1]; 865 if (lastEvent && lastEvent.tstamp > 0) { 866 vDuration = lastEvent.tstamp / 1e3; 867 } 868 } 869 } catch (e) { 870 } 871 if (vDuration <= 0) { 872 try { 873 const lastPage = verovioToolkit.getPageCount(); 874 const svg = verovioToolkit.renderToSVG(lastPage, {}); 875 const parser = new DOMParser(); 876 const doc = parser.parseFromString(svg, "image/svg+xml"); 877 const measures = doc.querySelectorAll(".measure"); 878 if (measures.length > 0) { 879 const lastId = measures[measures.length - 1].getAttribute("id"); 880 const lastStart = verovioToolkit.getTimeForElement(lastId) / 1e3; 881 if (lastStart > 0) { 882 const approxMeasureDur = visualMeasureKeys.length > 0 ? lastStart / visualMeasureKeys.length : 2; 883 vDuration = lastStart + approxMeasureDur; 884 } 885 } 886 } catch (e) { 887 } 888 } 889 if (vDuration <= 0) vDuration = duration || 30; 890 const numTuneLoops = duration > 0 && vDuration > 5 ? Math.max(1, Math.round(duration / vDuration)) : 1; 891 const loopAudioDur = duration > 0 ? duration / numTuneLoops : vDuration; 892 for (let loop = 0; loop < numTuneLoops; loop++) { 893 const loopStartAudio = loop * loopAudioDur; 894 const loopEndAudio = loop === numTuneLoops - 1 ? duration : (loop + 1) * loopAudioDur; 895 points.push({ vTime: 0, aTime: loopStartAudio }); 896 points.push({ vTime: vDuration, aTime: loopEndAudio - 1e-3 }); 897 } 898 } else { 899 measureMap.forEach((s) => { 900 if (!s || s.measure == null) return; 901 if (s.beat && s.beat > 1) return; 902 const mId = s.measure.toString(); 903 let vTimeMs = -1; 904 const mappedId = measureIdMapRef.current[mId]; 905 if (mappedId && ids.has(mappedId)) { 906 vTimeMs = verovioToolkit.getTimeForElement(mappedId); 907 } 908 if (vTimeMs < 0 && ids.has(mId)) vTimeMs = verovioToolkit.getTimeForElement(mId); 909 if (vTimeMs < 0 && ids.has(`m-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`m-${mId}`); 910 if (vTimeMs < 0 && ids.has(`measure-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`measure-${mId}`); 911 if (vTimeMs < 0 && ids.has(`metric-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`metric-${mId}`); 912 if (vTimeMs < 0) { 913 const longId = `measure-000000000${mId}`.slice(-18); 914 if (ids.has(longId)) vTimeMs = verovioToolkit.getTimeForElement(longId); 915 } 916 if (vTimeMs >= 0) { 917 measureBoundaries.push({ m: s.measure, vTime: vTimeMs / 1e3, aTime: s.time }); 918 } 919 });
920 measureMap.forEach((s) => { 921 if (!s || s.measure == null) return; 922 s.measure.toString(); 923 const boundary = measureBoundaries.find((b) => b.m === s.measure); 924 if (boundary && (!s.beat || s.beat === 1)) { 925 points.push({ vTime: boundary.vTime, aTime: s.time }); 926 return; 927 } 928 if (s.beat && s.beat > 1) { 929 const currentM = measureBoundaries.find((b) => b.m === s.measure); 930 const nextM = measureBoundaries.find((b) => b.m === s.measure + 1); 931 if (currentM && nextM) { 932 const mDurV = nextM.vTime - currentM.vTime; 933 const ratio = (s.time - currentM.aTime) / (nextM.aTime - currentM.aTime); 934 const interpolatedVTime = currentM.vTime + mDurV * ratio; 935 points.push({ vTime: interpolatedVTime, aTime: s.time }); 936 } else if (currentM) { 937 points.push({ vTime: currentM.vTime + (s.beat - 1), aTime: s.time }); 938 } 939 } 940 }); 941 if (points.length === 0) { 942 sections.forEach((s) => { 943 var _a; 944 const mId = (_a = s.measure) == null ? void 0 : _a.toString(); 945 if (!mId) return; 946 let vTimeMs = -1; 947 const mappedId = measureIdMapRef.current[mId]; 948 if (mappedId && ids.has(mappedId)) { 949 vTimeMs = verovioToolkit.getTimeForElement(mappedId); 950 } 951 if (vTimeMs < 0 && ids.has(mId)) vTimeMs = verovioToolkit.getTimeForElement(mId); 952 if (vTimeMs < 0 && ids.has(`m-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`m-${mId}`); 953 if (vTimeMs < 0 && ids.has(`measure-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`measure-${mId}`); 954 if (vTimeMs < 0) return; 955 points.push({ vTime: vTimeMs / 1e3, aTime: s.time }); 956 }); 957 } 958 } 959 const uniquePoints = points.sort((a, b) => a.aTime - b.aTime).filter((p, i, arr) => i === 0 || p.aTime > arr[i - 1].aTime); 960 if (uniquePoints.length === 0 || uniquePoints[0].aTime > 0) { 961 uniquePoints.unshift({ vTime: 0, aTime: 0 }); 962 } 963 if (duration > 0 && uniquePoints.length > 0) { 964 const last = uniquePoints[uniquePoints.length - 1]; 965 if (last.aTime < duration) { 966 try { 967 const lastPage = verovioToolkit.getPageCount(); 968 const svg = verovioToolkit.renderToSVG(lastPage, {}); 969 const parser = new DOMParser(); 970 const doc = parser.parseFromString(svg, "image/svg+xml"); 971 const measures = doc.querySelectorAll(".measure"); 972 if (measures.length > 0) { 973 const lastId = measures[measures.length - 1].getAttribute("id"); 974 const lastVTime = verovioToolkit.getTimeForElement(lastId) / 1e3; 975 uniquePoints.push({ vTime: Math.max(lastVTime, last.vTime + 0.1), aTime: duration }); 976 } else { 977 uniquePoints.push({ vTime: last.vTime + (duration - last.aTime), aTime: duration }); 978 } 979 } catch (e) { 980 uniquePoints.push({ vTime: last.vTime + (duration - last.aTime), aTime: duration }); 981 } 982 } 983 } 984 uniquePoints.sort((a, b) => a.aTime - b.aTime); 985 setSyncPoints(uniquePoints); 986 setLoadingProgress({ percent: 100, stage: "Score ready" }); 987 }, [verovioToolkit, isDataLoaded, sections, measureMap, duration, quantization]); 988 reactExports.useEffect(() => { 989 if (!isDataLoaded) return; 990 const params = new URLSearchParams(window.location.search); 991 const pageParam = params.get("page"); 992 if (pageParam) { 993 const targetPage = parseInt(pageParam, 10); 994 if (!isNaN(targetPage) && targetPage > 0) { 995 setCurrentPage(targetPage); 996 } 997 } 998 }, [isDataLoaded]); 999 return { 1000 verovioToolkit, 1001 loading, 1002 loadingProgress, 1003 setLoading, 1004 isDataLoaded, 1005 pageCount, 1006 currentPage, 1007 setCurrentPage, 1008 svgContent, 1009 svgHeight, 1010 syncPoints, 1011 renderPage 1012 }; 1013}; 1014const useSheetMusicPlayback = (props) => { 1015 const { 1016 verovioToolkit, 1017 isPlaying, 1018 currentTime, 1019 duration, 1020 audioLatency, 1021 onSeek, 1022 layoutMode, 1023 scale, 1024 pageTurnLookahead, 1025 dimensions, 1026 currentPage, 1027 setCurrentPage, 1028 pageCount, 1029 renderPage, 1030 syncPoints, 1031 containerRef, 1032 paperRef, 1033 cursorRef, 1034 showCursor, 1035 showHighlight, 1036 isDataLoaded = false, 1037 svgContent 1038 } = props; 1039 const [pageTriggerTimes, setPageTriggerTimes] = reactExports.useState([]); 1040 const isInternalScrollRef = reactExports.useRef(false); 1041 const lastManualScrollTimeRef = reactExports.useRef(0); 1042 const lastManualPageChangeRef = reactExports.useRef(0); 1043 const lastManualPageTimeRef = reactExports.useRef(-1); 1044 reactExports.useRef(null); 1045 const lastPropTimeRef = reactExports.useRef(currentTime); 1046 const isPendingSeekScrollRef = reactExports.useRef(false); 1047 const lastSystemTopRef = reactExports.useRef(-1); 1048 const lastTimeRef = reactExports.useRef({ time: currentTime, local: performance.now() }); 1049 const elementBoundsCacheRef = reactExports.useRef(/* @__PURE__ */ new Map()); 1050 const lastCachedPageRef = reactExports.useRef(-1); 1051 const highlightedNoteIdsRef = reactExports.useRef(/* @__PURE__ */ new Set()); 1052 const eventRangeRef = reactExports.useRef({ 1053 startTime: -1, 1054 startX: 0, 1055 nextTime: 0, 1056 nextX: 0, 1057 ids: "", 1058 systemTop: 0, 1059 systemBottom: 0 1060 }); 1061 const lastLayoutRef = reactExports.useRef({ scale, layoutMode, width: dimensions.width, height: dimensions.height }); 1062 const lastSvgContentRef = reactExports.useRef(svgContent); 1063 reactExports.useEffect(() => { 1064 lastTimeRef.current = { time: currentTime, local: performance.now() }; 1065 }, [currentTime]); 1066 const renderingPageRef = reactExports.useRef(currentPage); 1067 reactExports.useEffect(() => { 1068 renderingPageRef.current = currentPage; 1069 }, [currentPage]); 1070 const getSmoothTime = reactExports.useCallback(() => { 1071 if (!isPlaying) return currentTime; 1072 const now = performance.now(); 1073 const dt = (now - lastTimeRef.current.local) / 1e3; 1074 return lastTimeRef.current.time + Math.min(dt, 0.5); 1075 }, [currentTime, isPlaying]); 1076 const audioToVerovioTime = reactExports.useCallback((audioTime) => { 1077 if (syncPoints.length < 2) return audioTime; 1078 let i = 0; 1079 for (; i < syncPoints.length - 2; i++) { 1080 if (audioTime <= syncPoints[i + 1].aTime) break; 1081 } 1082 const p1 = syncPoints[i]; 1083 const p2 = syncPoints[i + 1]; 1084 const aSpan = p2.aTime - p1.aTime; 1085 const vSpan = p2.vTime - p1.vTime; 1086 if (aSpan <= 0) return p1.vTime; 1087 return p1.vTime + (audioTime - p1.aTime) / aSpan * vSpan; 1088 }, [syncPoints, duration]); 1089 const verovioToAudioTime = reactExports.useCallback((vTime) => { 1090 if (syncPoints.length < 2) return vTime; 1091 let i = 0; 1092 for (; i < syncPoints.length - 2; i++) { 1093 if (vTime <= syncPoints[i + 1].vTime) break; 1094 } 1095 const p1 = syncPoints[i]; 1096 const p2 = syncPoints[i + 1]; 1097 const vSpan = p2.vTime - p1.vTime; 1098 const aSpan = p2.aTime - p1.aTime; 1099 if (vSpan <= 0) return p1.aTime; 1100 return p1.aTime + (vTime - p1.vTime) / vSpan * aSpan; 1101 }, [syncPoints, duration]); 1102 reactExports.useEffect(() => { 1103 if (!verovioToolkit || !isDataLoaded || layoutMode === "screen-moving" || pageCount === 0) { 1104 setPageTriggerTimes([]); 1105 return; 1106 } 1107 let active = true; 1108 const triggers = []; 1109 const parser = new DOMParser(); 1110 const processBatch = (startPage) => { 1111 if (!active) return; 1112 const endPage = Math.min(startPage + 4, pageCount); 1113 for (let p = startPage; p <= endPage; p++) { 1114 try { 1115 const svg = verovioToolkit.renderToSVG(p, {}); 1116 const doc = parser.parseFromString(svg, "image/svg+xml"); 1117 const firstMeasure = doc.querySelector(".measure"); 1118 if (firstMeasure) { 1119 const id = firstMeasure.getAttribute("id"); 1120 const startTime = verovioToolkit.getTimeForElement(id); 1121 if (startTime > 0) { 1122 const prevElements = verovioToolkit.getElementsAtTime(startTime - 1); 1123 if (prevElements && prevElements.measure && prevElements.measure.length > 0) { 1124 const prevMeasureId = prevElements.measure[0]; 1125 const prevStartTime = verovioToolkit.getTimeForElement(prevMeasureId); 1126 if (prevStartTime > 0 && prevStartTime < startTime) { 1127 const measureDuration = startTime - prevStartTime; 1128 triggers[p] = startTime - measureDuration / 2; 1129 } else { 1130 triggers[p] = startTime - 500; 1131 } 1132 } else { 1133 triggers[p] = startTime - 500; 1134 } 1135 } 1136 } 1137 } catch (e) { 1138 } 1139 }
1140 if (endPage < pageCount) { 1141 setTimeout(() => processBatch(endPage + 1), 0); 1142 } else { 1143 if (active) setPageTriggerTimes(triggers); 1144 } 1145 }; 1146 processBatch(2); 1147 return () => { 1148 active = false; 1149 }; 1150 }, [verovioToolkit, isDataLoaded, layoutMode, scale, pageCount]); 1151 const getElementBounds = reactExports.useCallback((id) => { 1152 var _a; 1153 const cleanId = id.replace(/-rend\d+$/, ""); 1154 const cache = elementBoundsCacheRef.current; 1155 if (cache.has(cleanId)) return cache.get(cleanId); 1156 const el = (_a = containerRef.current) == null ? void 0 : _a.querySelector(`[id="${cleanId}"]`); 1157 if (!el || !paperRef.current) return null; 1158 const rect = el.getBoundingClientRect(); 1159 const paperRect = paperRef.current.getBoundingClientRect(); 1160 const scaleFactor = paperRect.width / (paperRef.current.offsetWidth || 1); 1161 const left = (rect.left - paperRect.left) / scaleFactor; 1162 const system = el.closest(".system"); 1163 const sysRect = system == null ? void 0 : system.getBoundingClientRect(); 1164 const systemTop = sysRect ? (sysRect.top - paperRect.top) / scaleFactor : (rect.top - paperRect.top) / scaleFactor; 1165 const systemBottom = systemTop + ((sysRect == null ? void 0 : sysRect.height) || rect.height) / scaleFactor; 1166 const bounds = { left, systemTop, systemBottom }; 1167 cache.set(cleanId, bounds); 1168 return bounds; 1169 }, [containerRef, paperRef]); 1170 const safeGetElementsAtTime = reactExports.useCallback((timeMs) => { 1171 if (!verovioToolkit || !isDataLoaded) return null; 1172 try { 1173 return verovioToolkit.getElementsAtTime(timeMs); 1174 } catch (e) { 1175 return null; 1176 } 1177 }, [verovioToolkit, isDataLoaded]); 1178 reactExports.useEffect(() => { 1179 if (!verovioToolkit || !isDataLoaded || !containerRef.current || !paperRef.current) return; 1180 let animationFrame; 1181 const update = () => { 1182 var _a, _b, _c, _d, _e, _f; 1183 const paperWidth = ((_a = paperRef.current) == null ? void 0 : _a.offsetWidth) || 0; 1184 const paperRect = (_b = paperRef.current) == null ? void 0 : _b.getBoundingClientRect(); 1185 if (paperWidth <= 0 || !paperRect || paperRect.width <= 0) { 1186 if (isPlaying) animationFrame = requestAnimationFrame(update); 1187 return; 1188 } 1189 const layoutChanged = lastLayoutRef.current.scale !== scale || lastLayoutRef.current.layoutMode !== layoutMode || lastLayoutRef.current.width !== dimensions.width || lastLayoutRef.current.height !== dimensions.height; 1190 const svgChanged = svgContent !== void 0 && lastSvgContentRef.current !== svgContent; 1191 if (svgChanged) { 1192 lastSvgContentRef.current = svgContent; 1193 } 1194 if (layoutChanged || svgChanged || renderingPageRef.current !== lastCachedPageRef.current) { 1195 lastLayoutRef.current = { scale, layoutMode, width: dimensions.width, height: dimensions.height }; 1196 elementBoundsCacheRef.current.clear(); 1197 lastCachedPageRef.current = renderingPageRef.current; 1198 eventRangeRef.current = { 1199 startTime: -1, 1200 startX: 0, 1201 nextTime: 0, 1202 nextX: 0, 1203 ids: "", 1204 systemTop: 0, 1205 systemBottom: 0 1206 }; 1207 isPendingSeekScrollRef.current = true; 1208 } 1209 const smoothTime = getSmoothTime(); 1210 let calibratedTime = audioToVerovioTime(smoothTime); 1211 const timeJump = Math.abs(currentTime - lastPropTimeRef.current); 1212 const isSeek = timeJump > 1; 1213 if (isSeek) { 1214 isPendingSeekScrollRef.current = true; 1215 } 1216 lastPropTimeRef.current = currentTime; 1217 const timeMs = Math.round((calibratedTime - audioLatency) * 1e3); 1218 if (isSeek) { 1219 eventRangeRef.current = { 1220 startTime: -1, 1221 startX: 0, 1222 nextTime: 0, 1223 nextX: 0, 1224 ids: "", 1225 systemTop: 0, 1226 systemBottom: 0 1227 }; 1228 } 1229 if (isNaN(timeMs)) { 1230 if (isPlaying) animationFrame = requestAnimationFrame(update); 1231 return; 1232 } 1233 let elements = safeGetElementsAtTime(timeMs) || { page: 1, notes: [] }; 1234 if (timeMs <= 0 || calibratedTime <= 0) elements = { page: 1, notes: [] }; 1235 if (elements && elements.page > 0) { 1236 let targetPage = elements.page; 1237 if (layoutMode !== "screen-moving") { 1238 if (timeMs <= 0 || calibratedTime <= 0) targetPage = 1; 1239 else if (isPlaying) {
1240 if (pageTurnLookahead === -1) { 1241 const nextPage = currentPage + 1; 1242 const triggerTime = pageTriggerTimes[nextPage]; 1243 if (triggerTime !== void 0 && triggerTime > 0 && timeMs >= triggerTime && nextPage <= pageCount) { 1244 targetPage = Math.max(targetPage, nextPage); 1245 } 1246 } else if (pageTurnLookahead > 0) { 1247 const lookaheadMs = pageTurnLookahead * 1e3; 1248 const lookaheadElements = safeGetElementsAtTime(timeMs + lookaheadMs); 1249 if (lookaheadElements && lookaheadElements.page > 0 && lookaheadElements.page > targetPage) { 1250 targetPage = Math.max(targetPage, lookaheadElements.page); 1251 } 1252 } 1253 } 1254 if (!isSeek && isPlaying && targetPage < currentPage) { 1255 targetPage = currentPage; 1256 } 1257 } 1258 const timeSinceManualChange = Date.now() - lastManualPageChangeRef.current; 1259 const shouldTurnPage = (isSeek || timeSinceManualChange >= 2e3) && targetPage !== currentPage && targetPage !== renderingPageRef.current && (isPlaying || isSeek); 1260 if (shouldTurnPage) { 1261 renderingPageRef.current = targetPage; 1262 setCurrentPage(targetPage); 1263 renderPage(targetPage); 1264 if (containerRef.current) containerRef.current.scrollTo({ top: 0, behavior: "smooth" }); 1265 if (isPlaying) { 1266 animationFrame = requestAnimationFrame(update); 1267 return; 1268 } 1269 } 1270 const currentIds = (elements.notes || []).join(","); 1271 const range = eventRangeRef.current; 1272 let currentInterpolatedX = range.startX; 1273 if (range.nextTime > range.startTime) { 1274 const progress = Math.max(0, Math.min(1, (timeMs - range.startTime) / (range.nextTime - range.startTime))); 1275 if (range.nextX >= range.startX) currentInterpolatedX = range.startX + (range.nextX - range.startX) * progress; 1276 } 1277 const currentTrigger = pageTriggerTimes[currentPage]; 1278 const isEarlyLookaheadActive = currentTrigger !== void 0 && currentTrigger > 0 && timeMs < currentTrigger; 1279 if (layoutMode !== "screen-moving" && isEarlyLookaheadActive) { 1280 if (cursorRef.current) cursorRef.current.style.display = "none"; 1281 if (isPlaying) animationFrame = requestAnimationFrame(update); 1282 return; 1283 } 1284 if (currentIds !== range.ids || currentIds === "" && timeMs >= range.nextTime || timeMs < range.startTime || range.startTime === -1) { 1285 highlightedNoteIdsRef.current.forEach((id) => { 1286 var _a2; 1287 const cleanId = id.replace(/-rend\d+$/, ""); 1288 const el = (_a2 = containerRef.current) == null ? void 0 : _a2.querySelector(`[id="${cleanId}"]`); 1289 if (el) el.classList.remove("highlight-note"); 1290 }); 1291 highlightedNoteIdsRef.current.clear(); 1292 if (elements.notes && elements.notes.length > 0) { 1293 let startX = Infinity, systemTop = Infinity, systemBottom = -Infinity, found = false; 1294 elements.notes.forEach((id) => { 1295 var _a2; 1296 const cleanId = id.replace(/-rend\d+$/, ""); 1297 if (showHighlight) { 1298 const el = (_a2 = containerRef.current) == null ? void 0 : _a2.querySelector(`[id="${cleanId}"]`); 1299 if (el) { 1300 el.classList.add("highlight-note"); 1301 highlightedNoteIdsRef.current.add(cleanId); 1302 } 1303 } 1304 const bounds = getElementBounds(cleanId); 1305 if (bounds) { 1306 startX = Math.min(startX, bounds.left); 1307 systemTop = Math.min(systemTop, bounds.systemTop); 1308 systemBottom = Math.max(systemBottom, bounds.systemBottom); 1309 found = true; 1310 } 1311 }); 1312 if (found) { 1313 if (range.startTime !== -1 && timeMs >= range.startTime && Math.abs(systemTop - range.systemTop) < 10 && startX < currentInterpolatedX) startX = currentInterpolatedX; 1314 let nextTime = timeMs + 500, nextX = startX + 20; 1315 for (let t = timeMs + 50; t <= timeMs + 2e3; t += 50) { 1316 const future = safeGetElementsAtTime(t); 1317 if (future && future.notes && future.notes.join(",") !== currentIds) { 1318 nextTime = t; 1319 let fMinX = Infinity;
1320 future.notes.forEach((id) => { 1321 const bounds = getElementBounds(id); 1322 if (bounds) { 1323 fMinX = Math.min(fMinX, bounds.left); 1324 } 1325 }); 1326 if (fMinX !== Infinity) { 1327 nextX = fMinX; 1328 } 1329 break; 1330 } 1331 } 1332 if (nextX < startX - 100) { 1333 const cleanFirst = elements.notes[0].replace(/-rend\d+$/, ""); 1334 const system = (_d = (_c = containerRef.current) == null ? void 0 : _c.querySelector(`[id="${cleanFirst}"]`)) == null ? void 0 : _d.closest(".system"); 1335 const paperRect2 = paperRef.current.getBoundingClientRect(); 1336 const scaleFactor = paperRect2.width / (paperRef.current.offsetWidth || 1); 1337 if (system) nextX = (system.getBoundingClientRect().right - paperRect2.left) / scaleFactor; 1338 } 1339 eventRangeRef.current = { startTime: timeMs, startX, nextTime, nextX, ids: currentIds, systemTop, systemBottom }; 1340 } 1341 } else { 1342 let nextTime = timeMs + 500, nextX = range.nextX || 0, foundNext = false, systemTop = range.systemTop || 0, systemBottom = range.systemBottom || 0; 1343 if (timeMs <= 0) { 1344 const firstMeasure = (_e = containerRef.current) == null ? void 0 : _e.querySelector(".measure"); 1345 if (firstMeasure) { 1346 const rect = firstMeasure.getBoundingClientRect(); 1347 const paperRect2 = paperRef.current.getBoundingClientRect(); 1348 const scaleFactor = paperRect2.width / (paperRef.current.offsetWidth || 1); 1349 const firstNote = firstMeasure.querySelector(".note, .chord, .rest") || ((_f = containerRef.current) == null ? void 0 : _f.querySelector(".note, .chord, .rest")); 1350 let startX = (rect.left - paperRect2.left) / scaleFactor; 1351 if (firstNote) { 1352 const noteRect = firstNote.getBoundingClientRect(); 1353 startX = Math.max(0, (noteRect.left - paperRect2.left) / scaleFactor - 6); 1354 } 1355 const system = firstMeasure.closest(".system"); 1356 const sysRect = system == null ? void 0 : system.getBoundingClientRect(); 1357 systemTop = sysRect ? (sysRect.top - paperRect2.top) / scaleFactor : (rect.top - paperRect2.top) / scaleFactor; 1358 systemBottom = systemTop + ((sysRect == null ? void 0 : sysRect.height) || rect.height) / scaleFactor; 1359 for (let t = 0; t <= 5e3; t += 50) { 1360 const future = safeGetElementsAtTime(t); 1361 if (future && future.notes && future.notes.length > 0) { 1362 nextTime = t; 1363 let fMinX = Infinity; 1364 future.notes.forEach((id) => { 1365 const bounds = getElementBounds(id); 1366 if (bounds) fMinX = Math.min(fMinX, bounds.left); 1367 }); 1368 if (fMinX !== Infinity) nextX = fMinX; 1369 break; 1370 } 1371 } 1372 if (nextX < startX) { 1373 nextX = startX; 1374 } 1375 eventRangeRef.current = { startTime: 0, startX, nextTime, nextX, ids: "", systemTop, systemBottom }; 1376 foundNext = true; 1377 } 1378 } 1379 if (!foundNext) { 1380 for (let t = timeMs + 50; t <= timeMs + 5e3; t += 50) { 1381 const future = safeGetElementsAtTime(t); 1382 if (future && future.notes && future.notes.length > 0) { 1383 nextTime = t; 1384 let fMinX = Infinity; 1385 future.notes.forEach((id) => { 1386 const bounds = getElementBounds(id); 1387 if (bounds) { 1388 fMinX = Math.min(fMinX, bounds.left); 1389 systemTop = bounds.systemTop; 1390 systemBottom = bounds.systemBottom; 1391 } 1392 }); 1393 if (fMinX !== Infinity) { 1394 nextX = fMinX; 1395 foundNext = true; 1396 } 1397 break; 1398 } 1399 } 1400 if (foundNext) { 1401 let safeStartX = currentInterpolatedX; 1402 if (Math.abs(timeMs - range.startTime) > 1e3 || range.startTime === -1) safeStartX = nextX - Math.min(200, (nextTime - timeMs) * 0.1); 1403 eventRangeRef.current = { startTime: timeMs, startX: safeStartX, nextTime, nextX, ids: "", systemTop, systemBottom }; 1404 } 1405 } 1406 } 1407 } 1408 const r = eventRangeRef.current; 1409 let interpolatedX = r.startX; 1410 if (r.nextTime > r.startTime && r.nextX >= r.startX) { 1411 interpolatedX = r.startX + (r.nextX - r.startX) * Math.max(0, Math.min(1, (timeMs - r.startTime) / (r.nextTime - r.startTime))); 1412 } else if (r.nextTime > r.startTime && r.nextX < r.startX) { 1413 interpolatedX = r.startX; 1414 } 1415 if (currentIds === "" && r.nextX === r.startX) { 1416 interpolatedX = r.startX + (timeMs - r.startTime) * 0.1; 1417 } 1418 const isManualScrollActive = Date.now() - lastManualScrollTimeRef.current < 2e3; 1419 const forceSeekScroll = isPendingSeekScrollRef.current; 1420 if (layoutMode === "screen-moving" && paperRef.current && containerRef.current) { 1421 if (paperRef.current.style.transform) paperRef.current.style.transform = ""; 1422 if (timeMs <= 0 && !isPlaying) { 1423 containerRef.current.scrollLeft = 0; 1424 if (forceSeekScroll) isPendingSeekScrollRef.current = false;
1425 } else if ((isPlaying || forceSeekScroll) && !isManualScrollActive) { 1426 isInternalScrollRef.current = true; 1427 containerRef.current.scrollLeft = Math.max(0, interpolatedX - dimensions.width * 0.2); 1428 if (forceSeekScroll) isPendingSeekScrollRef.current = false; 1429 } 1430 } 1431 if (cursorRef.current) { 1432 const r2 = eventRangeRef.current; 1433 cursorRef.current.style.display = showCursor && r2.systemBottom > r2.systemTop ? "block" : "none"; 1434 cursorRef.current.style.left = `${interpolatedX - 2}px`; 1435 cursorRef.current.style.top = `${r2.systemTop}px`; 1436 cursorRef.current.style.height = `${r2.systemBottom - r2.systemTop}px`; 1437 } 1438 if (layoutMode !== "screen-moving" && (isPlaying || forceSeekScroll || timeMs <= 0) && !isManualScrollActive) { 1439 const container = containerRef.current; 1440 if (container) { 1441 if (timeMs <= 0) { 1442 container.scrollTo({ top: 0, left: 0, behavior: "smooth" }); 1443 lastSystemTopRef.current = -1; 1444 if (forceSeekScroll) isPendingSeekScrollRef.current = false; 1445 } else { 1446 const isOutOfBounds = interpolatedX < container.scrollLeft || interpolatedX > container.scrollLeft + container.clientWidth * 0.8; 1447 if (forceSeekScroll || isOutOfBounds) { 1448 isInternalScrollRef.current = true; 1449 container.scrollTo({ 1450 left: Math.max(0, interpolatedX - container.clientWidth * 0.2), 1451 behavior: forceSeekScroll ? "auto" : "smooth" 1452 }); 1453 } 1454 if (layoutMode !== "screen-paged" && (forceSeekScroll || r.systemTop !== lastSystemTopRef.current)) { 1455 const isVerticalOutOfBounds = r.systemTop < container.scrollTop || r.systemBottom > container.scrollTop + container.clientHeight * 0.9; 1456 if (forceSeekScroll || isVerticalOutOfBounds) { 1457 isInternalScrollRef.current = true; 1458 container.scrollTo({ 1459 top: Math.max(0, r.systemTop - container.clientHeight * 0.2), 1460 behavior: forceSeekScroll ? "auto" : "smooth" 1461 }); 1462 } 1463 lastSystemTopRef.current = r.systemTop; 1464 } 1465 if (forceSeekScroll) isPendingSeekScrollRef.current = false; 1466 } 1467 } 1468 } 1469 } 1470 if (isPlaying) animationFrame = requestAnimationFrame(update); 1471 }; 1472 if (isPlaying) animationFrame = requestAnimationFrame(update); 1473 else update(); 1474 return () => { 1475 if (animationFrame) cancelAnimationFrame(animationFrame); 1476 }; 1477 }, [isPlaying, verovioToolkit, isDataLoaded, safeGetElementsAtTime, showCursor, showHighlight, layoutMode, scale, dimensions, currentPage, renderPage, getSmoothTime, pageTurnLookahead, pageTriggerTimes, pageCount, getElementBounds, audioLatency, audioToVerovioTime, svgContent]); 1478 reactExports.useEffect(() => { 1479 if (isPlaying || !verovioToolkit || !isDataLoaded) return; 1480 const frame = requestAnimationFrame(() => { 1481 var _a, _b, _c; 1482 const paperWidth = ((_a = paperRef.current) == null ? void 0 : _a.offsetWidth) || 0; 1483 if (paperWidth > 0 && containerRef.current && paperRef.current) { 1484 const calibratedTime = audioToVerovioTime(currentTime); 1485 const timeMs = Math.round((calibratedTime - audioLatency) * 1e3); 1486 if (timeMs <= 0) { 1487 const firstMeasure = (_b = containerRef.current) == null ? void 0 : _b.querySelector(".measure"); 1488 if (firstMeasure && paperRef.current && cursorRef.current) { 1489 const rect = firstMeasure.getBoundingClientRect(); 1490 const paperRect = paperRef.current.getBoundingClientRect(); 1491 const scaleFactor = paperRect.width / (paperRef.current.offsetWidth || 1); 1492 const firstNote = firstMeasure.querySelector(".note, .chord, .rest") || ((_c = containerRef.current) == null ? void 0 : _c.querySelector(".note, .chord, .rest")); 1493 let startX = (rect.left - paperRect.left) / scaleFactor; 1494 if (firstNote) { 1495 const noteRect = firstNote.getBoundingClientRect(); 1496 startX = Math.max(0, (noteRect.left - paperRect.left) / scaleFactor - 6); 1497 } 1498 const system = firstMeasure.closest(".system"); 1499 const sysRect = system == null ? void 0 : system.getBoundingClientRect(); 1500 const systemTop = sysRect ? (sysRect.top - paperRect.top) / scaleFactor : (rect.top - paperRect.top) / scaleFactor; 1501 const systemBottom = systemTop + ((sysRect == null ? void 0 : sysRect.height) || rect.height) / scaleFactor; 1502 cursorRef.current.style.display = showCursor ? "block" : "none"; 1503 cursorRef.current.style.left = `${startX - 2}px`; 1504 cursorRef.current.style.top = `${systemTop}px`; 1505 cursorRef.current.style.height = `${systemBottom - systemTop}px`; 1506 if (layoutMode === "screen-moving") { 1507 containerRef.current.scrollLeft = 0; 1508 } 1509 } 1510 return; 1511 } 1512 const elements = safeGetElementsAtTime(timeMs); 1513 if (elements && elements.notes && elements.notes.length > 0) { 1514 const bounds = getElementBounds(elements.notes[0]); 1515 if (bounds && cursorRef.current) { 1516 cursorRef.current.style.display = showCursor ? "block" : "none"; 1517 cursorRef.current.style.left = `${bounds.left - 2}px`; 1518 cursorRef.current.style.top = `${bounds.systemTop}px`; 1519 cursorRef.current.style.height = `${bounds.systemBottom - bounds.systemTop}px`; 1520 if (layoutMode === "screen-moving" && containerRef.current) { 1521 containerRef.current.scrollLeft = Math.max(0, bounds.left - dimensions.width * 0.2); 1522 } 1523 } 1524 } 1525 } 1526 }); 1527 return () => cancelAnimationFrame(frame); 1528 }, [currentTime, isPlaying, verovioToolkit, isDataLoaded, safeGetElementsAtTime, showCursor, getElementBounds, audioLatency, audioToVerovioTime, svgContent, layoutMode, dimensions.width]); 1529 const handleScroll = reactExports.useCallback(() => { 1530 if (isInternalScrollRef.current) { 1531 isInternalScrollRef.current = false;
1532 return; 1533 } 1534 lastManualScrollTimeRef.current = Date.now(); 1535 }, []); 1536 return { 1537 audioToVerovioTime, 1538 verovioToAudioTime, 1539 lastManualPageChangeRef, 1540 lastManualPageTimeRef, 1541 handleScroll, 1542 isInternalScrollRef, 1543 getSmoothTime 1544 }; 1545}; 1546const SheetMusicToolbar = (props) => { 1547 const { 1548 layoutMode, 1549 setLayoutMode, 1550 isAutoZoom, 1551 setIsAutoZoom, 1552 scale, 1553 setScale, 1554 zoomInput, 1555 setZoomInput, 1556 handleZoomChange, 1557 handleZoomBlur, 1558 handleZoomKeyDown, 1559 density, 1560 setDensity, 1561 onToggleMelody, 1562 isMelodyMode, 1563 isMMOMode, 1564 setIsMMOMode, 1565 onGameModeMute, 1566 melodyData, 1567 processedInstruments, 1568 isAnalysisStudioOpen, 1569 setIsAnalysisStudioOpen, 1570 isMenuExpanded, 1571 setIsMenuExpanded, 1572 showChords, 1573 showRoman, 1574 showTimeline, 1575 quantization, 1576 setQuantization, 1577 quantizedScorePaths, 1578 hasRecommended, 1579 handleRecommendQuantization, 1580 pageTurnLookahead, 1581 setPageTurnLookahead, 1582 melodySource, 1583 setMelodySource, 1584 handleDownloadPNG, 1585 handleDownloadPDF, 1586 loading, 1587 theme = "light" 1588 } = props; 1589 const [showDensityPopup, setShowDensityPopup] = reactExports.useState(false); 1590 const getToolbarBg = () => { 1591 if (theme === "light") return "bg-zinc-100 border-b border-zinc-200 text-zinc-800"; 1592 if (theme === "fun") return "bg-[#fff1f2] border-b border-pink-200 text-pink-900"; 1593 return "bg-[#050505] border-b border-white/10 text-white"; 1594 }; 1595 const getSelectorBg = () => { 1596 if (theme === "light") return "flex items-center gap-1 bg-zinc-200/60 border border-zinc-300/50 rounded-lg p-1"; 1597 if (theme === "fun") return "flex items-center gap-1 bg-pink-100 border border-pink-200 rounded-lg p-1"; 1598 return "flex items-center gap-1 bg-white/5 border border-white/5 rounded-lg p-1"; 1599 }; 1600 const getSelectTextClass = () => { 1601 if (theme === "light") return "bg-transparent border-none text-xs font-bold text-zinc-700 py-1 pl-1 pr-6 outline-none focus:ring-0 cursor-pointer appearance-none"; 1602 if (theme === "fun") return "bg-transparent border-none text-xs font-bold text-pink-900 py-1 pl-1 pr-6 outline-none focus:ring-0 cursor-pointer appearance-none"; 1603 return "bg-transparent border-none text-xs font-bold text-zinc-300 py-1 pl-1 pr-6 outline-none focus:ring-0 cursor-pointer appearance-none"; 1604 }; 1605 const getSelectArrow = () => { 1606 const colorHex = theme === "light" ? "%233f3f46" : theme === "fun" ? "%239d174d" : "%23a1a1aa"; 1607 return `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='${colorHex}' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`; 1608 }; 1609 const getBorderSeparator = () => { 1610 if (theme === "light") return "flex items-center gap-2 border-l border-zinc-200 pl-4"; 1611 if (theme === "fun") return "flex items-center gap-2 border-l border-pink-200 pl-4"; 1612 return "flex items-center gap-2 border-l border-white/10 pl-4"; 1613 }; 1614 const getAutoButtonClass = () => { 1615 if (isAutoZoom) { 1616 if (theme === "fun") return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-pink-200 text-pink-700 border-pink-300"; 1617 return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-indigo-600/25 text-indigo-400 border-indigo-500/30"; 1618 } 1619 if (theme === "light") return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-zinc-200/60 text-zinc-600 border-zinc-300 hover:bg-zinc-300 hover:text-zinc-800"; 1620 if (theme === "fun") return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-pink-50 text-pink-800 border-pink-200 hover:bg-pink-100 hover:text-pink-900"; 1621 return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-white/5 text-zinc-400 border-white/5 hover:bg-white/10 hover:text-white"; 1622 }; 1623 const getIconButtonClass = (isActive = false) => { 1624 if (isActive) { 1625 if (theme === "fun") return "p-1.5 bg-pink-100 text-pink-800 rounded-md transition-colors border border-pink-200"; 1626 return "p-1.5 bg-indigo-600/20 text-indigo-400 rounded-md transition-colors border border-indigo-500/30"; 1627 } 1628 if (theme === "light") return "p-1.5 hover:bg-zinc-200/60 text-zinc-600 hover:text-zinc-900 rounded-md transition-colors"; 1629 if (theme === "fun") return "p-1.5 hover:bg-pink-100/60 text-pink-800 hover:text-pink-950 rounded-md transition-colors"; 1630 return "p-1.5 hover:bg-white/10 text-zinc-400 hover:text-white transition-colors rounded-md"; 1631 }; 1632 const getZoomInputClass = () => { 1633 if (theme === "light") return "w-12 text-center text-xs font-bold bg-white text-zinc-700 border border-zinc-300 rounded py-1 focus:ring-1 focus:ring-indigo-500 outline-none"; 1634 if (theme === "fun") return "w-12 text-center text-xs font-bold bg-white text-pink-850 border border-pink-200 rounded py-1 focus:ring-1 focus:ring-pink-500 outline-none"; 1635 return "w-12 text-center text-xs font-bold bg-white/5 text-zinc-200 border border-white/5 rounded py-1 focus:ring-1 focus:ring-indigo-500 outline-none"; 1636 }; 1637 const getDensityPopupBg = () => { 1638 if (theme === "light") return "absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-white border border-zinc-200 shadow-2xl rounded-xl p-4 z-50 w-64 flex flex-col gap-3 animate-in fade-in slide-in-from-top-2"; 1639 if (theme === "fun") return "absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-[#fff1f2] border border-pink-200 shadow-2xl rounded-xl p-4 z-50 w-64 flex flex-col gap-3 animate-in fade-in slide-in-from-top-2"; 1640 return "absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-zinc-900 border border-white/10 shadow-2xl rounded-xl p-4 z-50 w-64 flex flex-col gap-3 animate-in fade-in slide-in-from-top-2 backdrop-blur-xl"; 1641 }; 1642 const getDensityPopupText = () => { 1643 if (theme === "light") return "text-xs font-bold text-zinc-700"; 1644 if (theme === "fun") return "text-xs font-bold text-pink-900"; 1645 return "text-xs font-bold text-zinc-200"; 1646 }; 1647 const getDensityPopupPill = () => { 1648 if (theme === "light") return "text-[10px] font-bold px-2 py-0.5 rounded-full bg-zinc-100 text-zinc-500 border border-zinc-200"; 1649 if (theme === "fun") return "text-[10px] font-bold px-2 py-0.5 rounded-full bg-pink-100 text-pink-700 border border-pink-200"; 1650 return "text-[10px] font-bold px-2 py-0.5 rounded-full bg-white/5 text-zinc-400 border-white/5"; 1651 }; 1652 const getRangeTrackClass = () => { 1653 if (theme === "light") return "w-full accent-indigo-600 h-1 bg-zinc-200 rounded-lg appearance-none cursor-pointer"; 1654 if (theme === "fun") return "w-full accent-pink-500 h-1 bg-pink-200 rounded-lg appearance-none cursor-pointer"; 1655 return "w-full accent-indigo-500 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer"; 1656 }; 1657 const getToggleGroupBg = () => { 1658 if (theme === "light") return "flex bg-zinc-200/60 rounded-md border border-zinc-300/40 p-0.5"; 1659 if (theme === "fun") return "flex bg-pink-100 rounded-md border border-pink-200 p-0.5"; 1660 return "flex bg-zinc-950/60 rounded-md border border-white/5 p-0.5"; 1661 }; 1662 const getToggleItemClass = (isActive, isOrange = false) => { 1663 const base = "flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-md transition-all"; 1664 if (isActive) { 1665 if (theme === "fun") { 1666 return `${base} bg-gradient-to-r from-pink-500 to-indigo-500 text-white shadow-lg shadow-indigo-500/20`; 1667 } 1668 return `${base} ${isOrange ? "bg-orange-600 border-orange-500/30" : "bg-indigo-600 border-indigo-500/30"} text-white shadow-lg ${isOrange ? "shadow-orange-500/20" : "shadow-indigo-500/20"}`; 1669 } 1670 if (theme === "light") return `${base} text-zinc-600 hover:bg-white/60 hover:text-zinc-900`; 1671 if (theme === "fun") return `${base} text-pink-800 hover:bg-pink-200/50 hover:text-pink-950`; 1672 return `${base} text-zinc-400 hover:bg-white/5 hover:text-white`; 1673 }; 1674 const getOptionsToggleBtn = () => { 1675 if (isMenuExpanded) { 1676 if (theme === "fun") return "p-1.5 rounded-md border transition-all bg-pink-200 border-pink-300 text-pink-850"; 1677 return "p-1.5 rounded-md border transition-all bg-white/10 border-white/20 text-white"; 1678 } 1679 if (theme === "light") return "p-1.5 rounded-md border transition-all bg-zinc-200/60 border-zinc-300 text-zinc-600 hover:bg-zinc-300/60 hover:text-zinc-800"; 1680 if (theme === "fun") return "p-1.5 rounded-md border transition-all bg-pink-50 border-pink-200 text-pink-800 hover:bg-pink-100 hover:text-pink-900"; 1681 return "p-1.5 rounded-md border transition-all bg-white/5 border-white/5 text-zinc-400 hover:bg-white/10 hover:text-white"; 1682 }; 1683 const getSubmenuBg = () => { 1684 if (theme === "light") return "flex flex-wrap items-center justify-between gap-4 px-4 py-2.5 bg-zinc-50 border-b border-t border-zinc-200/80 shadow-inner z-10 shrink-0 animate-in slide-in-from-top-2 duration-200"; 1685 if (theme === "fun") return "flex flex-wrap items-center justify-between gap-4 px-4 py-2.5 bg-[#fffafb] border-b border-t border-pink-200 shadow-inner z-10 shrink-0 animate-in slide-in-from-top-2 duration-200"; 1686 return "flex flex-wrap items-center justify-between gap-4 px-4 py-2.5 bg-zinc-950/40 border-b border-t border-white/10 shadow-inner z-10 shrink-0 animate-in slide-in-from-top-2 duration-200"; 1687 }; 1688 const getSubmenuLabelClass = () => { 1689 if (theme === "light") return "text-xs font-bold text-zinc-500 uppercase tracking-wider"; 1690 if (theme === "fun") return "text-xs font-bold text-pink-800 uppercase tracking-wider"; 1691 return "text-xs font-bold text-zinc-400 uppercase tracking-wider"; 1692 }; 1693 const getSubmenuSelectClass = () => { 1694 if (theme === "light") return "bg-white text-zinc-700 border border-zinc-300 rounded text-xs font-bold py-1 px-2 outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer"; 1695 if (theme === "fun") return "bg-white text-pink-850 border border-pink-200 rounded text-xs font-bold py-1 px-2 outline-none focus:ring-1 focus:ring-pink-500 cursor-pointer"; 1696 return "bg-zinc-950 text-zinc-200 border border-white/10 rounded text-xs font-bold py-1 px-2 outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer"; 1697 }; 1698 const getSubmenuSelectOptionClass = () => { 1699 if (theme === "light") return "bg-white text-zinc-700"; 1700 return "bg-zinc-900 text-zinc-200"; 1701 }; 1702 const getStudyViewBtnClass = () => { 1703 const isActive = isAnalysisStudioOpen || showChords || showRoman || showTimeline; 1704 if (isActive) { 1705 return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-amber-500/20 text-amber-400 border-amber-500/30 shadow-sm shadow-amber-500/10"; 1706 } 1707 if (theme === "light") return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-zinc-200/60 border-zinc-300 text-zinc-600 hover:bg-zinc-300 hover:text-zinc-800"; 1708 if (theme === "fun") return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-white/5 border-white/10 text-white hover:bg-white/15"; 1709 return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-white/5 text-zinc-400 border-white/5 hover:bg-white/10 hover:text-white"; 1710 }; 1711 const getPNGBtnClass = () => { 1712 if (theme === "light") return "flex items-center gap-1.5 px-3 py-1.5 bg-white border border-zinc-300 text-zinc-600 hover:bg-zinc-100 disabled:bg-zinc-200 disabled:text-zinc-400 rounded-lg text-xs font-bold transition-all shadow-sm"; 1713 if (theme === "fun") return "flex items-center gap-1.5 px-3 py-1.5 bg-white/5 border border-white/10 text-white/90 hover:bg-white/15 disabled:bg-zinc-800 disabled:text-zinc-500 rounded-lg text-xs font-bold transition-all shadow-sm"; 1714 return "flex items-center gap-1.5 px-3 py-1.5 bg-white/5 border border-white/5 hover:bg-white/10 disabled:bg-zinc-800 disabled:text-zinc-500 text-zinc-300 hover:text-white rounded-lg text-xs font-bold transition-all shadow-sm"; 1715 }; 1716 const getPDFBtnClass = () => { 1717 if (theme === "fun") return "flex items-center gap-1.5 px-3 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:bg-pink-800 text-white border border-pink-700 rounded-lg text-xs font-bold transition-all shadow-lg shadow-pink-500/20"; 1718 return "flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-800 text-white border border-indigo-700 rounded-lg text-xs font-bold transition-all shadow-lg shadow-indigo-500/20"; 1719 }; 1720 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1721 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1722 "div", 1723 { 1724 className: `flex items-center justify-between px-4 py-2 ${getToolbarBg()} z-20 shrink-0 relative`, 1725 onClick: (e) => e.stopPropagation(), 1726 children: [ 1727 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [ 1728 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getSelectorBg(), children: [ 1729 /* @__PURE__ */ jsxRuntimeExports.jsx(Monitor, { size: 14, className: "text-zinc-400 ml-1" }), 1730 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1731 "select", 1732 { 1733 value: layoutMode, 1734 onChange: (e) => { 1735 const newMode = e.target.value; 1736 setLayoutMode(newMode); 1737 setIsAutoZoom(true); 1738 let targetScale = scale; 1739 if (newMode === "a4") targetScale = 50; 1740 else if (newMode === "a3") targetScale = 25; 1741 setScale(targetScale); 1742 setZoomInput(targetScale.toString()); 1743 }, 1744 className: getSelectTextClass(), 1745 style: { backgroundImage: getSelectArrow(), backgroundPosition: "right 0.25rem center", backgroundRepeat: "no-repeat", backgroundSize: "1.5em 1.5em" }, 1746 children: [
1747 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "screen-moving", className: getSubmenuSelectOptionClass(), children: "Web (Moving)" }), 1748 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "screen-paged", className: getSubmenuSelectOptionClass(), children: "Web (Paged)" }), 1749 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "a4", className: getSubmenuSelectOptionClass(), children: "A4 Print" }), 1750 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "a3", className: getSubmenuSelectOptionClass(), children: "A3 Print" }) 1751 ] 1752 } 1753 ) 1754 ] }), 1755 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [ 1756 /* @__PURE__ */ jsxRuntimeExports.jsx( 1757 "button", 1758 { 1759 onClick: () => setIsAutoZoom(!isAutoZoom), 1760 className: getAutoButtonClass(), 1761 title: isAutoZoom ? "Auto Zoom: ON" : "Auto Zoom: OFF", 1762 children: "AUTO" 1763 } 1764 ),
1765 /* @__PURE__ */ jsxRuntimeExports.jsx( 1766 "button", 1767 { 1768 onClick: () => { 1769 setIsAutoZoom(false); 1770 setScale((prev) => { 1771 const next = Math.max(10, prev - 5); 1772 setZoomInput(next.toString()); 1773 return next; 1774 }); 1775 }, 1776 className: getIconButtonClass(false), 1777 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 16 }) 1778 } 1779 ), 1780 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 1781 /* @__PURE__ */ jsxRuntimeExports.jsx( 1782 "input", 1783 { 1784 type: "text", 1785 value: zoomInput, 1786 onChange: handleZoomChange, 1787 onBlur: handleZoomBlur, 1788 onKeyDown: handleZoomKeyDown, 1789 className: getZoomInputClass() 1790 } 1791 ),
1792 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500", children: "%" }) 1793 ] }), 1794 /* @__PURE__ */ jsxRuntimeExports.jsx( 1795 "button", 1796 { 1797 onClick: () => { 1798 setIsAutoZoom(false); 1799 setScale((prev) => { 1800 const next = Math.min(500, prev + 5); 1801 setZoomInput(next.toString()); 1802 return next; 1803 }); 1804 }, 1805 className: getIconButtonClass(false), 1806 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 16 }) 1807 } 1808 ), 1809 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 1810 /* @__PURE__ */ jsxRuntimeExports.jsx( 1811 "button", 1812 { 1813 onClick: () => setShowDensityPopup(!showDensityPopup), 1814 className: getIconButtonClass(showDensityPopup), 1815 title: "Adjust Display Density", 1816 children: /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersHorizontal, { size: 16 }) 1817 } 1818 ),
1819 showDensityPopup && /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [ 1820 /* @__PURE__ */ jsxRuntimeExports.jsx( 1821 "div", 1822 { 1823 className: "fixed inset-0 z-40", 1824 onClick: () => setShowDensityPopup(false) 1825 } 1826 ), 1827 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getDensityPopupBg(), children: [ 1828 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center", children: [ 1829 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getDensityPopupText(), children: "Display Density" }), 1830 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getDensityPopupPill(), children: density < 30 ? "Compact" : density > 70 ? "Expanded" : "Normal" }) 1831 ] }), 1832 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
1833 /* @__PURE__ */ jsxRuntimeExports.jsx( 1834 "input", 1835 { 1836 type: "range", 1837 min: "0", 1838 max: "100", 1839 value: density, 1840 onChange: (e) => setDensity(parseInt(e.target.value)), 1841 className: getRangeTrackClass() 1842 } 1843 ), 1844 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-[10px] text-zinc-500 font-medium px-1 mt-1", children: [ 1845 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Compact" }), 1846 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Normal" }), 1847 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Expanded" }) 1848 ] }) 1849 ] }) 1850 ] }) 1851 ] }) 1852 ] }) 1853 ] }) 1854 ] }),
1855 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1856 onToggleMelody && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getToggleGroupBg(), children: [ 1857 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1858 "button", 1859 { 1860 onClick: isMelodyMode ? onToggleMelody : void 0, 1861 className: getToggleItemClass(!isMelodyMode), 1862 children: [ 1863 /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 14 }), 1864 "Full Score" 1865 ] 1866 } 1867 ), 1868 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1869 "button", 1870 { 1871 onClick: !isMelodyMode ? onToggleMelody : void 0, 1872 className: getToggleItemClass(isMelodyMode), 1873 children: [ 1874 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 14 }), 1875 "Melody" 1876 ] 1877 } 1878 ) 1879 ] }), 1880 isMelodyMode && onGameModeMute && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1881 "button", 1882 { 1883 onClick: () => { 1884 setIsMMOMode(!isMMOMode); 1885 }, 1886 className: getToggleItemClass(isMMOMode, true), 1887 title: isMMOMode ? "Disable MMO Play-Along" : "Enable MMO Play-Along", 1888 children: [
1889 /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 14 }), 1890 "Play-Along" 1891 ] 1892 } 1893 ), 1894 /* @__PURE__ */ jsxRuntimeExports.jsx( 1895 "button", 1896 { 1897 onClick: () => setIsMenuExpanded(!isMenuExpanded), 1898 className: getOptionsToggleBtn(), 1899 title: "More Options", 1900 children: isMenuExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Ellipsis, { size: 18 }) 1901 } 1902 ) 1903 ] }) 1904 ] 1905 } 1906 ), 1907 isMenuExpanded && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1908 "div", 1909 { 1910 className: getSubmenuBg(), 1911 onClick: (e) => e.stopPropagation(), 1912 children: [ 1913 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [ 1914 !isMelodyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1915 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getSubmenuLabelClass(), children: "Quantize:" }), 1916 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 1917 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1918 "select", 1919 { 1920 value: quantization, 1921 onChange: (e) => setQuantization(parseInt(e.target.value)), 1922 className: getSubmenuSelectClass(), 1923 children: [ 1924 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0, className: getSubmenuSelectOptionClass(), children: "Off (Raw)" }), 1925 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 8, className: getSubmenuSelectOptionClass(), children: "Simple (8th)" }), 1926 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 16, className: getSubmenuSelectOptionClass(), children: "Standard (16th)" }), 1927 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 300, className: getSubmenuSelectOptionClass(), children: "Community Standard" }),
1928 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 500, className: getSubmenuSelectOptionClass(), children: "Studio Engraved" }), 1929 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.ly) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 600, className: getSubmenuSelectOptionClass(), children: "LilyPond Score" }), 1930 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.abc) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 700, className: getSubmenuSelectOptionClass(), children: "The Session (ABC)" }), 1931 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 800, className: getSubmenuSelectOptionClass(), children: "KernScores (Humdrum)" }), 1932 (quantizedScorePaths == null ? void 0 : quantizedScorePaths.urtext) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 900, className: getSubmenuSelectOptionClass(), children: "Urtext (First Edition)" }) 1933 ] 1934 } 1935 ), 1936 quantization > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( 1937 "button", 1938 { 1939 onClick: handleRecommendQuantization, 1940 className: `p-1.5 rounded-md border transition-colors ${hasRecommended ? "bg-emerald-500/20 border-emerald-500/30 text-emerald-400" : theme === "light" ? "bg-zinc-200/60 border-zinc-300 text-zinc-500 hover:text-indigo-600" : "bg-white/5 border-white/5 text-zinc-400 hover:text-indigo-400"}`, 1941 title: hasRecommended ? "Recommended!" : "Recommend this quantization", 1942 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsUp, { size: 14, className: hasRecommended ? "fill-emerald-400" : "" }) 1943 } 1944 ) 1945 ] }) 1946 ] }), 1947 layoutMode === "screen-paged" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [
1948 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `${getSubmenuLabelClass()} flex items-center gap-1`, children: [ 1949 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 12 }), 1950 " Lookahead:" 1951 ] }), 1952 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1953 "select", 1954 { 1955 value: pageTurnLookahead, 1956 onChange: (e) => { 1957 const val = parseFloat(e.target.value); 1958 setPageTurnLookahead(val); 1959 localStorage.setItem("sheet_page_turn_lookahead", val.toString()); 1960 }, 1961 className: getSubmenuSelectClass(), 1962 children: [ 1963 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: -1, className: getSubmenuSelectOptionClass(), children: "Half Measure" }), 1964 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0, className: getSubmenuSelectOptionClass(), children: "Exact Page Turn" }),
1965 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0.5, className: getSubmenuSelectOptionClass(), children: "0.5s Lookahead" }), 1966 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 1, className: getSubmenuSelectOptionClass(), children: "1.0s Lookahead" }), 1967 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 1.5, className: getSubmenuSelectOptionClass(), children: "1.5s Lookahead" }), 1968 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 2, className: getSubmenuSelectOptionClass(), children: "2.0s Lookahead" }), 1969 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 3, className: getSubmenuSelectOptionClass(), children: "3.0s Lookahead" }) 1970 ] 1971 } 1972 ) 1973 ] }), 1974 isMelodyMode && setMelodySource && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [ 1975 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getSubmenuLabelClass(), children: "Melody Source:" }),
1976 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1977 "select", 1978 { 1979 value: melodySource || "standard", 1980 onChange: (e) => setMelodySource(e.target.value), 1981 className: getSubmenuSelectClass(), 1982 children: [ 1983 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "standard", className: getSubmenuSelectOptionClass(), children: "Standard (Score Heuristics)" }), 1984 ((quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore_melody)) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "studio", className: getSubmenuSelectOptionClass(), children: "Studio Engraved (Extraction)" }) 1985 ] 1986 } 1987 ) 1988 ] }) 1989 ] }), 1990 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [ 1991 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1992 "button", 1993 { 1994 onClick: () => setIsAnalysisStudioOpen(!isAnalysisStudioOpen), 1995 className: getStudyViewBtnClass(), 1996 title: "Study View", 1997 children: [
1998 /* @__PURE__ */ jsxRuntimeExports.jsx(Library, { size: 14, className: isAnalysisStudioOpen || showChords || showRoman ? "text-amber-500" : "" }), 1999 "Study View" 2000 ] 2001 } 2002 ), 2003 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [ 2004 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2005 "button", 2006 { 2007 onClick: handleDownloadPNG, 2008 disabled: loading, 2009 className: getPNGBtnClass(), 2010 children: [ 2011 loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 14 }), 2012 "PNG" 2013 ] 2014 } 2015 ), 2016 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2017 "button", 2018 { 2019 onClick: handleDownloadPDF, 2020 disabled: loading, 2021 className: getPDFBtnClass(), 2022 children: [
2023 loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 14 }), 2024 "PDF" 2025 ] 2026 } 2027 ) 2028 ] }) 2029 ] }) 2030 ] 2031 } 2032 ) 2033 ] }); 2034}; 2035const SheetMusicCanvas = (props) => { 2036 const { 2037 containerRef, 2038 paperRef, 2039 cursorRef, 2040 svgContent, 2041 loading, 2042 loadingProgress, 2043 isDataLoaded, 2044 pageCount, 2045 currentPage, 2046 setCurrentPage, 2047 renderPage, 2048 layoutMode, 2049 dimensions, 2050 paperZoom, 2051 setPaperZoom, 2052 paperZoomInput, 2053 setPaperZoomInput, 2054 visibleTrackCount, 2055 showCursor, 2056 setIsDraggingBar, 2057 onSeek, 2058 pageInput, 2059 setPageInput, 2060 seekToPage, 2061 lastManualPageChangeRef, 2062 lastManualPageTimeRef, 2063 currentTime, 2064 handleScoreClick, 2065 handleScroll, 2066 title, 2067 midiId 2068 } = props; 2069 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 relative w-full h-full flex flex-col overflow-hidden", children: [ 2070 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2071 "div", 2072 { 2073 ref: containerRef, 2074 onClick: handleScoreClick, 2075 onScroll: handleScroll, 2076 className: `flex-1 min-h-0 w-full ${layoutMode === "screen-paged" ? "overflow-hidden" : "overflow-auto"} flex flex-col ${!layoutMode.startsWith("screen") ? "items-center bg-[#e5e7eb] p-4" : "bg-[#fdfaf5]"}`, 2077 children: [ 2078 loading && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-white/70 dark:bg-zinc-950/70 backdrop-blur-md transition-all duration-300", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-4 max-w-xs w-full px-6 py-5 bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-2xl", children: [
2079 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center justify-center w-14 h-14", children: [ 2080 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 48, className: "text-indigo-600 dark:text-indigo-400 animate-spin" }), 2081 loadingProgress && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "absolute text-[11px] font-black text-indigo-700 dark:text-indigo-300", children: [ 2082 Math.round(loadingProgress.percent), 2083 "%" 2084 ] }) 2085 ] }), 2086 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full self-stretch flex flex-col items-center gap-2 text-center", children: [ 2087 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-700 dark:text-zinc-200 tracking-wide", children: (loadingProgress == null ? void 0 : loadingProgress.stage) || (!isDataLoaded ? "Loading Score..." : !layoutMode.startsWith("screen") && pageCount > 0 ? `Page ${currentPage} of ${pageCount}` : "Engraving Score...") }),
2088 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full self-stretch h-2 bg-zinc-100 dark:bg-zinc-800 rounded-full overflow-hidden border border-zinc-200/50 dark:border-zinc-700/50", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2089 "div", 2090 { 2091 className: "h-full bg-gradient-to-r from-indigo-500 via-indigo-600 to-indigo-400 rounded-full transition-all duration-300 ease-out shadow-sm", 2092 style: { width: `${Math.max(5, Math.min(100, (loadingProgress == null ? void 0 : loadingProgress.percent) ?? 25))}%` } 2093 } 2094 ) }) 2095 ] }) 2096 ] }) }), 2097 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2098 "div", 2099 { 2100 ref: paperRef, 2101 className: `relative transition-transform duration-200 origin-top ${!layoutMode.startsWith("screen") ? "bg-white shadow-[0_20px_50px_rgba(0,0,0,0.15),0_10px_20px_rgba(0,0,0,0.1)] border border-zinc-200 rounded-sm" : layoutMode === "screen-paged" ? "w-full min-h-full bg-[#fdfaf5]" : "bg-[#fdfaf5] inline-block text-left align-top"}`, 2102 style: { 2103 ...layoutMode === "a4" ? { 2104 width: "210mm", 2105 minHeight: "297mm", 2106 transform: `scale(${paperZoom / 100})`, 2107 transformOrigin: "top center", 2108 display: "flex", 2109 flexDirection: "column", 2110 overflow: "visible", 2111 marginBottom: `${(paperZoom / 100 - 1) * 297}mm` 2112 } : layoutMode === "a3" ? { 2113 width: "420mm", 2114 minHeight: "297mm", 2115 transform: `scale(${paperZoom / 100})`, 2116 transformOrigin: "top center", 2117 display: "flex", 2118 flexDirection: "column", 2119 overflow: "visible", 2120 marginBottom: `${(paperZoom / 100 - 1) * 297}mm` 2121 } : layoutMode === "screen-paged" ? { 2122 width: "100%", 2123 height: "100%", 2124 overflow: "hidden" 2125 } : { 2126 width: "max-content", 2127 height: "auto" 2128 } 2129 }, 2130 children: [ 2131 (layoutMode === "a4" || layoutMode === "a3") && /* @__P
2131URE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex flex-col items-center pt-3 pb-1 px-6 text-center select-none", children: [ 2132 currentPage === 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-2xl font-serif text-zinc-900 tracking-tight leading-tight", children: title || "HamieNET.com Score" }), 2133 currentPage > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex justify-between border-b border-zinc-200 pb-1 text-[8pt] font-serif italic text-zinc-400 uppercase tracking-widest", children: [ 2134 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: title }), 2135 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 2136 "Page ", 2137 currentPage 2138 ] }) 2139 ] }) 2140 ] }), 2141 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "relative flex-1 w-full flex items-start justify-center overflow-hidden", children: svgContent ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-full flex flex-col justify-center [&>svg]:w-full [&>svg]:h-full [&>svg]:max-w-none", dangerouslySetInnerHTML: { __html: svgContent } }
2141) : !loading ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center p-12 text-center my-auto min-h-[260px] select-none", children: [ 2142 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-14 h-14 rounded-2xl bg-amber-500/10 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center mb-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 28 }) }), 2143 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-bold text-zinc-800 dark:text-zinc-200", children: "Sheet Music Unavailable" }), 2144 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 max-w-xs mt-1 leading-relaxed", children: "The notation score for this track could not be loaded or is currently queued for engraving." }) 2145 ] }) : null }), 2146 (layoutMode === "a4" || layoutMode === "a3") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full flex items-center justify-center pt-1 pb-2 px-6 text-center select-none mt-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
2147 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8pt] font-serif text-zinc-400 italic", children: [ 2148 "Page ", 2149 currentPage, 2150 " of ", 2151 pageCount 2152 ] }), 2153 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[7pt] font-sans text-zinc-400 font-bold uppercase tracking-[0.1em]", children: [ 2154 "HamieNET.com/midi", 2155 midiId 2156 ] }) 2157 ] }) }), 2158 /* @__PURE__ */ jsxRuntimeExports.jsx( 2159 "div", 2160 { 2161 ref: cursorRef, 2162 className: "score-playback-cursor", 2163 style: { 2164 display: showCursor ? "block" : "none", 2165 position: "absolute", 2166 width: "4px", 2167 backgroundColor: "rgba(99, 102, 241, 0.8)", 2168 boxShadow: "0 0 8px rgba(99, 102, 241, 0.6)", 2169 zIndex: 50, 2170 borderRadius: "2px", 2171 cursor: "col-resize", 2172 pointerEvents: "auto" 2173 }, 2174 onMouseDown: (e) => { 2175 e.stopPropagation(); 2176 setIsDraggingBar(true); 2177 } 2178 } 2179 ) 2180 ] 2181 } 2182 ) 2183 ] 2184 } 2185 ),
2186 (layoutMode === "a4" || layoutMode === "a3") && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-6 right-6 flex flex-col gap-2 bg-white/90 backdrop-blur shadow-lg rounded-lg p-2 border border-zinc-200 z-30 animate-in fade-in slide-in-from-bottom-4", children: [ 2187 /* @__PURE__ */ jsxRuntimeExports.jsx( 2188 "button", 2189 { 2190 onClick: (e) => { 2191 e.stopPropagation(); 2192 const val = Math.min(400, paperZoom + 10); 2193 setPaperZoom(val); 2194 setPaperZoomInput(val.toString()); 2195 }, 2196 className: "p-2 hover:bg-zinc-100 rounded-md text-zinc-600 transition-colors", 2197 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 20 }) 2198 } 2199 ), 2200 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-0.5 py-1 border-y border-zinc-100", children: [ 2201 /* @__PURE__ */ jsxRuntimeExports.jsx( 2202 "input", 2203 { 2204 type: "text", 2205 value: paperZoomInput, 2206 onChange: (e) => setPaperZoomInput(e.target.value), 2207 onBlur: () => { 2208 const val = parseInt(paperZoomInput); 2209 if (!isNaN(val)) { 2210 const clamped = Math.max(10, Math.min(500, val)); 2211 setPaperZoom(clamped); 2212 setPaperZoomInput(clamped.toString()); 2213 } else { 2214 setPaperZoomInput(paperZoom.toString()); 2215 } 2216 }, 2217 className: "w-10 text-center text-[10px] font-bold bg-transparent border-none outline-none text-zinc-600" 2218 } 2219 ),
2220 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] font-bold text-zinc-400", children: "%" }) 2221 ] }), 2222 /* @__PURE__ */ jsxRuntimeExports.jsx( 2223 "button", 2224 { 2225 onClick: (e) => { 2226 e.stopPropagation(); 2227 const val = Math.max(10, paperZoom - 10); 2228 setPaperZoom(val); 2229 setPaperZoomInput(val.toString()); 2230 }, 2231 className: "p-2 hover:bg-zinc-100 rounded-md text-zinc-600 transition-colors", 2232 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 20 }) 2233 } 2234 ), 2235 /* @__PURE__ */ jsxRuntimeExports.jsx( 2236 "button", 2237 { 2238 onClick: (e) => { 2239 e.stopPropagation(); 2240 setPaperZoom(100); 2241 setPaperZoomInput("100"); 2242 }, 2243 className: "text-[10px] font-bold text-zinc-400 hover:text-indigo-600 mt-1", 2244 children: "RESET" 2245 } 2246 ) 2247 ] }), 2248 pageCount > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 px-3 py-1 rounded-full bg-black/60 backdrop-blur-md border border-white/10 shadow-lg z-50 animate-in slide-in-from-bottom-4 fade-in duration-300 opacity-40 hover:opacity-100 transition-all", children: [
2249 /* @__PURE__ */ jsxRuntimeExports.jsx( 2250 "button", 2251 { 2252 onClick: (e) => { 2253 e.stopPropagation(); 2254 const newPage = Math.max(1, currentPage - 1); 2255 lastManualPageChangeRef.current = Date.now(); 2256 lastManualPageTimeRef.current = currentTime; 2257 setCurrentPage(newPage); 2258 renderPage(newPage); 2259 seekToPage(newPage); 2260 }, 2261 disabled: currentPage === 1, 2262 className: "p-1 hover:bg-white/20 disabled:opacity-30 rounded-full transition-colors text-white/80", 2263 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { size: 14 }) 2264 } 2265 ), 2266 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 2267 /* @__PURE__ */ jsxRuntimeExports.jsx( 2268 "input", 2269 { 2270 type: "text", 2271 value: pageInput, 2272 onChange: (e) => setPageInput(e.target.value), 2273 onBlur: () => { 2274 const val = parseInt(pageInput); 2275 if (!isNaN(val) && val >= 1 && val <= pageCount && val !== currentPage) { 2276 lastManualPageChangeRef.current = Date.now(); 2277 lastManualPageTimeRef.current = currentTime; 2278 setCurrentPage(val); 2279 renderPage(val); 2280 seekToPage(val); 2281 } else { 2282 setPageInput(currentPage.toString()); 2283 } 2284 }, 2285 className: "w-8 h-4 text-center text-[10px] font-bold text-white/90 bg-transparent hover:bg-white/10 focus:bg-white/20 border-none outline-none transition-colors px-0 rounded-[2px]" 2286 } 2287 ),
2288 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold text-white/90 drop-shadow-sm uppercase tracking-wider", children: [ 2289 "/ ", 2290 pageCount 2291 ] }) 2292 ] }), 2293 /* @__PURE__ */ jsxRuntimeExports.jsx( 2294 "button", 2295 { 2296 onClick: (e) => { 2297 e.stopPropagation(); 2298 const newPage = Math.min(pageCount, currentPage + 1); 2299 lastManualPageChangeRef.current = Date.now(); 2300 lastManualPageTimeRef.current = currentTime; 2301 setCurrentPage(newPage); 2302 renderPage(newPage); 2303 seekToPage(newPage); 2304 }, 2305 disabled: currentPage === pageCount, 2306 className: "p-1 hover:bg-white/20 disabled:opacity-30 rounded-full transition-colors text-white/80", 2307 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 }) 2308 } 2309 ) 2310 ] }) 2311 ] }); 2312}; 2313const AnalysisStudioMenu = ({ 2314 isOpen, 2315 onClose, 2316 showChords, 2317 setShowChords, 2318 showRoman, 2319 setShowRoman, 2320 showTimeline, 2321 setShowTimeline, 2322 showPills, 2323 setShowPills, 2324 showMeasureNumbers, 2325 setShowMeasureNumbers, 2326 showHighlight, 2327 setShowHighlight, 2328 showCursor, 2329 setShowCursor, 2330 hasSections, 2331 hasChordsData = true, 2332 hasRomanData = true 2333}) => { 2334 const menuRef = React.useRef(null); 2335 React.useEffect(() => { 2336 if (!isOpen) return; 2337 const handleClickOutside = (event) => { 2338 var _a; 2339 if (menuRef.current && !menuRef.current.contains(event.target)) { 2340 const target = event.target; 2341 if (target.closest('[title="Study View"]') || ((_a = target.textContent) == null ? void 0 : _a.includes("Study View"))) { 2342 return; 2343 } 2344 onClose(); 2345 } 2346 }; 2347 const handleKeyDown = (event) => { 2348 if (event.key === "Escape") { 2349 onClose(); 2350 } 2351 }; 2352 document.addEventListener("mousedown", handleClickOutside); 2353 document.addEventListener("keydown", handleKeyDown); 2354 return () => { 2355 document.removeEventListener("mousedown", handleClickOutside); 2356 document.removeEventListener("keydown", handleKeyDown); 2357 }; 2358 }, [isOpen, onClose]); 2359 if (!isOpen) return null; 2360 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2361 "div", 2362 { 2363 ref: menuRef, 2364 className: "absolute top-14 right-4 z-[100] w-72 bg-zinc-900/90 backdrop-blur-md border border-white/10 rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200 flex flex-col max-h-[calc(100%-80px)]", 2365 children: [
2366 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-white/5 flex items-center justify-between bg-zinc-950/20 shrink-0", children: [ 2367 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 2368 /* @__PURE__ */ jsxRuntimeExports.jsx(Library, { size: 18, className: "text-amber-500" }), 2369 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-200 tracking-tight", children: "Study View" }) 2370 ] }), 2371 /* @__PURE__ */ jsxRuntimeExports.jsx( 2372 "button", 2373 { 2374 onClick: onClose, 2375 className: "p-1 hover:bg-white/10 rounded-lg transition-colors text-zinc-400 hover:text-white", 2376 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 18 }) 2377 } 2378 ) 2379 ] }), 2380 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 space-y-4 overflow-y-auto flex-1 custom-scrollbar", children: [
2381 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "space-y-2", children: [ 2382 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "px-3 text-[10px] font-bold text-zinc-400 uppercase tracking-widest flex items-center gap-2", children: [ 2383 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 10 }), 2384 " Harmonic Layers" 2385 ] }), 2386 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 2387 /* @__PURE__ */ jsxRuntimeExports.jsx( 2388 ToggleItem, 2389 { 2390 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 14 }), 2391 title: "Chord Symbols", 2392 description: hasChordsData ? "Real-time harmonic labels" : "No analysis data", 2393 isActive: showChords, 2394 disabled: !hasChordsData, 2395 onClick: () => setShowChords(!showChords) 2396 } 2397 ), 2398 /* @__PURE__ */ jsxRuntimeExports.jsx( 2399 ToggleItem, 2400 { 2401 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 14 }), 2402 title: "Roman Numerals", 2403 description: hasRomanData ? "Musicological analysis" : "No analysis data", 2404 isActive: showRoman, 2405 disabled: !hasRomanData, 2406 onClick: () => setShowRoman(!showRoman) 2407 } 2408 ) 2409 ] }) 2410 ] }),
2411 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "space-y-2", children: [ 2412 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "px-3 text-[10px] font-bold text-zinc-400 uppercase tracking-widest flex items-center gap-2", children: [ 2413 /* @__PURE__ */ jsxRuntimeExports.jsx(Map$1, { size: 10 }), 2414 " Structural Analysis" 2415 ] }), 2416 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 2417 /* @__PURE__ */ jsxRuntimeExports.jsx( 2418 ToggleItem, 2419 { 2420 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Map$1, { size: 14 }), 2421 title: "Interactive Timeline", 2422 description: "Navigable structural segments", 2423 isActive: showTimeline, 2424 disabled: !hasSections, 2425 onClick: () => setShowTimeline(!showTimeline) 2426 } 2427 ), 2428 /* @__PURE__ */ jsxRuntimeExports.jsx( 2429 ToggleItem, 2430 { 2431 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Pill, { size: 14 }), 2432 title: "Section Labels", 2433 description: "Floating contextual pills", 2434 isActive: showPills, 2435 disabled: !hasSections, 2436 onClick: () => setShowPills(!showPills) 2437 } 2438 ) 2439 ] }),
2440 !hasSections && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-3 flex items-start gap-2 text-[10px] text-zinc-400 bg-amber-500/10 p-2 rounded-lg border border-amber-500/20", children: [ 2441 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 12, className: "shrink-0 mt-0.5" }), 2442 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "No musicological markers detected for this track." }) 2443 ] }) 2444 ] }), 2445 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "space-y-2 border-t border-white/5 pt-3", children: [ 2446 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "px-3 text-[10px] font-bold text-zinc-400 uppercase tracking-widest flex items-center gap-2", children: [ 2447 /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 10 }), 2448 " Notation Guides" 2449 ] }), 2450 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 2451 /* @__PURE__ */ jsxRuntimeExports.jsx( 2452 ToggleItem, 2453 { 2454 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 14 }), 2455 title: "Measure Numbers", 2456 isActive: showMeasureNumbers, 2457 onClick: () => setShowMeasureNumbers(!showMeasureNumbers) 2458 } 2459 ),
2460 /* @__PURE__ */ jsxRuntimeExports.jsx( 2461 ToggleItem, 2462 { 2463 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Palette, { size: 14 }), 2464 title: "Color Highlighting", 2465 isActive: showHighlight, 2466 onClick: () => setShowHighlight(!showHighlight) 2467 } 2468 ), 2469 /* @__PURE__ */ jsxRuntimeExports.jsx( 2470 ToggleItem, 2471 { 2472 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size: 14 }), 2473 title: "Playback Cursor", 2474 isActive: showCursor, 2475 onClick: () => setShowCursor(!showCursor) 2476 } 2477 ) 2478 ] }) 2479 ] }) 2480 ] }), 2481 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 bg-zinc-950/40 text-[9px] text-zinc-500 text-center italic shrink-0 border-t border-white/5", children: "HamieNET Research-Grade Educational Tool" }) 2482 ] 2483 } 2484 ); 2485}; 2486const ToggleItem = ({ icon, title, description, isActive, disabled, onClick }) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 2487 "button", 2488 { 2489 onClick, 2490 disabled, 2491 className: `w-full flex items-center justify-between p-2.5 rounded-lg transition-all ${disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-white/5"}`, 2492 children: [
2493 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 2494 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `p-1.5 rounded-md ${isActive ? "bg-amber-500/20 text-amber-400" : "bg-white/5 text-zinc-500"}`, children: icon }), 2495 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-left", children: [ 2496 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold text-zinc-200", children: title }), 2497 description && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500", children: description }) 2498 ] }) 2499 ] }), 2500 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-10 h-5 rounded-full relative transition-colors ${isActive ? "bg-amber-500" : "bg-white/10"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${isActive ? "left-6" : "left-1 shadow-sm"}` }) }) 2501 ] 2502 } 2503); 2504const sectionColors = { 2505 "exposition": "bg-indigo-600", 2506 "development": "bg-rose-600", 2507 "recapitulation": "bg-emerald-600", 2508 "coda": "bg-amber-600", 2509 "intro": "bg-slate-500", 2510 "verse": "bg-blue-600", 2511 "chorus": "bg-purple-600", 2512 "bridge": "bg-orange-600", 2513 "solo": "bg-red-600", 2514 "phrase": "bg-zinc-600", 2515 "part": "bg-zinc-600" 2516}; 2517const StructuralTimeline = ({ 2518 sections, 2519 duration, 2520 currentTime, 2521 onSeek, 2522 onSeekSection 2523}) => { 2524 if (!sections || sections.length === 0) return null; 2525 const maxSectionTime = Math.max(...sections.map((s) => s.time)); 2526 const effectiveDuration = duration > 0 ? duration : maxSectionTime + 10; 2527 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-9 bg-zinc-900 border-b border-white/10 flex items-stretch overflow-hidden group shadow-lg relative", children: sections.map((section, index) => { 2528 const nextSection = sections[index + 1]; 2529 const sectionDuration = (nextSection ? nextSection.time : effectiveDuration) - section.time; 2530 const widthPercent = sectionDuration / effectiveDuration * 100; 2531 const safeName = (section == null ? void 0 : section.label) || (section == null ? void 0 : section.name) || ""; 2532 const nameKey = safeName.toLowerCase(); 2533 const colorKey = Object.keys(sectionColors).find((k) => nameKey.includes(k)); 2534 const colorClass = colorKey ? sectionColors[colorKey] : "bg-zinc-700"; 2535 const isCurrent = currentTime >= section.time && (nextSection ? currentTime < nextSection.time : true); 2536 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2537 "button", 2538 { 2539 onClick: () => { 2540 if (onSeekSection) { 2541 onSeekSection(section); 2542 } else if (typeof section.time === "number" && isFinite(section.time)) { 2543 console.log(`[Timeline] Seeking to measure ${section.measure} at time ${section.time}`); 2544 onSeek(section.time); 2545 } else { 2546 console.warn(`[Timeline] Cannot seek: Invalid time for measure ${section.measure}`, section); 2547 } 2548 }, 2549 className: `relative h-full transition-all group/seg border-r border-black/20 last:border-0 overflow-hidden flex flex-col justify-center ${isCurrent ? "flex-[1.5] opacity-100 ring-1 ring-inset ring-white/30 z-10" : "flex-1 opacity-80 hover:opacity-100"}`, 2550 style: { width: `${widthPercent}%` }, 2551 title: `${safeName} (Measure ${section.measure})`, 2552 children: [
2553 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute inset-0 ${colorClass}` }), 2554 isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx( 2555 "div", 2556 { 2557 className: "absolute inset-y-0 left-0 bg-white/20", 2558 style: { 2559 width: `${Math.min(100, Math.max(0, (currentTime - section.time) / sectionDuration * 100))}%` 2560 } 2561 } 2562 ), 2563 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-10 px-2 h-full flex flex-col justify-center text-white drop-shadow-sm", children: [ 2564 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-black tracking-wider uppercase truncate leading-tight", children: safeName }), 2565 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[9px] font-bold opacity-90 leading-none", children: [ 2566 "m.", 2567 section.measure 2568 ] }) 2569 ] }), 2570 isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 inset-x-0 h-0.5 bg-white/70 shadow-[0_0_8px_white]" }) 2571 ] 2572 }, 2573 index 2574 ); 2575 }) }); 2576}; 2577const SectionPill = ({ currentSection }) => { 2578 const [visible, setVisible] = reactExports.useState(false); 2579 const [displayText, setDisplayText] = reactExports.useState(""); 2580 reactExports.useEffect(() => { 2581 if (currentSection) { 2582 setDisplayText(currentSection); 2583 setVisible(true); 2584 const timer = setTimeout(() => setVisible(false), 3e3); 2585 return () => clearTimeout(timer); 2586 } else { 2587 setVisible(false); 2588 } 2589 }, [currentSection]); 2590 if (!displayText) return null; 2591 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2592 "div", 2593 { 2594 className: `absolute top-16 left-1/2 -translate-x-1/2 z-[60] pointer-events-none transition-all duration-700 ${visible ? "translate-y-0 opacity-100" : "-translate-y-8 opacity-0"}`, 2595 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/90 backdrop-blur-xl text-white px-8 py-3 rounded-2xl border border-white/10 shadow-[0_20px_50px_rgba(0,0,0,0.5)] flex items-center gap-4", children: [
2596 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2597 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-3 h-3 rounded-full bg-amber-500 shadow-[0_0_15px_rgba(245,158,11,0.8)]" }), 2598 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 w-3 h-3 rounded-full bg-amber-500 animate-ping opacity-75" }) 2599 ] }), 2600 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 2601 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-bold tracking-[0.2em] uppercase leading-none mb-1", children: "Current Section" }), 2602 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-black tracking-widest uppercase leading-none", children: displayText }) 2603 ] }) 2604 ] }) 2605 } 2606 ); 2607}; 2608const SheetMusicViewer = (props) => { 2609 const { 2610 xmlData, 2611 scorePath, 2612 quantizedScorePaths, 2613 melodyData, 2614 trackVisibility, 2615 isPlaying, 2616 currentTime, 2617 duration, 2618 processedInstruments, 2619 recommendedQuantization, 2620 onSeek, 2621 title, 2622 selectedTrackIndex, 2623 midiId, 2624 isMelodyMode, 2625 onToggleMelody, 2626 melodySource, 2627 setMelodySource, 2628 onGameModeMute, 2629 onPlayPause, 2630 audioLatency = 0, 2631 uploaderName, 2632 xmlString, 2633 dimensions: initialDimensions, 2634 sections = [], 2635 measureMap = [], 2636 soundfont, 2637 harmonyChordsUrl, 2638 harmonyRomanUrl, 2639 theme = "light" 2640 } = props; 2641 const [harmonyAvailable, setHarmonyAvailable] = reactExports.useState({ chords: true, roman: true }); 2642 const hasHarmonyChords = harmonyAvailable.chords; 2643 const hasHarmonyRoman = harmonyAvailable.roman; 2644 const [layoutMode, setLayoutMode] = reactExports.useState("screen-moving"); 2645 const [scale, setScale] = reactExports.useState(45);
2646 const [zoomInput, setZoomInput] = reactExports.useState("45"); 2647 const [isAutoZoom, setIsAutoZoom] = reactExports.useState(true); 2648 const [paperZoom, setPaperZoom] = reactExports.useState(100); 2649 const [paperZoomInput, setPaperZoomInput] = reactExports.useState("100"); 2650 const [quantization, setQuantization] = reactExports.useState(() => { 2651 var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l; 2652 if (((_a = props.quantizedScorePaths) == null ? void 0 : _a.urtext) || ((_b = props.scorePath) == null ? void 0 : _b.includes("_urtext"))) return 900; 2653 if (((_c = props.quantizedScorePaths) == null ? void 0 : _c.krn) || ((_d = props.scorePath) == null ? void 0 : _d.includes("_krn"))) return 800; 2654 if (((_e = props.quantizedScorePaths) == null ? void 0 : _e.studio) || ((_f = props.quantizedScorePaths) == null ? void 0 : _f.musescore) || ((_g = props.scorePath) == null ? void 0 : _g.includes("_studio")) || ((_h = props.scorePath) == null ? void 0 : _h.includes("_musescore"))) return 500; 2655 if (((_i = props.quantizedScorePaths) == null ? void 0 : _i.abc) || ((_j = props.scorePath) == null ? void 0 : _j.includes("_abc"))) return 700; 2656 if (((_k = props.quantizedScorePaths) == null ? void 0 : _k.ly) || ((_l = props.scorePath) == null ? void 0 : _l.includes("_ly"))) return 600; 2657 return 500; 2658 }); 2659 const [pageInput, setPageInput] = reactExports.useState("1"); 2660 const [showCursor, setShowCursor] = reactExports.useState(() => localStorage.getItem("sheet_show_cursor") !== "false"); 2661 const [showHighlight, setShowHighlight] = reactExports.useState(() => localStorage.getItem("sheet_show_highlight") !== "false"); 2662 const [showMeasureNumbers, setShowMeasureNumbers] = reactExports.useState(() => localStorage.getItem("sheet_show_measure_numbers") !== "false"); 2663 const [showChords, setShowChords] = reactExports.useState(() => localStorage.getItem("sheet_show_chords") !== "false"); 2664 const [showRoman, setShowRoman] = reactExports.useState(() => localStorage.getItem("sheet_show_roman") === "true"); 2665 const [showTimeline, setShowTimeline] = reactExports.useState(false); 2666 const [showPills, setShowPills] = reactExports.useState(true); 2667 const [isMenuExpanded, setIsMenuExpanded] = reactExports.useState(false); 2668 const [isAnalysisStudioOpen, setIsAnalysisStudioOpen] = reactExports.useState(false); 2669 const [isMMOMode, setIsMMOMode] = reactExports.useState(false); 2670 const [isDraggingBar, setIsDraggingBar] = reactExports.useState(false); 2671 const [dimensions, setDimensions] = reactExports.useState(initialDimensions || { width: 0, height: 0 }); 2672 const [pageTurnLookahead, setPageTurnLookahead] = reactExports.useState(() => { 2673 const saved = localStorage.getItem("sheet_page_turn_lookahead"); 2674 return saved ? parseFloat(saved) : -1; 2675 }); 2676 const [density, setDensity] = reactExports.useState(50); 2677 const [hasRecommended, setHasRecommended] = reactExports.useState(false); 2678 const [practiceMode, setPracticeMode] = reactExports.useState(false); 2679 const lastSatisfiedTimeRef = reactExports.useRef(-1); 2680 const waitingNotesRef = reactExports.useRef(null); 2681 reactExports.useEffect(() => { 2682 lastSatisfiedTimeRef.current = -1; 2683 waitingNotesRef.current = null; 2684 }, [practiceMode, midiId]); 2685 reactExports.useEffect(() => { 2686 if (Math.abs(currentTime - currentTimeRef.current) > 0.5) { 2687 lastSatisfiedTimeRef.current = -1; 2688 waitingNotesRef.current = null; 2689 } 2690 }, [currentTime]); 2691 const [keyboardState, setKeyboardState] = reactExports.useState("off"); 2692 const keyboardCanvasRef = reactExports.useRef(null); 2693 const keyboardContainerRef = reactExports.useRef(null); 2694 reactExports.useRef(/* @__PURE__ */ new Set()); 2695 const baseOctaveRef = reactExports.useRef(4); 2696 const activeKeysRef = reactExports.useRef(/* @__PURE__ */ new Map()); 2697 const animationFrameRef = reactExports.useRef(); 2698 const currentTimeRef = reactExports.useRef(currentTime); 2699 const isPlayingRef = reactExports.useRef(isPlaying); 2700 const melodyDataRef = reactExports.useRef(melodyData); 2701 const onGameModeMuteRef = reactExports.useRef(onGameModeMute); 2702 const processedInstrumentsRef = reactExports.useRef(processedInstruments); 2703 const selectedTrackIndexRef = reactExports.useRef(selectedTrackIndex); 2704 reactExports.useEffect(() => { 2705 currentTimeRef.current = currentTime; 2706 }, [currentTime]); 2707 reactExports.useEffect(() => { 2708 isPlayingRef.current = isPlaying; 2709 }, [isPlaying]); 2710 reactExports.useEffect(() => { 2711 melodyDataRef.current = melodyData; 2712 }, [melodyData]); 2713 reactExports.useEffect(() => { 2714 onGameModeMuteRef.current = onGameModeMute; 2715 }, [onGameModeMute]); 2716 reactExports.useEffect(() => { 2717 processedInstrumentsRef.current = processedInstruments; 2718 }, [processedInstruments]); 2719 reactExports.useEffect(() => { 2720 selectedTrackIndexRef.current = selectedTrackIndex; 2721 }, [selectedTrackIndex]); 2722 const { playNote, stopNote, stopAllNotes, previewActiveNotesRef } = useSoundfontKeyboard(selectedTrackIndex, processedInstruments, void 0, soundfont); 2723 const containerRef = reactExports.useRef(null); 2724 const paperRef = reactExports.useRef(null); 2725 const cursorRef = reactExports.useRef(null); 2726 const resizeTimeout = reactExports.useRef(null); 2727 const visibleTrackCount = reactExports.useMemo(() => { 2728 if (!processedInstruments) return 1;
2729 let count = 0; 2730 processedInstruments.forEach((instr, idx) => { 2731 const name = instr.displayName || instr.name || ""; 2732 if (name !== "Informational Track" && trackVisibility[idx] !== false) count++; 2733 }); 2734 return Math.max(1, count); 2735 }, [processedInstruments, trackVisibility]); 2736 const estimatedStaffCount = reactExports.useMemo(() => { 2737 if (isMelodyMode) return 1; 2738 if (!processedInstruments || processedInstruments.length === 0) return 2; 2739 let staves = 0; 2740 processedInstruments.forEach((instr, idx) => { 2741 const name = (instr.displayName || instr.name || instr.originalName || "").toLowerCase(); 2742 const program = instr.program ?? instr.programNumber; 2743 if (name === "informational track" || trackVisibility[idx] === false) return; 2744 const isGrandStaff = program !== void 0 && (program >= 0 && program <= 7 || program >= 16 && program <= 23 || program === 46) || name.includes("piano") || name.includes("keyboard") || name.includes("organ") || name.includes("harp") || name.includes("klavier"); 2745 staves += isGrandStaff ? 2 : 1; 2746 }); 2747 return Math.max(1, staves); 2748 }, [processedInstruments, trackVisibility, isMelodyMode]); 2749 const { 2750 verovioToolkit, 2751 loading, 2752 loadingProgress, 2753 setLoading, 2754 isDataLoaded, 2755 pageCount, 2756 currentPage, 2757 setCurrentPage, 2758 svgContent, 2759 syncPoints, 2760 renderPage 2761 } = useSheetMusicEngine({ 2762 ...props, 2763 dimensions, 2764 layoutMode, 2765 scale, 2766 quantization, 2767 density, 2768 showMeasureNumbers, 2769 visibleTrackCount, 2770 showChords: showChords && hasHarmonyChords, 2771 showRoman: showRoman && hasHarmonyRoman, 2772 harmonyChordsUrl, 2773 harmonyRomanUrl, 2774 measureMap 2775 }); 2776 const { 2777 verovioToAudioTime, 2778 lastManualPageChangeRef, 2779 lastManualPageTimeRef, 2780 handleScroll 2781 } = useSheetMusicPlayback({ 2782 verovioToolkit, 2783 isPlaying, 2784 currentTime, 2785 duration, 2786 audioLatency, 2787 onSeek, 2788 layoutMode, 2789 scale, 2790 pageTurnLookahead, 2791 dimensions, 2792 currentPage, 2793 setCurrentPage, 2794 pageCount, 2795 renderPage, 2796 syncPoints, 2797 containerRef, 2798 paperRef, 2799 cursorRef, 2800 showCursor, 2801 showHighlight, 2802 isDataLoaded, 2803 svgContent 2804 }); 2805 useKeyboardInput({ 2806 playNote, 2807 stopNote, 2808 baseOctaveRef, 2809 activeKeysRef, 2810 enabled: keyboardState !== "off" 2811 }); 2812 reactExports.useEffect(() => { 2813 if (keyboardState === "off") return; 2814 const renderLoop = () => { 2815 const canvas = keyboardCanvasRef.current; 2816 const container = keyboardContainerRef.current; 2817 if (canvas && container) { 2818 const rect = container.getBoundingClientRect(); 2819 if (canvas.width !== rect.width || canvas.height !== rect.height) { 2820 canvas.width = rect.width; 2821 canvas.height = rect.height; 2822 } 2823 const ctx = canvas.getContext("2d"); 2824 if (ctx) { 2825 ctx.clearRect(0, 0, canvas.width, canvas.height); 2826 ctx.fillStyle = "#111"; 2827 ctx.fillRect(0, 0, canvas.width, canvas.height); 2828 const activeSongNotes = /* @__PURE__ */ new Set(); 2829 const currentMelData = melodyDataRef.current; 2830 const t = currentTimeRef.current; 2831 if (currentMelData && isPlayingRef.current) { 2832 currentMelData.forEach((note) => { 2833 if (t >= note.t && t <= note.t + note.d) { 2834 activeSongNotes.add(note.n); 2835 } 2836 }); 2837 } 2838 if (practiceMode && currentMelData) { 2839 if (isPlayingRef.current) { 2840 const upcomingNotes = []; 2841 for (let i = 0; i < currentMelData.length; i++) { 2842 const note = currentMelData[i]; 2843 if (note.t <= t + 0.05 && note.t > lastSatisfiedTimeRef.current + 0.02) { 2844 upcomingNotes.push(note); 2845 } 2846 if (note.t > t + 0.2) break; 2847 } 2848 if (upcomingNotes.length > 0) { 2849 const chordTime = upcomingNotes[0].t; 2850 const chordNotes = upcomingNotes.filter((n) => Math.abs(n.t - chordTime) < 0.05); 2851 const targetNoteNumbers = new Set(chordNotes.map((n) =>
2851 n.n)); 2852 if (targetNoteNumbers.size > 0) { 2853 let allPressed = true; 2854 for (const midiNum of targetNoteNumbers) { 2855 if (!previewActiveNotesRef.current.has(midiNum)) { 2856 allPressed = false; 2857 break; 2858 } 2859 } 2860 if (allPressed) { 2861 lastSatisfiedTimeRef.current = chordTime; 2862 waitingNotesRef.current = null; 2863 } else { 2864 waitingNotesRef.current = targetNoteNumbers; 2865 if (onPlayPause) onPlayPause(false); 2866 if (onSeek) onSeek(chordTime); 2867 } 2868 } 2869 } 2870 } else if (waitingNotesRef.current) { 2871 let allPressed = true; 2872 for (const midiNum of waitingNotesRef.current) { 2873 if (!previewActiveNotesRef.current.has(midiNum)) { 2874 allPressed = false; 2875 break; 2876 } 2877 } 2878 if (allPressed) { 2879 const satisfiedTime = Array.from(waitingNotesRef.current)[0]; 2880 const matchedNote = currentMelData.find((n) => n.n === satisfiedTime && Math.abs(n.t - t) < 0.25); 2881 const satisfiedTimeSec = matchedNote ? matchedNote.t : t; 2882 lastSatisfiedTimeRef.current = satisfiedTimeSec; 2883 waitingNotesRef.current = null; 2884 if (onPlayPause) onPlayPause(true); 2885 } 2886 } 2887 } else { 2888 waitingNotesRef.current = null; 2889 } 2890 renderSharedKeyboard(ctx, canvas.width, canvas.height, canvas.height, /* @__PURE__ */ new Set([...activeSongNotes, ...previewActiveNotesRef.current]), keyboardState === "on_pc" ? "pc" : keyboardState === "on_notes" ? "notes" : "off", baseOctaveRef.current, waitingNotesRef.current || void 0); 2891 } 2892 } 2893 animationFrameRef.current = requestAnimationFrame(renderLoop); 2894 }; 2895 renderLoop(); 2896 return () => { 2897 if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); 2898 }; 2899 }, [keyboardState, previewActiveNotesRef, practiceMode, onPlayPause, onSeek]); 2900 reactExports.useEffect(() => { 2901 if (isMMOMode && keyboardState === "off") { 2902 setKeyboardState("on_pc"); 2903 } else if (!isMMOMode && keyboardState !== "off") { 2904 setKeyboardState("off"); 2905 } 2906 }, [isMMOMode]); 2907 reactExports.useEffect(() => { 2908 const onGameModeMuteFn = onGameModeMuteRef.current; 2909 if (!onGameModeMuteFn) return; 2910 if (!isMMOMode) { 2911 console.log("[SheetMusicViewer] Play-Along Ended, Unmuting All"); 2912 onGameModeMuteFn(null); 2913 return; 2914 } 2915 let lastMutedStr = ""; 2916 const checkForMuteUpdates = () => { 2917 const time = currentTimeRef.current; 2918 const currentMelData = melodyDataRef.current; 2919 const currentInstruments = processedInstrumentsRef.current; 2920 selectedTrackIndexRef.current; 2921 const onMuteFn = onGameModeMuteRef.current; 2922 if (!onMuteFn) return; 2923 const activeTracks = /* @__PURE__ */ new Set(); 2924 const nextTracks = /* @__PURE__ */ new Set(); 2925 let nextTime = Infinity; 2926 let bestIdx = -1; 2927 if (currentMelData && currentMelData.length > 0 && currentInstruments) { 2928 for (const note of currentMelData) { 2929 let idx = -1; 2930 if (note.trackIdx !== void 0 && note.trackIdx !== null) { 2931 const num = Number(note.trackIdx); 2932 if (!isNaN(num) && num >= 0 && num < currentInstruments.length) { 2933 idx = num; 2934 } 2935 } 2936 if (idx === -1) { 2937 const name = note.i; 2938 idx = currentInstruments.findIndex( 2939 (inst) => inst.originalName === name || inst.displayName === name || inst.name === name 2940 ); 2941 } 2942 if (idx === -1) continue; 2943 if (note.t <= time && note.t + note.d > time) { 2944 activeTracks.add(idx); 2945 if (bestIdx === -1) bestIdx = idx; 2946 } 2947 if (note.t > time) {
2948 if (nextTime === Infinity) nextTime = note.t; 2949 if (note.t === nextTime) { 2950 nextTracks.add(idx); 2951 } 2952 } 2953 if (nextTime !== Infinity && note.t > nextTime) break; 2954 } 2955 } 2956 const finalMuteTracks = activeTracks.size > 0 ? activeTracks : nextTracks; 2957 if (bestIdx === -1 && nextTracks.size > 0) { 2958 bestIdx = Array.from(nextTracks)[0]; 2959 } 2960 const newMuted = Array.from(finalMuteTracks); 2961 newMuted.sort(); 2962 const newMutedStr = newMuted.join(","); 2963 if (newMutedStr !== lastMutedStr) { 2964 lastMutedStr = newMutedStr; 2965 const trackNames = newMuted.map((idx) => { 2966 const track = currentInstruments && currentInstruments[idx]; 2967 return track ? track.displayName || track.originalName || track.name : "Unknown"; 2968 }).join(", "); 2969 let noteDetail = activeTracks.size > 0 ? " (Active Notes)" : " (Rests / Sticky Next Notes)"; 2970 console.log(`%c[SheetMusicViewer] Play-Along Mute Handoff: Tracks [${newMutedStr}] ("${trackNames}")${noteDetail} at currentTime=${time.toFixed(2)}s`, "color: #ff9800; font-weight: bold; font-size: 11px;"); 2971 onMuteFn(newMuted.length > 0 ? newMuted : [0]); 2972 } 2973 }; 2974 const interval = setInterval(checkForMuteUpdates, 100); 2975 checkForMuteUpdates(); 2976 return () => { 2977 clearInterval(interval); 2978 console.log("[SheetMusicViewer] Cleanup: Unmuting All"); 2979 const onMuteFn = onGameModeMuteRef.current; 2980 if (onMuteFn) { 2981 onMuteFn(null); 2982 } 2983 }; 2984 }, [isMMOMode]); 2985 reactExports.useEffect(() => { 2986 localStorage.setItem("sheet_show_chords", showChords ? "true" : "false"); 2987 }, [showChords]); 2988 reactExports.useEffect(() => { 2989 localStorage.setItem("sheet_show_roman", showRoman ? "true" : "false"); 2990 }, [showRoman]); 2991 reactExports.useEffect(() => { 2992 localStorage.setItem("sheet_show_cursor", showCursor ? "true" : "false"); 2993 }, [showCursor]); 2994 reactExports.useEffect(() => { 2995 localStorage.setItem("sheet_show_highlight", showHighlight ? "true" : "false"); 2996 }, [showHighlight]); 2997 reactExports.useEffect(() => { 2998 localStorage.setItem("sheet_show_measure_numbers", showMeasureNumbers ? "true" : "false"); 2999 }, [showMeasureNumbers]); 3000 reactExports.useEffect(() => { 3001 setPageInput(currentPage.toString()); 3002 }, [currentPage]); 3003 reactExports.useEffect(() => { 3004 if (!isMenuExpanded) { 3005 setIsAnalysisStudioOpen(false); 3006 } 3007 }, [isMenuExpanded]); 3008 reactExports.useEffect(() => { 3009 if (isMelodyMode) return; 3010 const hasUrtext = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.urtext) || (scorePath == null ? void 0 : scorePath.includes("_urtext")); 3011 const hasKrn = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) || (scorePath == null ? void 0 : scorePath.includes("_krn")); 3012 const hasStudio = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore) || (scorePath == null ? void 0 : scorePath.includes("_studio")) || (scorePath == null ? void 0 : scorePath.includes("_musescore")); 3013 const hasAbc = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.abc) || (scorePath == null ? void 0 : scorePath.includes("_abc")); 3014 const hasLy = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.ly) || (scorePath == null ? void 0 : scorePath.includes("_ly")); 3015 if (hasUrtext) { 3016 setQuantization(900); 3017 } else if (hasKrn) { 3018 setQuantization(800); 3019 } else if (hasStudio) { 3020 setQuantization(500); 3021 } else if (hasAbc) { 3022 setQuantization(700); 3023 } else if (hasLy) { 3024 setQuantization(600); 3025 } else if (recommendedQuantization) { 3026 let q = recommendedQuantization; 3027 if (q === 4) q = 8; 3028 if (q === 32) q = 16; 3029 if (q === 8 && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || q === 16 && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16)) { 3030 setQuantization(q); 3031 } else if (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) { 3032 setQuantization(300); 3033 } else { 3034 setQuantization(500); 3035 } 3036 } else if (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) { 3037 setQuantization(300); 3038 } else { 3039 setQuantization(500); 3040 } 3041 }, [midiId, recommendedQuantization, scorePath, quantizedScorePaths, isMelodyMode]); 3042 reactExports.useEffect(() => { 3043 if ((layoutMode === "a4" || layoutMode === "a3") && dimensions.width > 100 && dimensions.height > 100) { 3044 const isA3 = layoutMode === "a3"; 3045 const paperW = isA3 ? 420 : 210; 3046 const paperH = 297; 3047 const availW = dimensions.width - 32; 3048 const availH = dimensions.height - 32; 3049 const paperW_px = paperW * 3.78; 3050 const paperH_px = paperH * 3.78; 3051 const zoomW = availW / paperW_px * 100; 3052 const zoomH = availH / paperH_px * 100; 3053 const bestZoom = Math.floor(Math.min(zoomW, zoomH)); 3054 setPaperZoom(bestZoom); 3055 setPaperZoomInput(bestZoom.toString()); 3056 const paperScale = 40; 3057 setScale(paperScale);
3058 setZoomInput(paperScale.toString()); 3059 } 3060 }, [layoutMode, dimensions.width, dimensions.height]); 3061 const handleScoreClick = (e) => { 3062 const target = e.target; 3063 const noteElement = target.closest(".note"); 3064 if (noteElement && verovioToolkit) { 3065 const id = noteElement.getAttribute("id"); 3066 if (id) { 3067 try { 3068 const midiValsStr = verovioToolkit.getMIDIValuesForElement(id); 3069 const midiVals = typeof midiValsStr === "string" ? JSON.parse(midiValsStr) : midiValsStr; 3070 if (midiVals && typeof midiVals.pitch === "number") { 3071 playNote(midiVals.pitch); 3072 setTimeout(() => stopNote(midiVals.pitch), 500); 3073 } 3074 } catch (err) { 3075 console.error("Failed to play note", err); 3076 } 3077 } 3078 } 3079 }; 3080 reactExports.useEffect(() => { 3081 if (!isDraggingBar) return; 3082 let lastRaf = null; 3083 let lastSeekTime = 0; 3084 const handleMouseMove = (e) => { 3085 if (lastRaf) cancelAnimationFrame(lastRaf); 3086 lastRaf = requestAnimationFrame(() => { 3087 if (!paperRef.current || !verovioToolkit || !onSeek) return; 3088 const now = Date.now(); 3089 if (now - lastSeekTime < 50) return; 3090 const notes = paperRef.current.querySelectorAll(".note, .measure"); 3091 let bestEl = null; 3092 let minDist = Infinity; 3093 for (let i = 0; i < notes.length; i++) { 3094 const n = notes[i]; 3095 const rect = n.getBoundingClientRect(); 3096 const distX = Math.max(rect.left - e.clientX, 0, e.clientX - rect.right); 3097 const distY = Math.max(rect.top - e.clientY, 0, e.clientY - rect.bottom); 3098 const dist = distX * distX + distY * distY; 3099 if (dist < minDist) { 3100 minDist = dist; 3101 bestEl = n; 3102 } 3103 } 3104 if (bestEl) { 3105 const id = bestEl.getAttribute("id"); 3106 if (id) { 3107 const timeMs = verovioToolkit.getTimeForElement(id); 3108 if (timeMs >= 0) { 3109 const aTime = verovioToAudioTime(timeMs / 1e3); 3110 if (!isNaN(aTime) && aTime >= 0) { 3111 onSeek(aTime); 3112 lastSeekTime = now; 3113 } 3114 } 3115 } 3116 } 3117 }); 3118 }; 3119 const handleMouseUp = () => { 3120 setIsDraggingBar(false); 3121 }; 3122 window.addEventListener("mousemove", handleMouseMove); 3123 window.addEventListener("mouseup", handleMouseUp); 3124 return () => { 3125 if (lastRaf) cancelAnimationFrame(lastRaf); 3126 window.removeEventListener("mousemove", handleMouseMove); 3127 window.removeEventListener("mouseup", handleMouseUp); 3128 }; 3129 }, [isDraggingBar, verovioToolkit, onSeek, verovioToAudioTime]); 3130 reactExports.useEffect(() => { 3131 if (!containerRef.current) return; 3132 const observer = new ResizeObserver((entries) => { 3133 for (const entry of entries) { 3134 const w = entry.contentRect.width; 3135 const h = entry.contentRect.height; 3136 if (w > 100 && h > 100) { 3137 if (resizeTimeout.current) clearTimeout(resizeTimeout.current); 3138 resizeTimeout.current = setTimeout(() => { 3139 setDimensions((prev) => { 3140 if (Math.abs(prev.width - w) < 4 && Math.abs(prev.height - h) < 4) { 3141 return prev; 3142 } 3143 return { width: w, height: h }; 3144 }); 3145 }, 200); 3146 } 3147 } 3148 }); 3149 observer.observe(containerRef.current); 3150 return () => { 3151 observer.disconnect(); 3152 if (resizeTimeout.current) clearTimeout(resizeTimeout.current); 3153 }; 3154 }, []); 3155 reactExports.useEffect(() => { 3156 var _a, _b, _c; 3157 if (!isAutoZoom || dimensions.height < 100 || !isDataLoaded) return; 3158 if (layoutMode === "a4" || layoutMode === "a3") return; 3159 const domStaves = (_c = (_b = (_a = paperRef.current) == null ? void 0 : _a.querySelector(".measure")) == null ? void 0 : _b.querySelectorAll(".staff")) == null ? void 0 : _c.length; 3160 const staffCount = domStaves && domStaves > 0 ? domStaves : estimatedStaffCount;
3161 const baseHeightPerStaff = 280; 3162 const totalHeightUnits = 120 + staffCount * baseHeightPerStaff; 3163 const availableHeight = dimensions.height - 40; 3164 let idealScale = Math.floor(availableHeight / totalHeightUnits * 65); 3165 const maxAutoZoomScale = isMelodyMode ? 30 : 45; 3166 idealScale = Math.max(20, Math.min(maxAutoZoomScale, idealScale)); 3167 if (idealScale !== scale) { 3168 setScale(idealScale); 3169 setZoomInput(idealScale.toString()); 3170 } 3171 }, [isAutoZoom, dimensions.height, estimatedStaffCount, isDataLoaded, isMelodyMode, layoutMode, scale, svgContent]); 3172 const seekToPage = (page) => { 3173 if (!verovioToolkit || !onSeek) return; 3174 const svg = verovioToolkit.renderToSVG(page, {}); 3175 const parser = new DOMParser(); 3176 const doc = parser.parseFromString(svg, "image/svg+xml"); 3177 const measure = doc.querySelector(".measure"); 3178 if (measure) { 3179 const id = measure.getAttribute("id"); 3180 const vTime = verovioToolkit.getTimeForElement(id) / 1e3; 3181 const aTime = verovioToAudioTime(vTime); 3182 onSeek(aTime); 3183 } 3184 }; 3185 const handleZoomChange = (e) => { 3186 setZoomInput(e.target.value); 3187 const val = parseInt(e.target.value); 3188 if (!isNaN(val)) { 3189 setIsAutoZoom(false); 3190 setScale(Math.max(10, Math.min(500, val))); 3191 } 3192 }; 3193 const handleZoomBlur = () => setZoomInput(scale.toString()); 3194 const handleZoomKeyDown = (e) => { 3195 if (e.key === "Enter") e.target.blur(); 3196 }; 3197 const handleRecommendQuantization = async () => { 3198 if (!midiId || quantization === 0) return; 3199 try { 3200 await fetch(`/api/tracks/${midiId}/quantization-recommendation`, { 3201 method: "POST", 3202 headers: { "Content-Type": "application/json" }, 3203 body: JSON.stringify({ value: quantization, type: "recommend" }) 3204 }); 3205 setHasRecommended(true); 3206 setTimeout(() => setHasRecommended(false), 2e3); 3207 } catch (e) { 3208 console.error("Failed to recommend:", e); 3209 } 3210 }; 3211 const handleDownloadPNG = async () => { 3212 if (!verovioToolkit) return; 3213 try { 3214 setLoading(true); 3215 const svg = verovioToolkit.renderToSVG(currentPage, {}); 3216 const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); 3217 const url = URL.createObjectURL(svgBlob); 3218 const img = new Image(); 3219 img.onload = () => { 3220 const canvas = document.createElement("canvas"); 3221 canvas.width = img.width * 2; 3222 canvas.height = img.height * 2; 3223 const ctx = canvas.getContext("2d"); 3224 if (ctx) { 3225 ctx.fillStyle = "white"; 3226 ctx.fillRect(0, 0, canvas.width, canvas.height); 3227 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 3228 const pngUrl = canvas.toDataURL("image/png"); 3229 const downloadLink = document.createElement("a"); 3230 downloadLink.href = pngUrl; 3231 downloadLink.download = `${title || "score"}_page_${currentPage}.png`; 3232 document.body.appendChild(downloadLink); 3233 downloadLink.click(); 3234 document.body.removeChild(downloadLink); 3235 } 3236 URL.revokeObjectURL(url); 3237 setLoading(false); 3238 }; 3239 img.src = url; 3240 } catch (e) { 3241 console.error(e); 3242 setLoading(false); 3243 } 3244 }; 3245 const handleDownloadPDF = async () => { 3246 if (!verovioToolkit) return; 3247 try { 3248 setLoading(true); 3249 const { default: jsPDF } = await __vitePreload(async () => { 3250 const { default: jsPDF2 } = await import("./vendor-pdf-CjPCU7YA.js"); 3251 return { default: jsPDF2 }; 3252 }, true ? __vite__mapDeps([0,1,2,3,4,5]) : void 0); 3253 const pdf = new jsPDF({ orientation: layoutMode === "a3" ? "landscape" : "portrait", unit: "mm", format: layoutMode === "a3" ? "a3" : "a4" }); 3254 for (let i = 1; i <= pageCount; i++) { 3255 if (i > 1) pdf.addPage(); 3256 const svg = verovioToolkit.renderToSVG(i, {}); 3257 const canvas = document.createElement("canvas"); 3258 const img = new Image(); 3259 const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); 3260 const url = URL.createObjectURL(svgBlob); 3261 await new Promise((resolve) => { 3262 img.onload = () => { 3263 canvas.width = img.width * 2;
3264 canvas.height = img.height * 2; 3265 const ctx = canvas.getContext("2d"); 3266 if (ctx) { 3267 ctx.fillStyle = "white"; 3268 ctx.fillRect(0, 0, canvas.width, canvas.height); 3269 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 3270 pdf.addImage(canvas.toDataURL("image/jpeg", 0.95), "JPEG", 0, 0, layoutMode === "a3" ? 420 : 210, layoutMode === "a3" ? 297 : 297); 3271 } 3272 URL.revokeObjectURL(url); 3273 resolve(null); 3274 }; 3275 img.src = url; 3276 }); 3277 } 3278 pdf.save(`${title || "score"}.pdf`); 3279 } catch (e) { 3280 console.error(e); 3281 } finally { 3282 setLoading(false); 3283 } 3284 }; 3285 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full w-full bg-[#fdfaf5] text-zinc-900 select-none relative overflow-hidden", children: [ 3286 /* @__PURE__ */ jsxRuntimeExports.jsx( 3287 SheetMusicToolbar, 3288 { 3289 ...{ 3290 layoutMode, 3291 setLayoutMode, 3292 isAutoZoom, 3293 setIsAutoZoom, 3294 scale, 3295 setScale, 3296 zoomInput, 3297 setZoomInput, 3298 handleZoomChange, 3299 handleZoomBlur, 3300 handleZoomKeyDown, 3301 density, 3302 setDensity, 3303 onToggleMelody, 3304 isMelodyMode, 3305 isMMOMode, 3306 setIsMMOMode, 3307 onGameModeMute, 3308 melodyData, 3309 processedInstruments, 3310 isAnalysisStudioOpen, 3311 setIsAnalysisStudioOpen, 3312 isMenuExpanded, 3313 setIsMenuExpanded, 3314 showChords, 3315 showRoman, 3316 showTimeline, 3317 quantization, 3318 setQuantization, 3319 quantizedScorePaths, 3320 hasRecommended, 3321 handleRecommendQuantization, 3322 pageTurnLookahead, 3323 setPageTurnLookahead, 3324 melodySource, 3325 setMelodySource, 3326 handleDownloadPNG, 3327 handleDownloadPDF, 3328 loading, 3329 theme 3330 } 3331 } 3332 ), 3333 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 relative flex flex-col", children: [
3334 /* @__PURE__ */ jsxRuntimeExports.jsx( 3335 SheetMusicCanvas, 3336 { 3337 ...{ 3338 containerRef, 3339 paperRef, 3340 cursorRef, 3341 svgContent, 3342 loading, 3343 loadingProgress, 3344 isDataLoaded, 3345 pageCount, 3346 currentPage, 3347 setCurrentPage, 3348 renderPage, 3349 layoutMode, 3350 dimensions, 3351 paperZoom, 3352 setPaperZoom, 3353 paperZoomInput, 3354 setPaperZoomInput, 3355 visibleTrackCount, 3356 showCursor, 3357 setIsDraggingBar, 3358 onSeek, 3359 pageInput, 3360 setPageInput, 3361 seekToPage, 3362 lastManualPageChangeRef, 3363 lastManualPageTimeRef, 3364 currentTime, 3365 handleScoreClick, 3366 handleScroll, 3367 title: title || "HamieNET.com Score", 3368 midiId 3369 } 3370 } 3371 ), 3372 showPills && sections && sections.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 left-4 z-40 flex flex-col gap-2", children: sections.map((s, i) => currentTime >= s.time && (sections[i + 1] ? currentTime < sections[i + 1].time : true) && /* @__P
3372URE__ */ jsxRuntimeExports.jsx(SectionPill, { currentSection: s.name || s.label || null }, i)) }), 3373 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `absolute right-4 flex flex-col gap-2 z-50 transition-all ${keyboardState !== "off" ? "bottom-4" : "bottom-16"}`, children: [ 3374 keyboardState !== "off" && /* @__PURE__ */ jsxRuntimeExports.jsx( 3375 "button", 3376 { 3377 onClick: (e) => { 3378 e.stopPropagation(); 3379 const nextLabel = keyboardState === "on_nolabels" ? "on_pc" : keyboardState === "on_pc" ? "on_notes" : "on_nolabels"; 3380 setKeyboardState(nextLabel); 3381 }, 3382 className: "p-2 rounded-full backdrop-blur-md border transition-all shadow-lg opacity-60 hover:opacity-100 bg-indigo-600/80 border-indigo-400/50 text-white", 3383 title: `Labels: ${keyboardState === "on_pc" ? "PC Keys" : keyboardState === "on_notes" ? "Note Names" : "Off"}`, 3384 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Monitor, { size: 16 }) 3385 } 3386 ),
3387 /* @__PURE__ */ jsxRuntimeExports.jsx( 3388 "button", 3389 { 3390 onClick: (e) => { 3391 e.stopPropagation(); 3392 setKeyboardState((prev) => { 3393 const next = prev === "off" ? "on_nolabels" : prev === "on_nolabels" ? "on_pc" : prev === "on_pc" ? "on_notes" : "off"; 3394 if (next === "off" && isMMOMode) { 3395 setIsMMOMode(false); 3396 if (onGameModeMute) onGameModeMute(null); 3397 } 3398 return next; 3399 }); 3400 }, 3401 className: `p-2 rounded-full backdrop-blur-md border transition-all shadow-lg opacity-40 hover:opacity-100 ${keyboardState !== "off" ? "bg-indigo-600/80 border-indigo-400/50 text-white" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`, 3402 title: "Toggle Keyboard", 3403 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 16 }) 3404 } 3405 ), 3406 /* @__PURE__ */ jsxRuntimeExports.jsx( 3407 "button", 3408 { 3409 onClick: (e) => { 3410 e.stopPropagation(); 3411 setPracticeMode((prev) => { 3412 const next = !prev; 3413 if (next) { 3414 if (keyboardState === "off") setKeyboardState("on_pc"); 3415 setIsMMOMode(true); 3416 } else { 3417 setIsMMOMode(false); 3418 if (onGameModeMute) onGameModeMute(null); 3419 } 3420 return next; 3421 }); 3422 }, 3423 className: `p-2 rounded-full backdrop-blur-md border transition-all shadow-lg opacity-60 hover:opacity-100 ${practiceMode ? "bg-amber-600/80 border-amber-400/50 text-white shadow-lg" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`, 3424 title: practiceMode ? "Disable Practice Mode" : "Enable Practice Mode", 3425 children: /* @__PURE__ */ jsxRuntimeExports.jsx(GraduationCap, { size: 16 }) 3426 } 3427 ) 3428 ] }) 3429 ] }), 3430 keyboardState !== "off" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: keyboardContainerRef, className: "w-full bg-[#050505] border-t border-zinc-800 shrink-0 relative z-40", style: { height: "15%" }, children: [
3431 /* @__PURE__ */ jsxRuntimeExports.jsx( 3432 "canvas", 3433 { 3434 ref: keyboardCanvasRef, 3435 className: "w-full h-full cursor-pointer", 3436 onPointerDown: (e) => { 3437 var _a; 3438 const rect = (_a = keyboardCanvasRef.current) == null ? void 0 : _a.getBoundingClientRect(); 3439 if (rect) { 3440 const midiVal = getMidiFromKeyboardClick(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height); 3441 if (midiVal !== null) playNote(midiVal); 3442 } 3443 }, 3444 onPointerUp: stopAllNotes, 3445 onPointerLeave: stopAllNotes 3446 } 3447 ), 3448 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => { 3449 setKeyboardState("off"); 3450 if (isMMOMode) { 3451 setIsMMOMode(false); 3452 if (onGameModeMute) onGameModeMute(null); 3453 } 3454 }, className: "absolute top-2 right-2 p-1 bg-white/10 hover:bg-white/20 rounded text-white/50", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 14 }) }) 3455 ] }), 3456 showTimeline && /* @__PURE__ */ jsxRuntimeExports.jsx(StructuralTimeline, { sections, currentTime, duration, onSeek }), 3457 isAnalysisStudioOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 3458 AnalysisStudioMenu, 3459 { 3460 isOpen: isAnalysisStudioOpen, 3461 onClose: () => setIsAnalysisStudioOpen(false), 3462 showChords, 3463 setShowChords, 3464 showRoman, 3465 setShowRoman, 3466 showTimeline, 3467 setShowTimeline, 3468 showPills, 3469 setShowPills, 3470 showMeasureNumbers, 3471 setShowMeasureNumbers, 3472 showHighlight, 3473 setShowHighlight, 3474 showCursor, 3475 setShowCursor, 3476 hasSections: sections && sections.length > 0, 3477 hasChordsData: hasHarmonyChords, 3478 hasRomanData: hasHarmonyRoman 3479 } 3480 ) 3481 ] }); 3482}; 3483export { 3484 SheetMusicViewer as default 3485};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.