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