1import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as Midi } from "./vendor-audio-DH3w0BzW.js"; 3import { u as useMidiKeyboard } from "./useMidiKeyboard-DlyEIN8T.js"; 4import { MidiEngine } from "./MidiEngine-BqW6hqRc.js"; 5const EditorContext = reactExports.createContext(null); 6const useEditor = () => { 7 const ctx = reactExports.useContext(EditorContext); 8 if (!ctx) throw new Error("useEditor must be used within EditorProvider"); 9 return ctx; 10}; 11const EditorProvider = ({ children, projectId }) => { 12 const engineRef = reactExports.useRef(null); 13 const [isEngineReady, setIsEngineReady] = reactExports.useState(false); 14 const [engineError, setEngineError] = reactExports.useState(null); 15 const [loadingStatus, setLoadingStatus] = reactExports.useState("Initializing DAW Engine..."); 16 const [midiVersion, setMidiVersion] = reactExports.useState(0); 17 const [currentTime, setCurrentTime_] = reactExports.useState(0); 18 const currentTimeRef = reactExports.useRef(0); 19 const setCurrentTime = (t) => { 20 currentTimeRef.current = t; 21 setCurrentTime_(t); 22 }; 23 const [isPlaying, setIsPlaying_] = reactExports.useState(false); 24 const isPlayingRef = reactExports.useRef(false); 25 const setIsPlaying = (p) => { 26 isPlayingRef.current = p; 27 setIsPlaying_(p); 28 }; 29 const [duration, setDuration] = reactExports.useState(0); 30 const playbackStartTimeRef = reactExports.useRef(0); 31 const playbackStartOffsetRef = reactExports.useRef(0); 32 const playbackModeRef = reactExports.useRef("song"); 33 const activePatternIdRef = reactExports.useRef(null); 34 const activeTrackIndexRef = reactExports.useRef(null); 35 const [activeTrackIndex, setActiveTrackIndex_] = reactExports.useState(null); 36 const setActiveTrackIndex = (idx) => { 37 activeTrackIndexRef.current = idx; 38 setActiveTrackIndex_(idx); 39 }; 40 const [selectedNoteIds, setSelectedNoteIds] = reactExports.useState([]); 41 const [selectedTempoIndex, setSelectedTempoIndex] = reactExports.useState(null); 42 const [selectedTimeSigIndex, setSelectedTimeSigIndex] = reactExports.useState(null); 43 const [hint, setHint] = reactExports.useState(null); 44 const [patterns, setPatterns_] = reactExports.useState([]); 45 const patternsRef = reactExports.useRef([]); 46 const setPatterns = reactExports.useCallback((pats) => { 47 setPatterns_((prev) => { 48 const next = typeof pats === "function" ? pats(prev) : pats; 49 patternsRef.current = next; 50 return next; 51 }); 52 }, []); 53 const [activePatternId, setActivePatternId_] = reactExports.useState(null); 54 const setActivePatternId = (id) => { 55 activePatternIdRef.current = id; 56 setActivePatternId_(id); 57 }; 58 const [playlistTracks, setPlaylistTracks_] = reactExports.useState([]); 59 const playlistTracksRef = reactExports.useRef([]); 60 const setPlaylistTracks = reactExports.useCallback((tracks) => { 61 setPlaylistTracks_((prev) => { 62 const next = typeof tracks === "function" ? tracks(prev) : tracks; 63 playlistTracksRef.current = next; 64 return next; 65 }); 66 }, []); 67 const [playlistClips, setPlaylistClips_] = reactExports.useState([]); 68 const playlistClipsRef = reactExports.useRef([]); 69 const setPlaylistClips = reactExports.useCallback((clips) => { 70 setPlaylistClips_((prev) => { 71 const next = typeof clips === "function" ? clips(prev) : clips; 72 playlistClipsRef.current = next; 73 return next; 74 }); 75 }, []); 76 const [selectedClipIds, setSelectedClipIds] = reactExports.useState([]); 77 const [automationClips, setAutomationClips_] = reactExports.useState([]); 78 const automationClipsRef = reactExports.useRef([]); 79 const setAutomationClips = reactExports.useCallback((clips) => { 80 setAutomationClips_((prev) => { 81 const next = typeof clips === "function" ? clips(prev) : clips; 82 automationClipsRef.current = next; 83 return next; 84 }); 85 }, []); 86 const [timeMarkers, setTimeMarkers] = reactExports.useState([]); 87 const [loopRegion, setLoopRegion] = reactExports.useState(null); 88 const [loopMarkerTick, setLoopMarkerTick_] = reactExports.useState(null); 89 const loopMarkerTickRef = reactExports.useRef(null);
90 const setLoopMarkerTick = (tick) => { 91 loopMarkerTickRef.current = tick; 92 setLoopMarkerTick_(tick); 93 }; 94 const [gridSnap, setGridSnap] = reactExports.useState("beat"); 95 const [currentPlaylistTool, setCurrentPlaylistTool] = reactExports.useState("draw"); 96 const [easyMode, setEasyMode_] = reactExports.useState(false); 97 const setEasyMode = setEasyMode_; 98 const [easyToProMergeState, setEasyToProMergeState] = reactExports.useState(null); 99 const proModeBackupRef = reactExports.useRef(null); 100 const absoluteNotesSnapshotRef = reactExports.useRef([]); 101 const [playbackMode, setPlaybackMode_] = reactExports.useState("song"); 102 const setPlaybackMode = (mode) => { 103 playbackModeRef.current = mode; 104 setPlaybackMode_(mode); 105 }; 106 const [isRecording, setIsRecording] = reactExports.useState(false); 107 const [isTypingKeyboardEnabled, setIsTypingKeyboardEnabled] = reactExports.useState(false); 108 const [activeMidiKeys, setActiveMidiKeys] = reactExports.useState(/* @__PURE__ */ new Set()); 109 const [isCountdownEnabled, setIsCountdownEnabled] = reactExports.useState(false); 110 const [isMetronomeEnabled, setIsMetronomeEnabled] = reactExports.useState(false); 111 const [isLoopRecordingEnabled, setIsLoopRecordingEnabled] = reactExports.useState(false); 112 const [masterVolume, setMasterVolumeState] = reactExports.useState(0.8); 113 const [masterPitch, setMasterPitchState] = reactExports.useState(0); 114 const [tempo, setTempoState] = reactExports.useState(130); 115 const tempoRef = reactExports.useRef(130); 116 const metronomeEnabledRef = reactExports.useRef(false); 117 reactExports.useEffect(() => { 118 tempoRef.current = tempo; 119 }, [tempo]); 120 reactExports.useEffect(() => { 121 metronomeEnabledRef.current = isMetronomeEnabled; 122 }, [isMetronomeEnabled]); 123 const activePattern = patterns.find((p) => p.id === activePatternId) || null; 124 const handleSetSelectedNoteIds = reactExports.useCallback((ids) => { 125 setSelectedNoteIds(ids); 126 if (ids.length > 0) { 127 setSelectedTempoIndex(null); 128 setSelectedTimeSigIndex(null); 129 } 130 }, []); 131 const handleSetSelectedTempoIndex = reactExports.useCallback((idx) => { 132 setSelectedTempoIndex(idx); 133 if (idx !== null) { 134 setSelectedNoteIds([]); 135 setSelectedTimeSigIndex(null); 136 } 137 }, []); 138 const handleSetSelectedTimeSigIndex = reactExports.useCallback((idx) => { 139 setSelectedTimeSigIndex(idx); 140 if (idx !== null) { 141 setSelectedNoteIds([]); 142 setSelectedTempoIndex(null); 143 } 144 }, []); 145 const [undoStack, setUndoStack] = reactExports.useState([]); 146 const [redoStack, setRedoStack] = reactExports.useState([]); 147 const [clipboard, setClipboard] = reactExports.useState(null); 148 const playMetronomeTick = reactExports.useCallback((isDownbeat) => { 149 if (!engineRef.current) return; 150 const ctx = engineRef.current.audioCtx; 151 if (!ctx) return; 152 const now = ctx.currentTime; 153 const osc = ctx.createOscillator(); 154 const gain = ctx.createGain(); 155 const filter = ctx.createBiquadFilter(); 156 osc.type = isDownbeat ? "sine" : "triangle"; 157 osc.frequency.setValueAtTime(isDownbeat ? 2e3 : 800, now); 158 filter.type = "lowpass"; 159 filter.frequency.setValueAtTime(isDownbeat ? 4e3 : 2e3, now); 160 gain.gain.setValueAtTime(0, now); 161 gain.gain.linearRampToValueAtTime(isDownbeat ? 0.8 : 0.3, now + 2e-3); 162 gain.gain.exponentialRampToValueAtTime(1e-3, now + (isDownbeat ? 0.15 : 0.06)); 163 osc.connect(filter); 164 filter.connect(gain); 165 gain.connect(engineRef.current.destNode); 166 osc.start(now); 167 osc.stop(now + (isDownbeat ? 0.12 : 0.08)); 168 setTimeout(() => { 169 osc.disconnect(); 170 gain.disconnect(); 171 filter.disconnect(); 172 }, 200); 173 }, []); 174 const rafRef = reactExports.useRef(null); 175 const lastBeatRef = reactExports.useRef(-1); 176 const loop = reactExports.useCallback(() => { 177 var _a, _b, _c; 178 if (engineRef.current) { 179 const hasNotes = engineRef.current.getNoteCount() > 0; 180 let time = 0; 181 let playing = false; 182 if (hasNotes) { 183 time = engineRef.current.getCurrentTime(); 184 playing = engineRef.current.isPlaying() && isPlayingRef.current; 185 const currentTicks = engineRef.current.secondsToTicks(time); 186 if (loopRegion) { 187 if (currentTicks >= loopRegion.endTick) { 188 const startSec = engineRef.current.ticksToSeconds(loopRegion.startTick); 189 engineRef.current.seek(startSec); 190 time = startSec; 191 setCurrentTime(startSec); 192 playbackStartOffsetRef.current = startSec; 193 playbackStartTimeRef.current = performance.now(); 194 } 195 } else { 196 const songEndSecs = engineRef.current.getDuration(); 197 if (time >= songEndSecs) { 198 if (playbackModeRef.current === "pat") { 199 if (loopMarkerTickRef.current !== null) { 200 const markerSec = engineRef.current.ticksToSeconds(loopMarkerTickRef.current); 201 engineRef.current.seek(markerSec); 202 time = markerSec; 203 setCurrentTime(markerSec); 204 playbackStartOffsetRef.current = markerSec; 205 playbackStartTimeRef.current = performance.now(); 206 } else { 207 engineRef.current.seek(0); 208 time = 0; 209 setCurrentTime(0); 210 playbackStartOffsetRef.current = 0; 211 playbackStartTimeRef.current = performance.now(); 212 } 213 } else { 214 engineRef.current.pause(); 215 engineRef.current.seek(0); 216 time = 0; 217 setCurrentTime(0); 218 setIsPlaying(false); 219 playing = false;
220 } 221 } 222 } 223 setIsPlaying(playing); 224 setCurrentTime(time); 225 } else { 226 playing = isPlayingRef.current; 227 if (playing) { 228 time = (performance.now() - playbackStartTimeRef.current) / 1e3 + playbackStartOffsetRef.current; 229 if (loopRegion) { 230 const currentTicks = engineRef.current.secondsToTicks(time); 231 if (currentTicks >= loopRegion.endTick) { 232 const startSec = engineRef.current.ticksToSeconds(loopRegion.startTick); 233 time = startSec; 234 playbackStartOffsetRef.current = startSec; 235 playbackStartTimeRef.current = performance.now(); 236 } 237 } 238 setCurrentTime(time); 239 } else { 240 time = currentTimeRef.current; 241 } 242 } 243 if (playing && metronomeEnabledRef.current) { 244 const beatDuration = 60 / tempoRef.current; 245 const currentBeat = Math.floor(time / beatDuration); 246 if (currentBeat !== lastBeatRef.current) { 247 if (lastBeatRef.current !== -1 && Math.abs(currentBeat - lastBeatRef.current) === 1) { 248 const numerator = ((_c = (_b = (_a = engineRef.current.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.timeSignatures[0]) == null ? void 0 : _c.timeSignature[0]) || 4; 249 const isDownbeat = currentBeat % numerator === 0; 250 playMetronomeTick(isDownbeat); 251 } 252 lastBeatRef.current = currentBeat; 253 } 254 } else if (!playing) { 255 lastBeatRef.current = -1; 256 } 257 } 258 rafRef.current = requestAnimationFrame(loop); 259 }, [playMetronomeTick]); 260 reactExports.useEffect(() => { 261 rafRef.current = requestAnimationFrame(loop); 262 return () => { 263 if (rafRef.current) cancelAnimationFrame(rafRef.current); 264 }; 265 }, [loop]); 266 const reconstructMidi = reactExports.useCallback((currentPatterns, currentClips, currentTracks, currentAutomation) => { 267 if (!engineRef.current) return; 268 const midi = engineRef.current.midi; 269 if (!midi) return; 270 while (midi.tracks.length < currentTracks.length) { 271 midi.addTrack(); 272 } 273 while (midi.tracks.length > currentTracks.length) { 274 midi.tracks.pop(); 275 } 276 midi.tracks.forEach((track, idx) => { 277 track.notes = []; 278 track.controlChanges = {}; 279 const pt = currentTracks[idx]; 280 if (pt) { 281 track.name = pt.name; 282 const instNum = pt.instrumentNumber !== void 0 ? pt.instrumentNumber : 0; 283 const chan = pt.channel !== void 0 ? pt.channel : idx === 9 ? 9 : idx % 16; 284 track.instrument.number = instNum; 285 track.channel = chan; 286 if (track.addControlChange) { 287 track.addControlChange({ 288 number: 7, 289 ticks: 0, 290 time: 0, 291 value: pt.volume 292 }); 293 const mappedPan = (pt.panning + 1) / 2; 294 track.addControlChange({ 295 number: 10, 296 ticks: 0, 297 time: 0, 298 value: mappedPan 299 }); 300 } 301 } 302 }); 303 const currentPlaybackMode = playbackModeRef.current; 304 const currentActivePatternId = activePatternIdRef.current; 305 const currentActiveTrackIndex = activeTrackIndexRef.current;
306 let compileClips = currentClips; 307 if (currentPlaybackMode === "pat" || currentPlaybackMode === "song" && currentClips.length === 0) { 308 const activePat = currentPatterns.find((p) => p.id === currentActivePatternId); 309 if (activePat && activePat.notes.length > 0) { 310 const trackIdx = currentActiveTrackIndex !== null ? currentActiveTrackIndex : 0; 311 const maxNoteTick = activePat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 312 const ppq = midi.header.ppq || 96; 313 const ticksPerBar = ppq * 4; 314 const patternLength = activePat.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar); 315 const activeClip = { 316 id: "active-pat-clip", 317 patternId: activePat.id, 318 trackIndex: trackIdx, 319 startTick: 0, 320 durationTicks: patternLength, 321 offsetTicks: 0, 322 isMuted: false 323 }; 324 compileClips = [activeClip]; 325 } else { 326 compileClips = []; 327 } 328 } 329 compileClips.forEach((clip) => { 330 if (clip.isMuted) return; 331 const pat = currentPatterns.find((p) => p.id === clip.patternId); 332 if (!pat || pat.notes.length === 0) { 333 return; 334 } 335 const track = midi.tracks[clip.trackIndex]; 336 if (!track) { 337 console.error(`[DEBUG] Track index ${clip.trackIndex} does not exist in midi.tracks.`); 338 return; 339 } 340 const maxNoteTick = pat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 341 const ppq = midi.header.ppq || 96; 342 const ticksPerBar = ppq * 4; 343 const patternLength = pat.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar); 344 if (patternLength <= 0) return; 345 const clipEndTick = clip.startTick + clip.durationTicks; 346 let loopIdx = 0; 347 while (true) { 348 const loopStartTick = clip.startTick + loopIdx * patternLength - clip.offsetTicks; 349 if (loopStartTick >= clipEndTick) break; 350 pat.notes.forEach((note) => { 351 if (note.isMuted) return; 352 const noteAbsTick = loopStartTick + note.ticks; 353 if (noteAbsTick >= clip.startTick && noteAbsTick < clipEndTick) { 354 const noteEndAbsTick = noteAbsTick + note.durationTicks; 355 const clippedEndTick = Math.min(noteEndAbsTick, clipEndTick); 356 const durTicks = clippedEndTick - noteAbsTick; 357 if (durTicks > 0) { 358 const time = engineRef.current.ticksToSeconds(noteAbsTick); 359 const duration2 = engineRef.current.ticksToSeconds(noteAbsTick + durTicks) - time; 360 track.addNote({ 361 midi: note.midi, 362 ticks: noteAbsTick, 363 durationTicks: durTicks, 364 time, 365 duration: duration2, 366 velocity: note.velocity 367 }); 368 const added = track.notes[track.notes.length - 1]; 369 if (added && note.voice !== void 0) { 370 added.voice = note.voice; 371 } 372 } 373 } 374 }); 375 loopIdx++; 376 } 377 }); 378 const compileAutomation = currentPlaybackMode === "pat" ? [] : currentAutomation; 379 compileAutomation.forEach((clip) => { 380 const track = midi.tracks[clip.trackIndex]; 381 if (!track || clip.nodes.length === 0) return; 382 const sortedNodes = [...clip.nodes].sort((a, b) => a.tick - b.tick); 383 const clipStart = clip.startTick; 384 const clipEnd = clip.startTick + clip.durationTicks; 385 const ppq = midi.header.ppq || 96; 386 const samplingInterval = Math.max(12, Math.round(ppq / 8)); 387 const evaluateCurve = (tick) => { 388 if (sortedNodes.length === 0) return 0.5; 389 if (tick <= sortedNodes[0].tick) return sortedNodes[0].value; 390 if (tick >= sortedNodes[sortedNodes.length - 1].tick) return sortedNodes[sortedNodes.length - 1].value; 391 let i = 0; 392 while (i < sortedNodes.length - 1 && tick > sortedNodes[i + 1].tick) { 393 i++; 394 } 395 const n1 = sortedNodes[i]; 396 const n2 = sortedNodes[i + 1]; 397 const t = (tick - n1.tick) / (n2.tick - n1.tick); 398 if (Math.abs(n1.tension || 0) < 0.01) { 399 return n1.value + t * (n2.value - n1.value); 400 } else {
401 const k = (n1.tension || 0) * 5; 402 const curveT = (Math.exp(t * k) - 1) / (Math.exp(k) - 1); 403 return n1.value + curveT * (n2.value - n1.value); 404 } 405 }; 406 if (track.addControlChange) { 407 for (let tick = clipStart; tick <= clipEnd; tick += samplingInterval) { 408 const val = evaluateCurve(tick); 409 const time = engineRef.current.ticksToSeconds(tick); 410 track.addControlChange({ 411 number: clip.ccNumber, 412 ticks: tick, 413 time, 414 value: val 415 }); 416 } 417 } 418 }); 419 }, []); 420 const forceSync = reactExports.useCallback(async () => { 421 if (engineRef.current) { 422 const midi = engineRef.current.midi; 423 if (midi) { 424 let nextTracks = [...playlistTracksRef.current]; 425 if (midi.tracks.length > nextTracks.length) { 426 for (let i = nextTracks.length; i < midi.tracks.length; i++) { 427 const mt = midi.tracks[i]; 428 nextTracks.push({ 429 id: `track-${i}-${Date.now()}`, 430 name: mt.name || `Track ${i + 1}`, 431 color: mt.channel === 9 ? "#e11d48" : `#${Math.floor((0.2 + 0.6 * Math.random()) * 16777215).toString(16)}`, 432 icon: mt.channel === 9 ? "drum" : "synth", 433 isMuted: false, 434 isSoloed: false, 435 volume: 0.8, 436 panning: 0, 437 height: 64, 438 parentId: null 439 }); 440 } 441 } else if (midi.tracks.length < nextTracks.length) { 442 nextTracks = nextTracks.slice(0, midi.tracks.length); 443 } 444 nextTracks = nextTracks.map((t, idx) => { 445 const mt = midi.tracks[idx]; 446 if (mt) { 447 return { 448 ...t, 449 name: mt.name || t.name, 450 channel: mt.channel, 451 instrumentNumber: mt.instrument.number 452 }; 453 } 454 return t; 455 }); 456 reconstructMidi(patternsRef.current, playlistClipsRef.current, nextTracks, automationClipsRef.current); 457 setPlaylistTracks(nextTracks); 458 } 459 engineRef.current.getNoteCount(); 460 await engineRef.current.syncSequencer(); 461 setDuration(engineRef.current.getDuration()); 462 setMidiVersion((v) => v + 1); 463 } 464 }, [reconstructMidi]); 465 const [meiDocs, setMeiDocs] = reactExports.useState({}); 466 const setMeiDoc = reactExports.useCallback((trackIdx, mei) => { 467 setMeiDocs((prev) => ({ ...prev, [trackIdx]: mei })); 468 }, []); 469 reactExports.useCallback((trackIdx) => { 470 setMeiDocs((prev) => { 471 if (!prev[trackIdx]) return prev; 472 const next = { ...prev }; 473 delete next[trackIdx]; 474 return next; 475 }); 476 }, []); 477 const createDefaultProject = reactExports.useCallback((engine) => { 478 var _a, _b; 479 const defaultPatId = `pat-1-${Date.now()}`; 480 const defaultTracks = []; 481 engine.midi.tracks.forEach((track, idx) => { 482 defaultTracks.push({ 483 id: `track-${idx}-${Date.now()}`, 484 name: track.name || `Track ${idx + 1}`, 485 color: track.channel === 9 ? "#e11d48" : "#6366f1", 486 icon: track.channel === 9 ? "drum" : "synth", 487 isMuted: false, 488 isSoloed: false, 489 volume: 0.8, 490 panning: 0, 491 height: 64, 492 parentId: null 493 }); 494 }); 495 if (defaultTracks.length === 0) { 496 defaultTracks.push({ 497 id: `track-0-${Date.now()}`, 498 name: "Track 1", 499 color: "#6366f1", 500 icon: "synth", 501 isMuted: false, 502 isSoloed: false, 503 volume: 0.8, 504 panning: 0, 505 height: 64, 506 parentId: null 507 }); 508 } 509 const defaultPatterns = [{ 510 id: defaultPatId, 511 name: "Pattern 1", 512 color: "#6366f1", 513 notes: [] 514 }]; 515 const ppq = ((_b = (_a = engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 516 const defaultClip = { 517 id: `clip-1-${Date.now()}`, 518 patternId: defaultPatId, 519 trackIndex: 0, 520 startTick: 0, 521 durationTicks: ppq * 16, 522 // Default 4 bars 523 offsetTicks: 0, 524 isMuted: false 525 }; 526 setPatterns(defaultPatterns); 527 setPlaylistTracks(defaultTracks); 528 setPlaylistClips([defaultClip]); 529 setAutomationClips([]); 530 setTimeMarkers([]);
531 setLoopRegion(null); 532 setActivePatternId(defaultPatId); 533 setTimeout(() => { 534 reconstructMidi(defaultPatterns, [defaultClip], defaultTracks, []); 535 engine.syncSequencer(); 536 }, 0); 537 }, [reconstructMidi]); 538 const migrateLegacyMidi = reactExports.useCallback((engine) => { 539 var _a; 540 const midi = engine.midi; 541 const newPats = []; 542 const newClips = []; 543 const newTracks = []; 544 midi.tracks.forEach((track, idx) => { 545 const trackId = `track-${idx}-${Date.now()}`; 546 const patId = `pat-${idx}-${Date.now()}`; 547 newTracks.push({ 548 id: trackId, 549 name: track.name || `Track ${idx + 1}`, 550 color: track.channel === 9 ? "#e11d48" : `#${Math.floor((0.2 + 0.6 * Math.random()) * 16777215).toString(16)}`, 551 icon: track.channel === 9 ? "drum" : "synth", 552 isMuted: false, 553 isSoloed: false, 554 volume: 0.8, 555 panning: 0, 556 height: 64, 557 parentId: null 558 }); 559 if (track.notes.length > 0) { 560 const patNotes = track.notes.map((n) => ({ 561 midi: n.midi, 562 ticks: n.ticks, 563 durationTicks: n.durationTicks, 564 velocity: n.velocity, 565 voice: n.voice || 1, 566 isMuted: n.isMuted || false 567 })); 568 newPats.push({ 569 id: patId, 570 name: `Pattern ${idx + 1}`, 571 color: newTracks[idx].color, 572 notes: patNotes 573 }); 574 const maxTick = patNotes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 575 const ppq = midi.header.ppq || 96; 576 const durationTicks = Math.max(maxTick, ppq * 16); 577 newClips.push({ 578 id: `clip-${idx}-${Date.now()}`, 579 patternId: patId, 580 trackIndex: idx, 581 startTick: 0, 582 durationTicks, 583 offsetTicks: 0, 584 isMuted: false 585 }); 586 } 587 }); 588 if (newPats.length === 0) { 589 const defaultPatId = `pat-1-${Date.now()}`; 590 newPats.push({ 591 id: defaultPatId, 592 name: "Pattern 1", 593 color: "#6366f1", 594 notes: [] 595 }); 596 } 597 setPatterns(newPats); 598 setPlaylistTracks(newTracks); 599 setPlaylistClips(newClips); 600 setAutomationClips([]); 601 setTimeMarkers([]); 602 setLoopRegion(null); 603 setActivePatternId(((_a = newPats[0]) == null ? void 0 : _a.id) || null); 604 setTimeout(() => { 605 reconstructMidi(newPats, newClips, newTracks, []); 606 engine.syncSequencer(); 607 }, 0); 608 }, [reconstructMidi]); 609 const convertToEasyModeStructure = reactExports.useCallback((currentPats, currentClips, currentTracks) => { 610 const pats = currentPats || patternsRef.current; 611 const clips = currentClips || playlistClipsRef.current; 612 const tracks = currentTracks || playlistTracksRef.current; 613 const newPatterns = []; 614 const newClips = []; 615 tracks.forEach((track, idx) => { 616 const patId = `pat-easy-${idx}-${Date.now()}`; 617 const trackNotes = []; 618 clips.forEach((clip) => { 619 if (clip.trackIndex !== idx) return; 620 const pattern = pats.find((p) => p.id === clip.patternId); 621 if (!pattern) return; 622 pattern.notes.forEach((note) => { 623 const absTick = clip.startTick + note.ticks - clip.offsetTicks; 624 if (note.ticks >= clip.offsetTicks && note.ticks < clip.offsetTicks + clip.durationTicks) { 625 trackNotes.push({ 626 ...note, 627 ticks: absTick 628 }); 629 } 630 }); 631 }); 632 newPatterns.push({ 633 id: patId, 634 name: `${track.name || `Track ${idx + 1}`}`, 635 color: track.color, 636 notes: trackNotes 637 }); 638 const maxNoteTick = trackNotes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 639 const ppq = 96; 640 const minDuration = ppq * 4 * 128; 641 const durationTicks = Math.max(maxNoteTick + ppq * 16, minDuration); 642 newClips.push({ 643 id: `clip-easy-${idx}-${Date.now()}`, 644 patternId: patId, 645 trackIndex: idx, 646 startTick: 0, 647 durationTicks, 648 offsetTicks: 0, 649 isMuted: false 650 }); 651 }); 652 setPatterns(newPatterns); 653 setPlaylistClips(newClips); 654 const currentActiveTrackIdx = activeTrackIndexRef.current; 655 if (currentActiveTrackIdx !== null && currentActiveTrackIdx >= 0 && currentActiveTrackIdx < tracks.length) {
656 setActivePatternId(newPatterns[currentActiveTrackIdx].id); 657 } else if (newPatterns.length > 0) { 658 setActivePatternId(newPatterns[0].id); 659 setActiveTrackIndex(0); 660 } 661 setTimeout(() => { 662 reconstructMidi(newPatterns, newClips, tracks, []); 663 if (engineRef.current) { 664 engineRef.current.syncSequencer(); 665 } 666 }, 0); 667 }, [reconstructMidi, setActiveTrackIndex]); 668 reactExports.useEffect(() => { 669 let isMounted = true; 670 let localEngine = null; 671 let localCtx = null; 672 const initEngine = async () => { 673 var _a; 674 const ctx = new (window.AudioContext || window.webkitAudioContext)(); 675 localCtx = ctx; 676 const dest = ctx.createGain(); 677 dest.connect(ctx.destination); 678 const engine = new MidiEngine(ctx, dest); 679 localEngine = engine; 680 try { 681 setLoadingStatus("Initializing DAW Engine..."); 682 await engine.initialize(); 683 if (!isMounted) { 684 engine.destroy(); 685 ctx.close().catch(() => { 686 }); 687 return; 688 } 689 engineRef.current = engine; 690 if (projectId) { 691 const isSavedProject = isNaN(Number(projectId)); 692 if (isSavedProject) { 693 setLoadingStatus("Loading saved MIDI project in progress..."); 694 const res = await fetch(`/api/projects/${projectId}`); 695 if (res.ok) { 696 const data = await res.json(); 697 if (data.projectData) { 698 if (data.projectData.patterns) { 699 setPatterns(data.projectData.patterns); 700 setPlaylistTracks(data.projectData.playlistTracks || []); 701 setPlaylistClips(data.projectData.playlistClips || []); 702 setAutomationClips(data.projectData.automationClips || []); 703 setTimeMarkers(data.projectData.timeMarkers || []); 704 setLoopRegion(data.projectData.loopRegion || null); 705 setEasyMode(!!data.projectData.easyMode); 706 if (data.projectData.gridSnap) setGridSnap(data.projectData.gridSnap);
707 setActivePatternId(data.projectData.activePatternId || (((_a = data.projectData.patterns[0]) == null ? void 0 : _a.id) || null)); 708 if (data.projectData.midi) { 709 const binaryString = atob(data.projectData.midi); 710 const len = binaryString.length; 711 const bytes = new Uint8Array(len); 712 for (let i = 0; i < len; i++) { 713 bytes[i] = binaryString.charCodeAt(i); 714 } 715 engine.loadMidi(bytes.buffer); 716 } 717 } else if (data.projectData.midi) { 718 const binaryString = atob(data.projectData.midi); 719 const len = binaryString.length; 720 const bytes = new Uint8Array(len); 721 for (let i = 0; i < len; i++) { 722 bytes[i] = binaryString.charCodeAt(i); 723 } 724 engine.loadMidi(bytes.buffer); 725 migrateLegacyMidi(engine); 726 } else { 727 engine.midi.fromJSON(data.projectData); 728 migrateLegacyMidi(engine); 729 } 730 } else { 731 engine.loadEmptyMidi(); 732 createDefaultProject(engine); 733 } 734 } else { 735 engine.loadEmptyMidi(); 736 createDefaultProject(engine); 737 } 738 } else { 739 setLoadingStatus("Loading MIDI file in progress..."); 740 const res = await fetch(`/api/tracks/${projectId}/midi`); 741 if (res.ok) { 742 const buffer = await res.arrayBuffer(); 743 engine.loadMidi(buffer); 744 migrateLegacyMidi(engine); 745 } else { 746 engine.loadEmptyMidi(); 747 createDefaultProject(engine); 748 } 749 } 750 } else { 751 setLoadingStatus("Creating empty project..."); 752 engine.loadEmptyMidi(); 753 createDefaultProject(engine); 754 } 755 if (engine.midi.tracks.length > 0) {
756 setActiveTrackIndex(0); 757 } else { 758 setActiveTrackIndex(null); 759 } 760 if (engine.midi.header.tempos && engine.midi.header.tempos.length > 0) { 761 setTempoState(engine.midi.header.tempos[0].bpm); 762 } 763 setDuration(engine.getDuration()); 764 setIsEngineReady(true); 765 setLoadingStatus(""); 766 setEngineError(null); 767 } catch (err) { 768 console.error("Failed to initialize MidiEngine", err); 769 setLoadingStatus(""); 770 setEngineError(err.message || "An unknown error occurred during engine initialization."); 771 } 772 }; 773 initEngine(); 774 return () => { 775 isMounted = false; 776 if (localEngine) { 777 localEngine.destroy(); 778 } 779 if (localCtx) { 780 localCtx.close().catch(() => { 781 }); 782 } 783 if (engineRef.current) { 784 engineRef.current = null; 785 } 786 }; 787 }, [projectId]); 788 const countdownTimeoutRef = reactExports.useRef(null); 789 const play = reactExports.useCallback(async () => { 790 var _a; 791 if (engineRef.current) { 792 if (((_a = engineRef.current.audioCtx) == null ? void 0 : _a.state) === "suspended") { 793 try { 794 await engineRef.current.audioCtx.resume(); 795 } catch (e) { 796 console.error("Failed to resume AudioContext:", e); 797 } 798 } 799 if (countdownTimeoutRef.current) { 800 clearTimeout(countdownTimeoutRef.current); 801 countdownTimeoutRef.current = null; 802 } 803 await forceSync(); 804 const hasNotes = engineRef.current.getNoteCount() > 0; 805 if (!hasNotes) { 806 playbackStartTimeRef.current = performance.now(); 807 playbackStartOffsetRef.current = currentTimeRef.current; 808 } 809 if (isRecording && isCountdownEnabled) { 810 let count = 4; 811 const beatDurationMs = 60 / tempoRef.current * 1e3; 812 const playCountdownClick = async () => { 813 var _a2; 814 if (count > 0) { 815 const isDownbeat = count === 4; 816 playMetronomeTick(isDownbeat); 817 count--; 818 countdownTimeoutRef.current = setTimeout(playCountdownClick, beatDurationMs); 819 } else { 820 if (!hasNotes) { 821 playbackStartTimeRef.current = performance.now(); 822 } 823 await ((_a2 = engineRef.current) == null ? void 0 : _a2.play()); 824 setIsPlaying(true); 825 } 826 }; 827 playCountdownClick(); 828 } else { 829 await engineRef.current.play(); 830 setIsPlaying(true); 831 } 832 } 833 }, [isRecording, isCountdownEnabled, playMetronomeTick, forceSync]); 834 const pause = reactExports.useCallback(() => { 835 if (countdownTimeoutRef.current) { 836 clearTimeout(countdownTimeoutRef.current); 837 countdownTimeoutRef.current = null; 838 } 839 if (engineRef.current) { 840 engineRef.current.pause(); 841 setIsPlaying(false); 842 } 843 }, []); 844 const stop = reactExports.useCallback(() => { 845 if (countdownTimeoutRef.current) { 846 clearTimeout(countdownTimeoutRef.current); 847 countdownTimeoutRef.current = null; 848 } 849 if (engineRef.current) { 850 engineRef.current.stop(); 851 engineRef.current.seek(0); 852 setIsPlaying(false); 853 setCurrentTime(0); 854 playbackStartOffsetRef.current = 0; 855 } 856 }, []); 857 const seek = reactExports.useCallback((time) => { 858 if (engineRef.current) { 859 engineRef.current.seek(time); 860 setCurrentTime(time); 861 playbackStartOffsetRef.current = time; 862 playbackStartTimeRef.current = performance.now(); 863 } 864 }, []); 865 const handleSetActiveTrackIndex = reactExports.useCallback((idx) => { 866 setActiveTrackIndex(idx); 867 if (playbackMode === "pat") { 868 setTimeout(() => forceSync(), 0); 869 } 870 }, [playbackMode, forceSync]); 871 const handleSetActivePatternId = reactExports.useCallback((id) => { 872 setActivePatternId(id); 873 if (playbackMode === "pat") { 874 setTimeout(() => forceSync(), 0); 875 } 876 }, [playbackMode, forceSync]); 877 const handleSetEasyMode = reactExports.useCallback((val) => { 878 if (val === easyMode) return; 879 if (val) { 880 const pats = patternsRef.current; 881 const clips = playlistClipsRef.current; 882 proModeBackupRef.current = { 883 patterns: pats, 884 playlistClips: clips 885 }; 886 const snapshot = []; 887 clips.forEach((clip) => { 888 const pattern = pats.find((p) => p.id === clip.patternId); 889 if (!pattern) return;
890 pattern.notes.forEach((note, noteIdx) => { 891 const absTick = clip.startTick + note.ticks - clip.offsetTicks; 892 if (note.ticks >= clip.offsetTicks && note.ticks < clip.offsetTicks + clip.durationTicks) { 893 snapshot.push({ 894 midi: note.midi, 895 absoluteTick: absTick, 896 durationTicks: note.durationTicks, 897 velocity: note.velocity, 898 voice: note.voice, 899 originalPatternId: pattern.id, 900 originalNoteIndex: noteIdx, 901 trackIndex: clip.trackIndex 902 }); 903 } 904 }); 905 }); 906 absoluteNotesSnapshotRef.current = snapshot; 907 setEasyMode_(true); 908 setPlaybackMode("song"); 909 setTimeout(() => { 910 convertToEasyModeStructure(pats, clips, playlistTracksRef.current); 911 }, 0); 912 } else { 913 if (!proModeBackupRef.current) { 914 setEasyMode_(false); 915 return; 916 } 917 const backupPats = JSON.parse(JSON.stringify(proModeBackupRef.current.patterns)); 918 const backupClips = JSON.parse(JSON.stringify(proModeBackupRef.current.playlistClips)); 919 const snapshot = absoluteNotesSnapshotRef.current; 920 const addedNotesPerTrack = {}; 921 let totalAdded = 0; 922 playlistTracksRef.current.forEach((track, idx) => { 923 const trackClip = playlistClipsRef.current.find((c) => c.trackIndex === idx); 924 if (!trackClip) return; 925 const trackEasyPattern = patternsRef.current.find((p) => p.id === trackClip.patternId); 926 if (!trackEasyPattern) return; 927 trackEasyPattern.notes.forEach((note) => { 928 const wasInSnapshot = snapshot.some( 929 (sn) => sn.trackIndex === idx && sn.midi === note.midi && sn.absoluteTick === note.ticks 930 ); 931 if (!wasInSnapshot) { 932 if (!addedNotesPerTrack[idx]) { 933 addedNotesPerTrack[idx] = []; 934 } 935 addedNotesPerTrack[idx].push({ 936 midi: note.midi, 937 absoluteTick: note.ticks, 938 durationTicks: note.durationTicks, 939 velocity: note.velocity, 940 voice: note.voice 941 }); 942 totalAdded++; 943 } 944 }); 945 }); 946 const applyTransition = (resolutions) => { 947 const nextPatterns = [...backupPats]; 948 const nextClips = [...backupClips]; 949 const nextTracks = [...playlistTracksRef.current]; 950 const deletedNoteInfos = snapshot.filter((backupNote) => { 951 const trackClip = playlistClipsRef.current.find((c) => c.trackIndex === backupNote.trackIndex); 952 if (!trackClip) return true; 953 const trackEasyPattern = patternsRef.current.find((p) => p.id === trackClip.patternId); 954 if (!trackEasyPattern) return true; 955 const exists = trackEasyPattern.notes.some((n) => n.midi === backupNote.midi && n.ticks === backupNote.absoluteTick); 956 return !exists; 957 }); 958 deletedNoteInfos.forEach((delInfo) => { 959 const pattern = nextPatterns.find((p) => p.id === delInfo.originalPatternId); 960 if (pattern) { 961 const clip = backupClips.find((c) => c.trackIndex === delInfo.trackIndex && c.patternId === delInfo.originalPatternId); 962 const clipStart = clip ? clip.startTick : 0; 963 const clipOffset = clip ? clip.offsetTicks : 0; 964 const relTick = delInfo.absoluteTick - clipStart + clipOffset; 965 const noteIdx = pattern.notes.findIndex((n) => n.midi === delInfo.midi && n.ticks === relTick); 966 if (noteIdx !== -1) { 967 pattern.notes.splice(noteIdx, 1); 968 } 969 } 970 }); 971 snapshot.forEach((backupNote) => { 972 const trackClip = playlistClipsRef.current.find((c) => c.trackIndex === backupNote.trackIndex); 973 if (!trackClip) return; 974 const trackEasyPattern = patternsRef.current.find((p) => p.id === trackClip.patternId); 975 if (!trackEasyPattern) return; 976 const matchedEasyNote = trackEasyPattern.notes.find((n) => n.midi === backupNote.midi && n.ticks === backupNote.absoluteTick); 977 if (matchedEasyNote) { 978 if (matchedEasyNote.durationTicks !== backupNote.durationTicks || matchedEasyNote.velocity !== backupNote.velocity) { 979 const pattern = nextPatterns.find((p) => p.id === backupNote.originalPatternId); 980 if (pattern) { 981 const clip = backupClips.find((c) => c.trackIndex === backupNote.trackIndex && c.patternId === backupNote.originalPatternId); 982 const clipStart = clip ? clip.startTick : 0; 983 const clipOffset = clip ? clip.offsetTicks : 0; 984 const relTick = backupNote.absoluteTick - clipStart + clipOffset; 985 const noteIdx = pattern.notes.findIndex((n) => n.midi === backupNote.midi && n.ticks === relTick); 986 if (noteIdx !== -1) { 987 pattern.notes[noteIdx].durationTicks = matchedEasyNote.durationTicks; 988 pattern.notes[noteIdx].velocity = matchedEasyNote.velocity; 989 } 990 } 991 } 992 } 993 });
994 Object.entries(resolutions).forEach(([trackIdxStr, res]) => { 995 var _a; 996 const trackIdx = parseInt(trackIdxStr, 10); 997 const added = addedNotesPerTrack[trackIdx]; 998 if (!added || added.length === 0) return; 999 if (res.action === "merge") { 1000 const targetPatId = res.targetPatternId; 1001 const pattern = nextPatterns.find((p) => p.id === targetPatId); 1002 if (pattern) { 1003 const clip = nextClips.find((c) => c.trackIndex === trackIdx && c.patternId === targetPatId); 1004 const clipStart = clip ? clip.startTick : 0; 1005 const clipOffset = clip ? clip.offsetTicks : 0; 1006 added.forEach((n) => { 1007 const relTick = n.absoluteTick - clipStart + clipOffset; 1008 pattern.notes.push({ 1009 midi: n.midi, 1010 ticks: relTick, 1011 durationTicks: n.durationTicks, 1012 velocity: n.velocity, 1013 voice: n.voice || 1 1014 }); 1015 }); 1016 } 1017 } else if (res.action === "new-track") { 1018 const nextTrackId = `track-easy-new-${trackIdx}-${Date.now()}`; 1019 const nextTrackName = `New Track (${((_a = nextTracks[trackIdx]) == null ? void 0 : _a.name) || `Track ${trackIdx + 1}`})`; 1020 const nextTrackColor = `#${Math.floor((0.3 + 0.6 * Math.random()) * 16777215).toString(16)}`; 1021 const newTrack = { 1022 id: nextTrackId, 1023 name: nextTrackName, 1024 color: nextTrackColor, 1025 icon: "synth", 1026 isMuted: false, 1027 isSoloed: false, 1028 volume: 0.8, 1029 panning: 0, 1030 height: 64, 1031 parentId: null 1032 }; 1033 nextTracks.push(newTrack); 1034 const newPatId = `pat-easy-new-${trackIdx}-${Date.now()}`; 1035 const newPat = { 1036 id: newPatId, 1037 name: nextTrackName, 1038 color: nextTrackColor, 1039 notes: added.map((n) => ({ 1040 midi: n.midi, 1041 ticks: n.absoluteTick, 1042 durationTicks: n.durationTicks, 1043 velocity: n.velocity, 1044 voice: n.voice || 1 1045 })) 1046 }; 1047 nextPatterns.push(newPat); 1048 const maxNoteTick = newPat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 1049 const durationTicks = Math.max(maxNoteTick + 96 * 16, 96 * 4 * 128); 1050 const newClip = { 1051 id: `clip-easy-new-${trackIdx}-${Date.now()}`, 1052 patternId: newPatId, 1053 trackIndex: nextTracks.length - 1, 1054 startTick: 0, 1055 durationTicks, 1056 offsetTicks: 0, 1057 isMuted: false 1058 }; 1059 nextClips.push(newClip); 1060 if (engineRef.current) { 1061 engineRef.current.addTrack(); 1062 } 1063 } 1064 }); 1065 setPatterns(nextPatterns); 1066 setPlaylistClips(nextClips); 1067 setPlaylistTracks(nextTracks); 1068 if (nextPatterns.length > 0) {
1069 setActivePatternId(nextPatterns[0].id); 1070 setActiveTrackIndex(0); 1071 } 1072 setEasyMode_(false); 1073 setEasyToProMergeState(null); 1074 proModeBackupRef.current = null; 1075 absoluteNotesSnapshotRef.current = []; 1076 setTimeout(() => { 1077 var _a; 1078 reconstructMidi(nextPatterns, nextClips, nextTracks, []); 1079 (_a = engineRef.current) == null ? void 0 : _a.syncSequencer(); 1080 }, 0); 1081 }; 1082 if (totalAdded > 0) { 1083 const mergeTracks = []; 1084 Object.entries(addedNotesPerTrack).forEach(([trackIdxStr, notes]) => { 1085 const trackIdx = parseInt(trackIdxStr, 10); 1086 const track = playlistTracksRef.current[trackIdx]; 1087 const availablePats = backupClips.filter((c) => c.trackIndex === trackIdx).map((c) => { 1088 const p = backupPats.find((pat) => pat.id === c.patternId); 1089 return { id: c.patternId, name: (p == null ? void 0 : p.name) || "Unnamed Pattern" }; 1090 }); 1091 const seen = /* @__PURE__ */ new Set(); 1092 const uniquePats = availablePats.filter((p) => { 1093 if (seen.has(p.id)) return false; 1094 seen.add(p.id); 1095 return true; 1096 }); 1097 mergeTracks.push({ 1098 trackIndex: trackIdx, 1099 trackName: (track == null ? void 0 : track.name) || `Track ${trackIdx + 1}`, 1100 addedNotes: notes, 1101 availablePatterns: uniquePats 1102 }); 1103 }); 1104 setEasyToProMergeState({ 1105 tracks: mergeTracks, 1106 resolve: (resolutions) => { 1107 applyTransition(resolutions); 1108 } 1109 }); 1110 } else { 1111 applyTransition({}); 1112 } 1113 } 1114 }, [easyMode, convertToEasyModeStructure, setPlaybackMode, setPatterns, setPlaylistClips, setPlaylistTracks, setActivePatternId, setActiveTrackIndex, reconstructMidi]); 1115 const setMasterVolume = reactExports.useCallback((vol) => { 1116 setMasterVolumeState(vol); 1117 if (engineRef.current) { 1118 engineRef.current.setMasterVolume(vol); 1119 } 1120 }, []); 1121 const setMasterPitch = reactExports.useCallback((pitch) => { 1122 setMasterPitchState(pitch); 1123 if (engineRef.current) { 1124 engineRef.current.setMasterPitch(pitch); 1125 } 1126 }, []); 1127 const setTempo = reactExports.useCallback((bpm) => { 1128 setTempoState(bpm); 1129 if (engineRef.current) { 1130 engineRef.current.addTempo(0, bpm); 1131 setMidiVersion((v) => v + 1); 1132 } 1133 }, []); 1134 const setTrackVolume = reactExports.useCallback((trackIdx, volume) => { 1135 setPlaylistTracks((prev) => prev.map((track, idx) => { 1136 if (idx === trackIdx) { 1137 return { ...track, volume }; 1138 } 1139 return track; 1140 })); 1141 if (engineRef.current) { 1142 engineRef.current.setTrackVolume(trackIdx, volume); 1143 } 1144 }, [setPlaylistTracks]); 1145 const setTrackPanning = reactExports.useCallback((trackIdx, panning) => { 1146 setPlaylistTracks((prev) => prev.map((track, idx) => { 1147 if (idx === trackIdx) { 1148 return { ...track, panning }; 1149 } 1150 return track; 1151 })); 1152 if (engineRef.current) { 1153 engineRef.current.setTrackPan(trackIdx, panning); 1154 } 1155 }, [setPlaylistTracks]); 1156 const toggleTrackMute = reactExports.useCallback((trackIdx) => { 1157 setPlaylistTracks((prev) => { 1158 const next = prev.map((track, idx) => { 1159 if (idx === trackIdx) { 1160 const isMuted = !track.isMuted; 1161 if (engineRef.current) { 1162 engineRef.current.setTrackMute(idx, isMuted); 1163 } 1164 return { ...track, isMuted }; 1165 } 1166 return track; 1167 }); 1168 return next; 1169 }); 1170 }, [setPlaylistTracks]); 1171 const toggleTrackSolo = reactExports.useCallback((trackIdx) => { 1172 setPlaylistTracks((prev) => { 1173 const targetTrack = prev[trackIdx]; 1174 if (!targetTrack) return prev; 1175 const isCurrentlySoloed = targetTrack.isSoloed; 1176 const willSolo = !isCurrentlySoloed; 1177 const next = prev.map((track, idx) => { 1178 if (willSolo) { 1179 const isMuted = idx !== trackIdx; 1180 const isSoloed = idx === trackIdx; 1181 if (engineRef.current) { 1182 engineRef.current.setTrackMute(idx, isMuted); 1183 } 1184 return { ...track, isMuted, isSoloed }; 1185 } else { 1186 if (engineRef.current) { 1187 engineRef.current.setTrackMute(idx, false); 1188 }
1189 return { ...track, isMuted: false, isSoloed: false }; 1190 } 1191 }); 1192 return next; 1193 }); 1194 }, [setPlaylistTracks]); 1195 const pushHistoryState = reactExports.useCallback(() => { 1196 if (!engineRef.current) return; 1197 const state = { 1198 patterns: JSON.parse(JSON.stringify(patterns)), 1199 playlistTracks: JSON.parse(JSON.stringify(playlistTracks)), 1200 playlistClips: JSON.parse(JSON.stringify(playlistClips)), 1201 automationClips: JSON.parse(JSON.stringify(automationClips)), 1202 timeMarkers: JSON.parse(JSON.stringify(timeMarkers)), 1203 loopRegion: loopRegion ? { ...loopRegion } : null, 1204 easyMode, 1205 activePatternId, 1206 meiDocs: { ...meiDocs } 1207 }; 1208 setUndoStack((prev) => { 1209 const next = [...prev, state]; 1210 if (next.length > 30) next.shift(); 1211 return next; 1212 }); 1213 setRedoStack([]); 1214 }, [patterns, playlistTracks, playlistClips, automationClips, timeMarkers, loopRegion, easyMode, activePatternId, meiDocs]); 1215 const undo = reactExports.useCallback(() => { 1216 if (undoStack.length === 0 || !engineRef.current) return; 1217 const currentState = { 1218 patterns: JSON.parse(JSON.stringify(patterns)), 1219 playlistTracks: JSON.parse(JSON.stringify(playlistTracks)), 1220 playlistClips: JSON.parse(JSON.stringify(playlistClips)), 1221 automationClips: JSON.parse(JSON.stringify(automationClips)), 1222 timeMarkers: JSON.parse(JSON.stringify(timeMarkers)), 1223 loopRegion: loopRegion ? { ...loopRegion } : null, 1224 easyMode, 1225 activePatternId, 1226 meiDocs: { ...meiDocs } 1227 }; 1228 setRedoStack((prev) => [...prev, currentState]); 1229 const prevState = undoStack[undoStack.length - 1]; 1230 setUndoStack((prev) => prev.slice(0, -1)); 1231 setPatterns(prevState.patterns); 1232 setPlaylistTracks(prevState.playlistTracks); 1233 setPlaylistClips(prevState.playlistClips); 1234 setAutomationClips(prevState.automationClips); 1235 setTimeMarkers(prevState.timeMarkers); 1236 setLoopRegion(prevState.loopRegion); 1237 setEasyMode(prevState.easyMode); 1238 setActivePatternId(prevState.activePatternId); 1239 setMeiDocs(prevState.meiDocs); 1240 setTimeout(() => { 1241 var _a; 1242 reconstructMidi(prevState.patterns, prevState.playlistClips, prevState.playlistTracks, prevState.automationClips); 1243 (_a = engineRef.current) == null ? void 0 : _a.syncSequencer(); 1244 setMidiVersion((v) => v + 1); 1245 }, 0); 1246 setSelectedNoteIds([]); 1247 }, [undoStack, patterns, playlistTracks, playlistClips, automationClips, timeMarkers, loopRegion, easyMode, activePatternId, meiDocs, reconstructMidi]); 1248 const redo = reactExports.useCallback(() => { 1249 if (redoStack.length === 0 || !engineRef.current) return; 1250 const currentState = { 1251 patterns: JSON.parse(JSON.stringify(patterns)), 1252 playlistTracks: JSON.parse(JSON.stringify(playlistTracks)), 1253 playlistClips: JSON.parse(JSON.stringify(playlistClips)), 1254 automationClips: JSON.parse(JSON.stringify(automationClips)), 1255 timeMarkers: JSON.parse(JSON.stringify(timeMarkers)), 1256 loopRegion: loopRegion ? { ...loopRegion } : null, 1257 easyMode, 1258 activePatternId, 1259 meiDocs: { ...meiDocs } 1260 }; 1261 setUndoStack((prev) => [...prev, currentState]); 1262 const nextState = redoStack[redoStack.length - 1]; 1263 setRedoStack((prev) => prev.slice(0, -1)); 1264 setPatterns(nextState.patterns); 1265 setPlaylistTracks(nextState.playlistTracks); 1266 setPlaylistClips(nextState.playlistClips); 1267 setAutomationClips(nextState.automationClips); 1268 setTimeMarkers(nextState.timeMarkers); 1269 setLoopRegion(nextState.loopRegion); 1270 setEasyMode(nextState.easyMode); 1271 setActivePatternId(nextState.activePatternId); 1272 setMeiDocs(nextState.meiDocs); 1273 setTimeout(() => { 1274 var _a; 1275 reconstructMidi(nextState.patterns, nextState.playlistClips, nextState.playlistTracks, nextState.automationClips); 1276 (_a = engineRef.current) == null ? void 0 : _a.syncSequencer(); 1277 setMidiVersion((v) => v + 1); 1278 }, 0); 1279 setSelectedNoteIds([]); 1280 }, [redoStack, patterns, playlistTracks, playlistClips, automationClips, timeMarkers, loopRegion, easyMode, activePatternId, meiDocs, reconstructMidi]); 1281 const copy = reactExports.useCallback(() => { 1282 if (!activePatternId || selectedNoteIds.length === 0) return; 1283 const pat = patterns.find((p) => p.id === activePatternId); 1284 if (!pat) return; 1285 const copiedNotes = selectedNoteIds.map((idx) => { 1286 const n = pat.notes[idx]; 1287 if (!n) return null; 1288 return { 1289 midi: n.midi, 1290 durationTicks: n.durationTicks, 1291 velocity: n.velocity, 1292 originalTicks: n.ticks, 1293 voice: n.voice 1294 }; 1295 }
1295).filter(Boolean); 1296 if (copiedNotes.length > 0) { 1297 const minTick = Math.min(...copiedNotes.map((n) => n.originalTicks)); 1298 const normalizedNotes = copiedNotes.map((n) => ({ 1299 ...n, 1300 offsetTicks: n.originalTicks - minTick 1301 })); 1302 setClipboard({ trackIdx: activeTrackIndex || 0, notes: normalizedNotes }); 1303 } 1304 }, [activePatternId, selectedNoteIds, patterns, activeTrackIndex]); 1305 const paste = reactExports.useCallback(() => { 1306 if (!activePatternId || !clipboard || clipboard.notes.length === 0 || !engineRef.current) return; 1307 pushHistoryState(); 1308 const playheadTicks = engineRef.current.secondsToTicks(currentTime); 1309 setPatterns((prev) => { 1310 const next = prev.map((p) => { 1311 if (p.id === activePatternId) { 1312 const notes = [...p.notes]; 1313 clipboard.notes.forEach((noteData) => { 1314 const pasteTicks = playheadTicks + noteData.offsetTicks; 1315 notes.push({ 1316 midi: noteData.midi, 1317 ticks: pasteTicks, 1318 durationTicks: noteData.durationTicks, 1319 velocity: noteData.velocity, 1320 voice: noteData.voice || 1 1321 }); 1322 }); 1323 return { ...p, notes }; 1324 } 1325 return p; 1326 }); 1327 setSelectedNoteIds([]); 1328 setTimeout(() => forceSync(), 0); 1329 return next; 1330 }); 1331 }, [activePatternId, clipboard, currentTime, forceSync, pushHistoryState]); 1332 const addNote = reactExports.useCallback((trackIdx, midiNote, ticks, durationTicks, velocity, source, voice = 1) => { 1333 pushHistoryState(); 1334 if (!activePatternId) return; 1335 if (easyMode) { 1336 setPlaylistClips((prevClips) => { 1337 return prevClips.map((clip) => { 1338 if (clip.patternId === activePatternId) { 1339 const noteEnd = ticks + durationTicks; 1340 if (noteEnd > clip.durationTicks) { 1341 return { 1342 ...clip, 1343 durationTicks: noteEnd + 96 * 16 1344 }; 1345 } 1346 } 1347 return clip; 1348 }); 1349 }); 1350 } 1351 setPatterns((prev) => { 1352 const next = prev.map((p) => { 1353 if (p.id === activePatternId) { 1354 return { 1355 ...p, 1356 notes: [ 1357 ...p.notes, 1358 { midi: midiNote, ticks, durationTicks, velocity, voice } 1359 ] 1360 }; 1361 } 1362 return p; 1363 }); 1364 setTimeout(() => forceSync(), 0); 1365 return next; 1366 }); 1367 }, [activePatternId, forceSync, pushHistoryState, easyMode]); 1368 const addNotes = reactExports.useCallback((trackIdx, notesToAdd, source) => { 1369 pushHistoryState(); 1370 if (!activePatternId) return; 1371 if (easyMode && notesToAdd.length > 0) { 1372 const maxNoteEnd = notesToAdd.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 1373 setPlaylistClips((prevClips) => { 1374 return prevClips.map((clip) => { 1375 if (clip.patternId === activePatternId) { 1376 if (maxNoteEnd > clip.durationTicks) { 1377 return { 1378 ...clip, 1379 durationTicks: maxNoteEnd + 96 * 16 1380 }; 1381 } 1382 } 1383 return clip; 1384 }); 1385 }); 1386 } 1387 setPatterns((prev) => { 1388 const next = prev.map((p) => { 1389 if (p.id === activePatternId) { 1390 const newNotes = notesToAdd.map((n) => ({ 1391 midi: n.midi, 1392 ticks: n.ticks, 1393 durationTicks: n.durationTicks, 1394 velocity: n.velocity, 1395 voice: n.voice || 1 1396 })); 1397 return { 1398 ...p, 1399 notes: [...p.notes, ...newNotes] 1400 }; 1401 } 1402 return p; 1403 }); 1404 setTimeout(() => forceSync(), 0); 1405 return next; 1406 }); 1407 }, [activePatternId, forceSync, pushHistoryState, easyMode]); 1408 const updateNote = reactExports.useCallback((trackIdx, noteIndex, newProps, source) => { 1409 pushHistoryState(); 1410 if (!activePatternId) return; 1411 if (easyMode) { 1412 const currentPattern = patternsRef.current.find((p) => p.id === activePatternId); 1413 const note = currentPattern == null ? void 0 : currentPattern.notes[noteIndex]; 1414 if (note) { 1415 const finalTicks = newProps.ticks !== void 0 ? newProps.ticks : note.ticks; 1416 const finalDuration = newProps.durationTicks !== void 0 ? newProps.durationTicks : note.durationTicks;
1417 const noteEnd = finalTicks + finalDuration; 1418 setPlaylistClips((prevClips) => { 1419 return prevClips.map((clip) => { 1420 if (clip.patternId === activePatternId) { 1421 if (noteEnd > clip.durationTicks) { 1422 return { 1423 ...clip, 1424 durationTicks: noteEnd + 96 * 16 1425 }; 1426 } 1427 } 1428 return clip; 1429 }); 1430 }); 1431 } 1432 } 1433 setPatterns((prev) => { 1434 const next = prev.map((p) => { 1435 if (p.id === activePatternId) { 1436 const notes = [...p.notes]; 1437 if (notes[noteIndex]) { 1438 notes[noteIndex] = { 1439 ...notes[noteIndex], 1440 ...newProps 1441 }; 1442 } 1443 return { ...p, notes }; 1444 } 1445 return p; 1446 }); 1447 setTimeout(() => forceSync(), 0); 1448 return next; 1449 }); 1450 }, [activePatternId, forceSync, pushHistoryState, easyMode]); 1451 const updateNotes = reactExports.useCallback((trackIdx, updates, source) => { 1452 pushHistoryState(); 1453 if (!activePatternId || updates.length === 0) return; 1454 if (easyMode) { 1455 const currentPattern = patternsRef.current.find((p) => p.id === activePatternId); 1456 if (currentPattern) { 1457 let maxNoteEnd = 0; 1458 updates.forEach(({ noteIndex, newProps }) => { 1459 const note = currentPattern.notes[noteIndex]; 1460 if (note) { 1461 const finalTicks = newProps.ticks !== void 0 ? newProps.ticks : note.ticks; 1462 const finalDuration = newProps.durationTicks !== void 0 ? newProps.durationTicks : note.durationTicks; 1463 const noteEnd = finalTicks + finalDuration; 1464 if (noteEnd > maxNoteEnd) { 1465 maxNoteEnd = noteEnd; 1466 } 1467 } 1468 }); 1469 if (maxNoteEnd > 0) { 1470 setPlaylistClips((prevClips) => { 1471 return prevClips.map((clip) => { 1472 if (clip.patternId === activePatternId) { 1473 if (maxNoteEnd > clip.durationTicks) { 1474 return { 1475 ...clip, 1476 durationTicks: maxNoteEnd + 96 * 16 1477 }; 1478 } 1479 } 1480 return clip; 1481 }); 1482 }); 1483 } 1484 } 1485 } 1486 setPatterns((prev) => { 1487 const next = prev.map((p) => { 1488 if (p.id === activePatternId) { 1489 const notes = [...p.notes]; 1490 updates.forEach(({ noteIndex, newProps }) => { 1491 if (notes[noteIndex]) { 1492 notes[noteIndex] = { 1493 ...notes[noteIndex], 1494 ...newProps 1495 }; 1496 } 1497 }); 1498 return { ...p, notes }; 1499 } 1500 return p; 1501 }); 1502 setTimeout(() => forceSync(), 0); 1503 return next; 1504 }); 1505 }, [activePatternId, forceSync, pushHistoryState, easyMode]); 1506 const deleteNote = reactExports.useCallback((trackIdx, noteIndex, source) => { 1507 pushHistoryState(); 1508 if (!activePatternId) return; 1509 setPatterns((prev) => { 1510 const next = prev.map((p) => { 1511 if (p.id === activePatternId) { 1512 const notes = [...p.notes]; 1513 notes.splice(noteIndex, 1); 1514 return { ...p, notes }; 1515 } 1516 return p; 1517 }); 1518 setTimeout(() => forceSync(), 0); 1519 return next; 1520 }); 1521 }, [activePatternId, forceSync, pushHistoryState]); 1522 const splitNote = reactExports.useCallback((trackIdx, noteIndex, splitTick, source) => { 1523 pushHistoryState(); 1524 if (!activePatternId) return; 1525 setPatterns((prev) => { 1526 const next = prev.map((p) => { 1527 if (p.id === activePatternId) { 1528 const notes = [...p.notes]; 1529 const note = notes[noteIndex]; 1530 if (note && splitTick > note.ticks && splitTick < note.ticks + note.durationTicks) { 1531 const firstDuration = splitTick - note.ticks; 1532 const secondDuration = note.durationTicks - firstDuration; 1533 const secondTick = splitTick; 1534 notes[noteIndex] = { 1535 ...note, 1536 durationTicks: firstDuration 1537 }; 1538 notes.push({ 1539 midi: note.midi, 1540 ticks: secondTick, 1541 durationTicks: secondDuration, 1542 velocity: note.velocity, 1543 voice: note.voice, 1544 isMuted: note.isMuted 1545 }); 1546 } 1547 return { ...p, notes }; 1548 } 1549 return p; 1550 }); 1551 setTimeout(() => forceSync(), 0); 1552 return next; 1553 }); 1554 }, [activePatternId, forceSync, pushHistoryState]); 1555 const deleteSelection = reactExports.useCallback(() => { 1556 if (!activePatternId || selectedNoteIds.length === 0) return; 1557 pushHistoryState(); 1558 setPatterns((prev) => { 1559 const next = prev.map((p) => { 1560 if (p.id === activePatternId) { 1561 const sortedIds = [...selectedNoteIds].sort((a, b) => b - a); 1562 const notes = [...p.notes]; 1563 for (const idx of sortedIds) { 1564 if (idx >= 0 && idx < notes.length) { 1565 notes.splice(idx, 1); 1566 } 1567 } 1568 return { ...p, notes }; 1569 } 1570 return p; 1571 }); 1572 setSelectedNoteIds([]); 1573 setTimeout(() => forceSync(), 0); 1574 return next; 1575 }); 1576 }, [activePatternId, selectedNoteIds, forceSync, pushHistoryState]); 1577 const activeRecordingNotesRef = reactExports.useRef({}); 1578 const commitRecordedNote = reactExports.useCallback((note, startTick, endTick, velocity) => { 1579 var _a; 1580 if (!activePatternId || !isRecording || !isPlaying || !engineRef.current) return; 1581 const durationTicks = Math.max(1, endTick - startTick); 1582 const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0; 1583 addNote(activeTrackIndex || 0, note, startTick, durationTicks, velocity, "pianoroll", channel); 1584 }, [activePatternId, isRecording, isPlaying, activeTrackIndex, playlistTracks, addNote]); 1585 const handleNoteOn = reactExports.useCallback((note, velocity = 100) => { 1586 var _a; 1587 if (!engineRef.current || !isEngineReady) return; 1588 const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0; 1589 engineRef.current.startNotePreview(note, channel); 1590 setActiveMidiKeys((prev) => { 1591 const next = new Set(prev); 1592 next.add(note); 1593 return next; 1594 }); 1595 if (isRecording && isPlaying) { 1596 const currentTick = Math.floor(engineRef.current.secondsToTicks(engineRef.current.getCurrentTime())); 1597 activeRecordingNotesRef.current[note] = currentTick; 1598 } 1599 }, [isRecording, isPlaying, activeTrackIndex, playlistTracks, isEngineReady]); 1600 const handleNoteOff = reactExports.useCallback((note) => { 1601 var _a; 1602 if (!engineRef.current || !isEngineReady) return; 1603 const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0; 1604 engineRef.current.stopNotePreview(note, channel); 1605 setActiveMidiKeys((prev) => { 1606 const next = new Set(prev); 1607 next.delete(note); 1608 return next; 1609 }); 1610 if (isRecording && isPlaying && activeRecordingNotesRef.current[note] !== void 0) { 1611 const endTick = Math.floor(engineRef.current.secondsToTicks(engineRef.current.getCurrentTime())); 1612 const startTick = activeRecordingNotesRef.current[note]; 1613 delete activeRecordingNotesRef.current[note]; 1614 commitRecordedNote(note, startTick, endTick, 100); 1615 } 1616 }, [isRecording, isPlaying, activeTrackIndex, playlistTracks, commitRecordedNote, isEngineReady]); 1617 const { 1618 isMidiSupported, 1619 isMidiConnected, 1620 connectMidi, 1621 error: midiError 1622 } = useMidiKeyboard({ 1623 playNote: handleNoteOn, 1624 stopNote: handleNoteOff, 1625 enabled: isEngineReady 1626 }); 1627 reactExports.useEffect(() => { 1628 if (!isTypingKeyboardEnabled || !isEngineReady || !engineRef.current) return;
1629 const keyMap = { 1630 "a": 60, 1631 // C4 1632 "w": 61, 1633 // C#4 1634 "s": 62, 1635 // D4 1636 "e": 63, 1637 // D#4 1638 "d": 64, 1639 // E4 1640 "f": 65, 1641 // F4 1642 "t": 66, 1643 // F#4 1644 "g": 67, 1645 // G4 1646 "y": 68, 1647 // G#4 1648 "h": 69, 1649 // A4 1650 "u": 70, 1651 // A#4 1652 "j": 71, 1653 // B4 1654 "k": 72, 1655 // C5 1656 "o": 73, 1657 // C#5 1658 "l": 74, 1659 // D5 1660 "p": 75, 1661 // D#5 1662 ";": 76, 1663 // E5 1664 "'": 77 1665 // F5 1666 }; 1667 const activeNotes = /* @__PURE__ */ new Set(); 1668 const handleKeyDown = (e) => { 1669 const activeEl = document.activeElement; 1670 if (activeEl) { 1671 const tag = activeEl.tagName.toLowerCase(); 1672 if (tag === "input" || tag === "textarea" || activeEl.hasAttribute("contenteditable")) { 1673 return; 1674 } 1675 } 1676 const key = e.key.toLowerCase(); 1677 if (keyMap[key] !== void 0) { 1678 e.preventDefault(); 1679 const note = keyMap[key]; 1680 if (!activeNotes.has(note)) { 1681 activeNotes.add(note); 1682 handleNoteOn(note); 1683 } 1684 } 1685 }; 1686 const handleKeyUp = (e) => { 1687 const key = e.key.toLowerCase(); 1688 if (keyMap[key] !== void 0) { 1689 const note = keyMap[key]; 1690 if (activeNotes.has(note)) { 1691 activeNotes.delete(note); 1692 handleNoteOff(note); 1693 } 1694 } 1695 }; 1696 window.addEventListener("keydown", handleKeyDown); 1697 window.addEventListener("keyup", handleKeyUp); 1698 return () => { 1699 window.removeEventListener("keydown", handleKeyDown); 1700 window.removeEventListener("keyup", handleKeyUp); 1701 activeNotes.forEach((note) => { 1702 var _a, _b; 1703 const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0; 1704 (_b = engineRef.current) == null ? void 0 : _b.stopNotePreview(note, channel); 1705 }); 1706 }; 1707 }, [isTypingKeyboardEnabled, isEngineReady, activeTrackIndex, playlistTracks, handleNoteOn, handleNoteOff]); 1708 const loadMidiData = reactExports.useCallback((buffer) => { 1709 if (engineRef.current) { 1710 engineRef.current.loadMidi(buffer); 1711 migrateLegacyMidi(engineRef.current); 1712 if (engineRef.current.midi.header.tempos && engineRef.current.midi.header.tempos.length > 0) { 1713 setTempoState(engineRef.current.midi.header.tempos[0].bpm); 1714 } 1715 } 1716 }, [migrateLegacyMidi]); 1717 const loadProjectData = reactExports.useCallback((projectData) => { 1718 var _a, _b, _c; 1719 if (!projectData) return; 1720 pushHistoryState(); 1721 if (projectData.patterns) { 1722 setPatterns(projectData.patterns); 1723 setPlaylistTracks(projectData.playlistTracks || []); 1724 setPlaylistClips(projectData.playlistClips || []); 1725 setAutomationClips(projectData.automationClips || []); 1726 setTimeMarkers(projectData.timeMarkers || []); 1727 setLoopRegion(projectData.loopRegion || null); 1728 setEasyMode_(!!projectData.easyMode); 1729 if (projectData.gridSnap) setGridSnap(projectData.gridSnap); 1730 setActivePatternId(projectData.activePatternId || (((_a = projectData.patterns[0]) == null ? void 0 : _a.id) || null)); 1731 if (projectData.meiDocs) { 1732 setMeiDocs(projectData.meiDocs); 1733 } else { 1734 setMeiDocs({}); 1735 } 1736 if (projectData.midi) { 1737 const binaryString = atob(projectData.midi); 1738 const len = binaryString.length; 1739 const bytes = new Uint8Array(len); 1740 for (let i = 0; i < len; i++) { 1741 bytes[i] = binaryString.charCodeAt(i); 1742 } 1743 if (engineRef.current) { 1744 engineRef.current.loadMidi(bytes.buffer); 1745 if (projectData.title) { 1746 engineRef.current.midi.name = projectData.title; 1747 } 1748 if (((_c = (_b = engineRef.current.midi) == null ? void 0 : _b.header) == null ? void 0 : _c.tempos) && engineRef.current.midi.header.tempos.length > 0) { 1749 setTempoState(engineRef.current.midi.header.tempos[0].bpm); 1750 } 1751 } 1752 } 1753 setMidiVersion((v) => v + 1); 1754 } 1755 }, [setPatterns, setPlaylistTracks, setPlaylistClips, setAutomationClips, setTimeMarkers, setLoopRegion, setGridSnap, setA
1755ctivePatternId, pushHistoryState]); 1756 const makePatternUnique = reactExports.useCallback((clipId) => { 1757 pushHistoryState(); 1758 setPlaylistClips((prevClips) => { 1759 const targetClipIndex = prevClips.findIndex((c) => c.id === clipId); 1760 if (targetClipIndex === -1) return prevClips; 1761 const targetClip = prevClips[targetClipIndex]; 1762 const originalPat = patterns.find((p) => p.id === targetClip.patternId); 1763 if (!originalPat) return prevClips; 1764 const newPatId = `pat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 1765 const newPat = { 1766 ...originalPat, 1767 id: newPatId, 1768 name: `${originalPat.name} (Unique)`, 1769 notes: JSON.parse(JSON.stringify(originalPat.notes)) 1770 }; 1771 setPatterns((prevPats) => [...prevPats, newPat]); 1772 const updatedClips = [...prevClips]; 1773 updatedClips[targetClipIndex] = { 1774 ...targetClip, 1775 patternId: newPatId 1776 }; 1777 setActivePatternId(newPatId); 1778 setTimeout(() => forceSync(), 0); 1779 return updatedClips; 1780 }); 1781 }, [patterns, forceSync, pushHistoryState]); 1782 const value = { 1783 engine: engineRef.current, 1784 isEngineReady, 1785 midiVersion, 1786 currentTime, 1787 isPlaying, 1788 duration, 1789 activeTrackIndex, 1790 selectedNoteIds, 1791 selectedTempoIndex, 1792 selectedTimeSigIndex, 1793 play, 1794 pause, 1795 stop, 1796 seek, 1797 setActiveTrackIndex: handleSetActiveTrackIndex, 1798 setSelectedNoteIds: handleSetSelectedNoteIds, 1799 setSelectedTempoIndex: handleSetSelectedTempoIndex, 1800 setSelectedTimeSigIndex: handleSetSelectedTimeSigIndex, 1801 forceSync, 1802 addNote, 1803 addNotes, 1804 updateNote, 1805 updateNotes, 1806 deleteNote, 1807 splitNote, 1808 deleteSelection, 1809 engineError, 1810 loadingStatus, 1811 loadMidiData, 1812 loadProjectData, 1813 meiDocs, 1814 setMeiDoc, 1815 canUndo: undoStack.length > 0, 1816 canRedo: redoStack.length > 0, 1817 undo, 1818 redo, 1819 pushHistoryState, 1820 clipboard, 1821 copy, 1822 paste, 1823 hint, 1824 setHint, 1825 // Pattern DAW integrations 1826 patterns, 1827 setPatterns, 1828 activePatternId, 1829 setActivePatternId: handleSetActivePatternId, 1830 playlistTracks, 1831 setPlaylistTracks, 1832 playlistClips, 1833 setPlaylistClips, 1834 selectedClipIds, 1835 setSelectedClipIds, 1836 automationClips, 1837 setAutomationClips, 1838 timeMarkers, 1839 setTimeMarkers, 1840 loopRegion, 1841 setLoopRegion, 1842 loopMarkerTick, 1843 setLoopMarkerTick, 1844 gridSnap, 1845 setGridSnap, 1846 currentPlaylistTool, 1847 setCurrentPlaylistTool, 1848 easyMode, 1849 setEasyMode: handleSetEasyMode, 1850 easyToProMergeState, 1851 setEasyToProMergeState, 1852 makePatternUnique, 1853 activePattern, 1854 // FL Studio Transport additionals 1855 playbackMode, 1856 setPlaybackMode, 1857 isRecording, 1858 setIsRecording, 1859 isTypingKeyboardEnabled, 1860 setIsTypingKeyboardEnabled, 1861 activeMidiKeys, 1862 isMidiSupported, 1863 isMidiConnected, 1864 connectMidi, 1865 midiError, 1866 isCountdownEnabled, 1867 setIsCountdownEnabled, 1868 isMetronomeEnabled, 1869 setIsMetronomeEnabled, 1870 isLoopRecordingEnabled, 1871 setIsLoopRecordingEnabled, 1872 masterVolume, 1873 setMasterVolume, 1874 masterPitch, 1875 setMasterPitch, 1876 tempo, 1877 setTempo, 1878 setTrackVolume, 1879 setTrackPanning, 1880 toggleTrackMute, 1881 toggleTrackSolo 1882 }; 1883 return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorContext.Provider, { value, children }); 1884}; 1885const DRUM_MAP = { 1886 // Kicks 1887 35: { pname: "f", oct: 4 }, 1888 // Acoustic Bass Drum 1889 36: { pname: "f", oct: 4 }, 1890 // Bass Drum 1 1891 // Snares 1892 38: { pname: "c", oct: 5 }, 1893 // Acoustic Snare 1894 40: { pname: "c", oct: 5 }, 1895 // Electric Snare 1896 37: { pname: "c", oct: 5, shape: "x" }, 1897 // Side Stick 1898 // Toms 1899 41: { pname: "e", oct: 4 }, 1900 // Low Floor Tom 1901 43: { pname: "g", oct: 4 }, 1902 // High Floor Tom 1903 45: { pname: "a", oct: 4 }, 1904 // Low Tom 1905 47: { pname: "c", oct: 5 }, 1906 // Low-Mid Tom (same line as snare usually, or 'b' 4) 1907 48: { pname: "d", oct: 5 }, 1908 // Hi-Mid Tom 1909 50: { pname: "e", oct: 5 }, 1910 // High Tom 1911 // Hi-Hats 1912 42: { pname: "g", oct: 5, shape: "x" }, 1913 // Closed Hi Hat 1914 44: { pname: "g", oct: 5, shape: "x" }, 1915 // Pedal Hi-Hat 1916 46: { pname: "g", oct: 5, shape: "diamond" }, 1917 // Open Hi-Hat 1918 // Cymbals 1919 49: { pname: "a", oct: 5, shape: "x" }, 1920 // Crash Cymbal 1 1921 51: { pname: "f", oct: 5, shape: "x" }, 1922 // Ride Cymbal 1 1923 52: { pname: "a", oct: 5, shape: "x" }, 1924 // Chinese Cymbal 1925 53: { pname: "f", oct: 5, shape: "diamond" }, 1926 // Ride Bell 1927 55: { pname: "a", oct: 5, shape: "x" }, 1928 // Splash Cymbal 1929 57: { pname: "a", oct: 5, shape: "x" }, 1930 // Crash Cymbal 2 1931 59: { pname: "f", oct: 5, shape: "x" }, 1932 // Ride Cymbal 2 1933 // Extras 1934 56: { pname: "g", oct: 4, shape: "triangle" }, 1935 // Cowbell 1936 81: { pname: "g", oct: 4, shape: "triangle" }, 1937 // Open Triangle 1938 80: { pname: "g", oct: 4, shape: "triangle" } 1939 // Mute Triangle 1940}; 1941const REVERSE_DRUM_MAP = Object.entries(DRUM_MAP).reduce((acc, [midi, props]) => { 1942 const key = `${props.pname}${props.oct}`; 1943 if (!acc[key]) acc[key] = parseInt(midi); 1944 return acc; 1945}, {}); 1946const createEmptyMEI = (title = "Title", isPercussion = false, isGrandStaff = false) => { 1947 let measures = ""; 1948 for (let i = 1; i <= 20; i++) { 1949 const systemBreak = i > 1 && (i - 1) % 4 === 0 ? "<sb />" : ""; 1950 let staves = ""; 1951 if (isGrandStaff) { 1952 staves = ` 1953 <staff n="1" xml:id="s-${i}-1"> 1954 <layer n="1" xml:id="l-${i}-1-1"> 1955 <mRest xml:id="mr-${i}-1" /> 1956 </layer> 1957 </staff> 1958 <staff n="2" xml:id="s-${i}-2"> 1959 <layer n="1" xml:id="l-${i}-2-1"> 1960 <mRest xml:id="mr-${i}-2" /> 1961 </layer> 1962 </staff>`; 1963 } else { 1964 staves = ` 1965 <staff n="1" xml:id="s-${i}"> 1966 <layer n="1" xml:id="l-${i}"> 1967 <mRest xml:id="mr-${i}" /> 1968 </layer> 1969 </staff>`; 1970 } 1971 measures += ` 1972 ${systemBreak} 1973 <measure n="${i}" xml:id="m-${i}">${staves} 1974 </measure>`; 1975 } 1976 let staffGrp = ""; 1977 if (isGrandStaff) { 1978 staffGrp = ` 1979 <staffGrp symbol="brace" bar.thru="true"> 1980 <staffDef n="1" lines="5" clef.shape="G" clef.line="2" meter.count="4" meter.unit="4" key.sig="0" /> 1981 <staffDef n="2" lines="5" clef.shape="F" clef.line="4" meter.count="4" meter.unit="4" key.sig="0" /> 1982 </staffGrp>`; 1983 } else { 1984 staffGrp = `<staffDef n="1" lines="5" clef.shape="${isPercussion ? "perc" : "G"}" clef.line="${isPercussion ? "3" : "2"}" meter.count="4" meter.unit="4" key.sig="0" label=" " />`; 1985 } 1986 return `<?xml version="1.0" encoding="UTF-8"?> 1987<?xml-model href="https://music-encoding.org/schema/dev/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> 1988<?xml-model href="https://music-encoding.org/schema/dev/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> 1989<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0"> 1990 <meiHead> 1991 <fileDesc> 1992 <titleStmt> 1993 <title>${title}</title> 1994 <title type="subtitle">Subtitle</title> 1995 <respStmt> 1996 <persName role="composer">Composer</persName> 1997 <persName role="lyricist">Lyricist</persName> 1998 </respStmt> 1999 </titleStmt> 2000 <pubStmt/> 2001 </fileDesc> 2002 </meiHead> 2003 <music> 2004 <body> 2005 <mdiv> 2006 <score> 2007 <scoreDef> 2008 <staffGrp> 2009${staffGrp} 2010 </staffGrp> 2011 </scoreDef> 2012 <section>${measures} 2013 </section> 2014 </score> 2015 </mdiv> 2016 </body> 2017 </music> 2018</mei>`; 2019}; 2020const getDurationInBeats$1 = (verovioDur) => { 2021 switch (verovioDur) { 2022 case "1": 2023 return 4; 2024 case "2": 2025 return 2; 2026 case "4": 2027 return 1; 2028 case "8": 2029 return 0.5; 2030 case "16": 2031 return 0.25; 2032 case "32": 2033 return 0.125; 2034 case "64": 2035 return 0.0625; 2036 default: 2037 return 1; 2038 } 2039}; 2040const getMidiFromMEI$1 = (pname, oct, accid) => { 2041 const basePitches = { 2042 "c": 0, 2043 "d": 2, 2044 "e": 4, 2045 "f": 5, 2046 "g": 7, 2047 "a": 9, 2048 "b": 11 2049 }; 2050 let midi = (oct + 1) * 12 + basePitches[pname.toLowerCase()]; 2051 if (accid === "s") midi += 1; 2052 if (accid === "f") midi -= 1; 2053 if (accid === "ss" || accid === "xs") midi += 2; 2054 if (accid === "ff" || accid === "df") midi -= 2; 2055 return midi; 2056}; 2057const getMEIFromMidi = (midiNote) => { 2058 const noteNames2 = ["c", "c", "d", "d", "e", "f", "f", "g", "g", "a", "a", "b"]; 2059 const accids = ["", "s", "", "s", "", "", "s", "", "s", "", "s", ""]; 2060 const pitchClass = midiNote % 12; 2061 const oct = Math.floor(midiNote / 12) - 1; 2062 return { 2063 pname: noteNames2[pitchClass], 2064 oct, 2065 accid: accids[pitchClass] || void 0 2066 }; 2067}; 2068const getBeats$1 = (dur, dots, num, numbase) => { 2069 let base = 1; 2070 switch (dur) { 2071 case "1": 2072 base = 4; 2073 break; 2074 case "2": 2075 base = 2; 2076 break; 2077 case "4": 2078 base = 1; 2079 break; 2080 case "8": 2081 base = 0.5; 2082 break; 2083 case "16": 2084 base = 0.25; 2085 break; 2086 case "32": 2087 base = 0.125; 2088 break; 2089 case "64": 2090 base = 0.0625; 2091 break; 2092 } 2093 if (dots) { 2094 const numDots = parseInt(dots); 2095 if (numDots === 1) base *= 1.5; 2096 else if (numDots === 2) base *= 1.75; 2097 } 2098 if (num && numbase) { 2099 const n = parseInt(num); 2100 const nb = parseInt(numbase); 2101 if (n > 0 && nb > 0) { 2102 base = base * nb / n; 2103 } 2104 } 2105 return base; 2106}; 2107const generateRestsForDuration = (doc, beats) => { 2108 const rests = []; 2109 const ns = "http://www.music-encoding.org/ns/mei"; 2110 let remaining = beats; 2111 const restTypes = [ 2112 { beats: 4, dur: "1", dots: 0 }, 2113 { beats: 3, dur: "2", dots: 1 }, 2114 { beats: 2, dur: "2", dots: 0 }, 2115 { beats: 1.5, dur: "4", dots: 1 }, 2116 { beats: 1, dur: "4", dots: 0 }, 2117 { beats: 0.75, dur: "8", dots: 1 }, 2118 { beats: 0.5, dur: "8", dots: 0 }, 2119 { beats: 0.375, dur: "16", dots: 1 }, 2120 { beats: 0.25, dur: "16", dots: 0 }, 2121 { beats: 0.125, dur: "32", dots: 0 }, 2122 { beats: 0.0625, dur: "64", dots: 0 } 2123 ]; 2124 while (remaining > 1e-3) { 2125 const match = restTypes.find((r) => r.beats <= remaining + 1e-3); 2126 if (!match) break; 2127 const restEl = doc.createElementNS(ns, "rest"); 2128 restEl.setAttribute("xml:id", "r-" + Math.random().toString(36).substring(2, 9)); 2129 restEl.setAttribute("dur", match.dur); 2130 if (match.dots > 0) { 2131 restEl.setAttribute("dots", match.dots.toString()); 2132 } 2133 rests.push(restEl); 2134 remaining -= match.beats; 2135 } 2136 return rests; 2137}; 2138const consolidateMeiRests = (doc) => { 2139 const layers = Array.from(doc.querySelectorAll("layer")); 2140 const ns = "http://www.music-encoding.org/ns/mei";
2141 layers.forEach((layer) => { 2142 const noteEvents = []; 2143 let currentBeat = 0; 2144 const children = Array.from(layer.children); 2145 children.forEach((child) => { 2146 const tmpStart = child.getAttribute("tmp-start-beat"); 2147 const startBeat = tmpStart ? parseFloat(tmpStart) : currentBeat; 2148 child.removeAttribute("tmp-start-beat"); 2149 if (child.tagName === "note") { 2150 const durAttr = child.getAttribute("dur") || "4"; 2151 const dotsAttr = child.getAttribute("dots") || null; 2152 const duration = getBeats$1(durAttr, dotsAttr); 2153 console.log("EDITOR_LOG: parsed child note:", child.getAttribute("xml:id"), "durAttr =", durAttr, "duration =", duration); 2154 noteEvents.push({ 2155 element: child, 2156 pname: child.getAttribute("pname") || "c", 2157 oct: child.getAttribute("oct") || "4", 2158 accid: child.getAttribute("accid") || void 0, 2159 startBeat, 2160 duration, 2161 tie: child.getAttribute("tie") || void 0 2162 }); 2163 if (!tmpStart) { 2164 currentBeat += duration; 2165 } 2166 } else if (child.tagName === "chord") { 2167 const durAttr = child.getAttribute("dur") || "4"; 2168 const dotsAttr = child.getAttribute("dots") || null; 2169 const duration = getBeats$1(durAttr, dotsAttr); 2170 const notes = Array.from(child.querySelectorAll("note")); 2171 notes.forEach((n) => { 2172 noteEvents.push({ 2173 element: n, 2174 pname: n.getAttribute("pname") || "c", 2175 oct: n.getAttribute("oct") || "4", 2176 accid: n.getAttribute("accid") || void 0, 2177 startBeat, 2178 duration, 2179 tie: n.getAttribute("tie") || child.getAttribute("tie") || void 0 2180 }); 2181 }); 2182 if (!tmpStart) { 2183 currentBeat += duration; 2184 } 2185 } else if (child.tagName === "rest" || child.tagName === "mRest") { 2186 const durAttr = child.getAttribute("dur") || "4"; 2187 const dotsAttr = child.getAttribute("dots") || null; 2188 const duration = child.tagName === "mRest" ? 4 : getBeats$1(durAttr, dotsAttr); 2189 if (!tmpStart) { 2190 currentBeat += duration; 2191 } 2192 } 2193 }); 2194 while (layer.firstChild) { 2195 layer.removeChild(layer.firstChild); 2196 } 2197 if (noteEvents.length === 0) { 2198 if (layer.getAttribute("n") === "1") { 2199 const mRestEl = doc.createElementNS(ns, "mRest"); 2200 const layerId = layer.getAttribute("xml:id") || ""; 2201 const mRestId = layerId.replace("l-", "mr-") || "mr-" + Math.random().toString(36).substring(2, 9); 2202 mRestEl.setAttribute("xml:id", mRestId); 2203 layer.appendChild(mRestEl); 2204 } else { 2205 layer.remove(); 2206 } 2207 return; 2208 } 2209 noteEvents.sort((a, b) => { 2210 if (Math.abs(a.startBeat - b.startBeat) > 1e-3) { 2211 return a.startBeat - b.startBeat; 2212 } 2213 return a.pname.localeCompare(b.pname); 2214 }); 2215 const uniqueStartBeats = Array.from(new Set(noteEvents.map((e) => Math.round(e.startBeat * 1e3) / 1e3))).sort((a, b) => a - b); 2216 for (let i = 0; i < uniqueStartBeats.length - 1; i++) { 2217 const currentStart = uniqueStartBeats[i]; 2218 const nextStart = uniqueStartBeats[i + 1]; 2219 const currentGroup = noteEvents.filter((e) => Math.abs(e.startBeat - currentStart) < 1e-3); 2220 const maxDur = Math.max(...currentGroup.map((g) => g.duration)); 2221 if (currentStart + maxDur > nextStart) { 2222 const allowedDur = nextStart - currentStart; 2223 currentGroup.forEach((g) => { 2224 g.duration = allowedDur; 2225 }); 2226 } 2227 } 2228 const groupedEvents = []; 2229 noteEvents.forEach((evt) => { 2230 let grp = groupedEvents.find((g) => Math.abs(g.startBeat - evt.startBeat) < 1e-3); 2231 if (!grp) { 2232 grp = { startBeat: evt.startBeat, duration: evt.duration, notes: [] }; 2233 groupedEvents.push(grp); 2234 } 2235 grp.notes.push(evt); 2236 grp.duration = evt.duration; 2237 }); 2238 groupedEvents.sort((a, b) => a.startBeat - b.startBeat); 2239 let rebuildBeat = 0; 2240 const measureBeats = 4; 2241 groupedEvents.forEach((grp) => { 2242 const gap = grp.startBeat - rebuildBeat; 2243 if (gap > 1e-3) { 2244 const rests = generateRestsForDuration(doc, gap);
2245 rests.forEach((r) => layer.appendChild(r)); 2246 } 2247 const { dur: durAttr, dots: dotsAttr } = getMeiDurAttr(grp.duration); 2248 console.log("EDITOR_LOG: grp.duration =", grp.duration, "durAttr =", durAttr, "dotsAttr =", dotsAttr); 2249 if (grp.notes.length > 1) { 2250 const chord = doc.createElementNS(ns, "chord"); 2251 chord.setAttribute("xml:id", "chord-" + Math.random().toString(36).substr(2, 9)); 2252 chord.setAttribute("dur", durAttr); 2253 if (dotsAttr) chord.setAttribute("dots", dotsAttr); 2254 grp.notes.forEach((n) => { 2255 const noteEl = n.element; 2256 noteEl.setAttribute("xml:id", noteEl.getAttribute("xml:id") || "note-" + Math.random().toString(36).substr(2, 9)); 2257 noteEl.setAttribute("pname", n.pname); 2258 noteEl.setAttribute("oct", n.oct); 2259 if (n.accid) noteEl.setAttribute("accid", n.accid); 2260 else noteEl.removeAttribute("accid"); 2261 noteEl.removeAttribute("dur"); 2262 noteEl.removeAttribute("dots"); 2263 if (n.tie) { 2264 noteEl.setAttribute("tie", n.tie); 2265 } else { 2266 noteEl.removeAttribute("tie"); 2267 } 2268 chord.appendChild(noteEl); 2269 }); 2270 layer.appendChild(chord); 2271 } else { 2272 const n = grp.notes[0]; 2273 const noteEl = n.element; 2274 noteEl.setAttribute("xml:id", noteEl.getAttribute("xml:id") || "note-" + Math.random().toString(36).substr(2, 9)); 2275 noteEl.setAttribute("pname", n.pname); 2276 noteEl.setAttribute("oct", n.oct); 2277 if (n.accid) noteEl.setAttribute("accid", n.accid); 2278 else noteEl.removeAttribute("accid"); 2279 noteEl.setAttribute("dur", durAttr); 2280 if (dotsAttr) noteEl.setAttribute("dots", dotsAttr); 2281 else noteEl.removeAttribute("dots"); 2282 if (n.tie) noteEl.setAttribute("tie", n.tie); 2283 else noteEl.removeAttribute("tie"); 2284 layer.appendChild(noteEl); 2285 } 2286 rebuildBeat = grp.startBeat + grp.duration; 2287 }); 2288 const remaining = measureBeats - rebuildBeat; 2289 if (remaining > 1e-3) { 2290 const rests = generateRestsForDuration(doc, remaining); 2291 rests.forEach((r) => layer.appendChild(r)); 2292 } 2293 }); 2294}; 2295const wrapMeiTuplets = (doc) => { 2296 const ns = "http://www.music-encoding.org/ns/mei"; 2297 const layersAll = Array.from(doc.querySelectorAll("layer")); 2298 layersAll.forEach((layer) => { 2299 const tuplets = Array.from(layer.querySelectorAll("tuplet")); 2300 tuplets.forEach((t) => { 2301 const children2 = Array.from(t.children); 2302 children2.forEach((child) => { 2303 layer.insertBefore(child, t); 2304 }); 2305 t.remove(); 2306 }); 2307 const children = Array.from(layer.children); 2308 let tupletGroup = []; 2309 let tupletSum = 0; 2310 const processTupletGroup = () => { 2311 if (tupletGroup.length > 0 && Math.abs(tupletSum - 1) < 0.15) { 2312 const tupletEl = doc.createElementNS(ns, "tuplet"); 2313 tupletEl.setAttribute("num", "3"); 2314 tupletEl.setAttribute("numbase", "2"); 2315 tupletEl.setAttribute("xml:id", "tup-" + Math.random().toString(36).substring(2, 9)); 2316 layer.insertBefore(tupletEl, tupletGroup[0]); 2317 tupletGroup.forEach((el) => { 2318 el.removeAttribute("num"); 2319 el.removeAttribute("numbase"); 2320 tupletEl.appendChild(el); 2321 }); 2322 } else if (tupletGroup.length > 0) { 2323 tupletGroup.forEach((el) => { 2324 el.setAttribute("num", "3"); 2325 el.setAttribute("numbase", "2"); 2326 }); 2327 } 2328 tupletGroup = []; 2329 tupletSum = 0; 2330 }; 2331 children.forEach((child) => { 2332 if (child.tagName === "note" || child.tagName === "rest" || child.tagName === "chord") { 2333 let dur = child.getAttribute("dur") || "4"; 2334 let dots = child.getAttribute("dots") || null; 2335 if (child.tagName === "chord") { 2336 const firstNote = child.querySelector("note"); 2337 if (firstNote) { 2338 dur = firstNote.getAttribute("dur") || "4"; 2339 dots = firstNote.getAttribute("dots") || null; 2340 } 2341 } 2342 const num = child.getAttribute("num"); 2343 const numbase = child.getAttribute("numbase"); 2344 const isTripletAttr = num === "3" && numbase === "2"; 2345 const beatDur = getBeats$1(dur, dots, num, numbase); 2346 const isTriplet = isTripletAttr || Math.abs(beatDur - 0.333) < 0.05 || Math.abs(beatDur - 0.666) < 0.05; 2347 if (isTriplet) { 2348 tupletGroup.push(child); 2349 tupletSum += beatDur; 2350 if (tupletSum >= 0.95) { 2351 processTupletGroup(); 2352 } 2353 } else { 2354 processTupletGroup(); 2355 } 2356 } else { 2357 processTupletGroup(); 2358 } 2359 }); 2360 processTupletGroup(); 2361 }); 2362}; 2363const getMeiDurAttr = (beats) => { 2364 const approx = (val) => Math.abs(beats - val) < 5e-3; 2365 if (beats >= 4) return { dur: "1" }; 2366 if (beats >= 3.5) return { dur: "1", dots: "2" }; 2367 if (beats >= 3) return { dur: "2", dots: "1" }; 2368 if (beats >= 2.75) return { dur: "2", dots: "2" }; 2369 if (beats >= 2) return { dur: "2" }; 2370 if (beats >
2370= 1.75) return { dur: "4", dots: "2" }; 2371 if (beats >= 1.5) return { dur: "4", dots: "1" }; 2372 if (approx(1.333)) return { dur: "2" }; 2373 if (beats >= 1) return { dur: "4" }; 2374 if (beats >= 0.875) return { dur: "8", dots: "2" }; 2375 if (beats >= 0.75) return { dur: "8", dots: "1" }; 2376 if (approx(0.666)) return { dur: "4" }; 2377 if (beats >= 0.5) return { dur: "8" }; 2378 if (beats >= 0.4375) return { dur: "16", dots: "2" }; 2379 if (beats >= 0.375) return { dur: "16", dots: "1" }; 2380 if (approx(0.333)) return { dur: "8" }; 2381 if (beats >= 0.25) return { dur: "16" }; 2382 if (beats >= 0.21875) return { dur: "32", dots: "2" }; 2383 if (beats >= 0.125) return { dur: "32" }; 2384 if (beats >= 0.109375) return { dur: "64", dots: "2" }; 2385 return { dur: "64" }; 2386}; 2387const convertMidiToMEI = (track, ppq = 480) => { 2388 const parser = new DOMParser(); 2389 const isPiano = track.instrument.name.toLowerCase().includes("piano"); 2390 const doc = parser.parseFromString(createEmptyMEI("Title", track.channel === 9, isPiano), "application/xml"); 2391 const ns = "http://www.music-encoding.org/ns/mei"; 2392 const processedNotes = []; 2393 const flatNotes = [...track.notes].sort((a, b) => a.ticks - b.ticks); 2394 for (let i = 0; i < flatNotes.length; i++) { 2395 const n = flatNotes[i]; 2396 let startBeat = n.ticks / ppq; 2397 let durBeats = n.durationTicks / ppq; 2398 if (i < flatNotes.length - 1) { 2399 const nextN = flatNotes[i + 1]; 2400 const nextStartBeat = nextN.ticks / ppq; 2401 const endBeat = startBeat + durBeats; 2402 const gap = nextStartBeat - endBeat; 2403 if (gap > 0 && gap <= 0.55) { 2404 durBeats += gap; 2405 } 2406 } 2407 const snapToGrid = (val) => { 2408 const nearest16th = Math.round(val * 4) / 4; 2409 const nearestTriplet = Math.round(val * 3) / 3; 2410 if (Math.abs(val - nearestTriplet) < Math.abs(val - nearest16th) && Math.abs(val - nearestTriplet) < 0.05) { 2411 return nearestTriplet; 2412 } 2413 return nearest16th; 2414 }; 2415 startBeat = snapToGrid(startBeat); 2416 durBeats = snapToGrid(durBeats); 2417 if (durBeats <= 0.01) durBeats = 0.25; 2418 let staff = 1; 2419 if (isPiano && track.channel !== 9) { 2420 if (n.midi < 60) staff = 2; 2421 } 2422 processedNotes.push({ ...n, _beatTime: startBeat, _beatDuration: durBeats, staff, _originalIdx: i }); 2423 } 2424 const splitRhythms = (notes) => { 2425 const splitNotes = []; 2426 notes.forEach((note) => { 2427 let currentBeat = note._beatTime; 2428 let remainingDur = note._beatDuration; 2429 const measureBeats = 4; 2430 const parts = []; 2431 while (remainingDur > 1e-3) { 2432 const beatInMeasure = currentBeat % measureBeats; 2433 let maxDur = measureBeats - beatInMeasure; 2434 if (beatInMeasure < 2 && beatInMeasure + remainingDur > 2) { 2435 if (!(beatInMeasure === 0 && remainingDur >= 4) && !(beatInMeasure === 0 && remainingDur === 3)) { 2436 maxDur = Math.min(maxDur, 2 - beatInMeasure); 2437 } 2438 } 2439 const isOffBeat = beatInMeasure % 1 > 1e-3; 2440 if (isOffBeat && remainingDur > 0.5) { 2441 const nextBeat = Math.ceil(beatInMeasure); 2442 maxDur = Math.min(maxDur, nextBeat - beatInMeasure); 2443 } 2444 const durInThisPart = Math.min(remainingDur, maxDur); 2445 parts.push({ ...note, _beatTime: currentBeat, _beatDuration: durInThisPart }); 2446 currentBeat += durInThisPart; 2447 remainingDur -= durInThisPart; 2448 } 2449 if (parts.length > 1) { 2450 for (let i = 0; i < parts.length; i++) { 2451 if (i === 0) parts[i].tie = "i"; 2452 else if (i === parts.length - 1) parts[i].tie = "t"; 2453 else parts[i].tie = "m"; 2454 } 2455 } 2456 splitNotes.push(...parts); 2457 }); 2458 return splitNotes; 2459 }; 2460 const tiedNotes = splitRhythms(processedNotes); 2461 const stavesNotes = { 2462 1: { 1: [], 2: [], 3: [], 4: [] }, 2463 2: { 1: [], 2: [], 3: [], 4: [] } 2464 }; 2465 tiedNotes.sort((a, b) => { 2466 if (Math.abs(a._beatTime - b._beatTime) < 1e-3) return b._beatDuration - a._beatDuration; 2467 return a._beatTime - b._beatTime; 2468 }); 2469 tiedNotes.forEach((note) => { 2470 const staffIdx = note.staff; 2471 const voice = note.voice || 1; 2472 const safeVoice = Math.max(1, Math.min(4, voice)); 2473 stavesNotes[staffIdx][safeVoice].push(note); 2474 }); 2475 let maxBeat = 0;
2476 tiedNotes.forEach((n) => { 2477 maxBeat = Math.max(maxBeat, n._beatTime + n._beatDuration); 2478 }); 2479 const maxMeasureNum = Math.max(20, Math.floor(maxBeat / 4) + 1); 2480 const section = doc.querySelector("section"); 2481 if (section) { 2482 for (let m = 1; m <= maxMeasureNum; m++) { 2483 let measure = doc.querySelector(`measure[n="${m}"]`); 2484 if (!measure) { 2485 measure = doc.createElementNS(ns, "measure"); 2486 measure.setAttribute("n", m.toString()); 2487 measure.setAttribute("xml:id", `m-${m}`); 2488 section.appendChild(measure); 2489 } 2490 const staffNums = isPiano ? [1, 2] : [1]; 2491 staffNums.forEach((staffNum) => { 2492 let staff = measure.querySelector(`staff[n="${staffNum}"]`); 2493 if (!staff) { 2494 staff = doc.createElementNS(ns, "staff"); 2495 staff.setAttribute("n", staffNum.toString()); 2496 staff.setAttribute("xml:id", `s-${m}-${staffNum}`); 2497 measure.appendChild(staff); 2498 } 2499 const layers = stavesNotes[staffNum]; 2500 for (let l = 1; l <= 4; l++) { 2501 const notesInLayer = layers[l]; 2502 if (notesInLayer.length === 0) continue; 2503 const layerN = l.toString(); 2504 let layer = staff.querySelector(`layer[n="${layerN}"]`); 2505 if (!layer) { 2506 layer = doc.createElementNS(ns, "layer"); 2507 layer.setAttribute("n", layerN); 2508 layer.setAttribute("xml:id", `l-${m}-${staffNum}-${layerN}`); 2509 const mRest = doc.createElementNS(ns, "mRest"); 2510 mRest.setAttribute("xml:id", `mr-${m}-${staffNum}-${layerN}`); 2511 layer.appendChild(mRest); 2512 staff.appendChild(layer); 2513 } 2514 } 2515 }); 2516 } 2517 } 2518 Object.keys(stavesNotes).forEach((staffKey) => { 2519 const staffNum = parseInt(staffKey); 2520 const layers = stavesNotes[staffNum]; 2521 Object.keys(layers).forEach((layerKey) => { 2522 const layerNum = parseInt(layerKey); 2523 const notesInLayer = layers[layerNum]; 2524 if (notesInLayer.length === 0) return; 2525 const layerN = layerNum.toString(); 2526 const notesByTime = {}; 2527 notesInLayer.forEach((note) => { 2528 const timeKey = note._beatTime.toFixed(3); 2529 if (!notesByTime[timeKey]) notesByTime[timeKey] = []; 2530 notesByTime[timeKey].push(note); 2531 }); 2532 const sortedTimes = Object.keys(notesByTime).map(Number).sort((a, b) => a - b); 2533 sortedTimes.forEach((time) => { 2534 const notes = notesByTime[time.toFixed(3)]; 2535 if (notes.length === 0) return; 2536 const startBeat = time; 2537 const measureNum = Math.floor(startBeat / 4) + 1; 2538 const beatInMeasure = startBeat % 4; 2539 const measure = doc.querySelector(`measure[n="${measureNum}"]`); 2540 if (!measure) return; 2541 const staff = measure.querySelector(`staff[n="${staffNum}"]`); 2542 if (!staff) return; 2543 const layer = staff.querySelector(`layer[n="${layerN}"]`); 2544 if (!layer) return; 2545 const mRest = layer.querySelector("mRest"); 2546 if (mRest) mRest.remove(); 2547 if (notes.length > 1) { 2548 const chord = doc.createElementNS(ns, "chord"); 2549 chord.setAttribute("xml:id", "chord-" + Math.random().toString(36).substr(2, 9)); 2550 const { dur: chordDur, dots: chordDots } = getMeiDurAttr(notes[0]._beatDuration); 2551 chord.setAttribute("dur", chordDur); 2552 if (chordDots) chord.setAttribute("dots", chordDots); 2553 chord.setAttribute("tmp-start-beat", beatInMeasure.toString()); 2554 chord.setAttribute("tmp-beat-duration", notes[0]._beatDuration.toString()); 2555 notes.forEach((note) => { 2556 const noteEl = doc.createElementNS(ns, "note"); 2557 noteEl.setAttribute("xml:id", "note-" + Math.random().toString(36).substr(2, 9)); 2558 if (track.channel === 9) { 2559 const mapped = DRUM_MAP[note.midi] || { pname: "c", oct: 5, shape: "diamond" }; 2560 noteEl.setAttribute("pname", mapped.pname); 2561 noteEl.setAttribute("oct", mapped.oct.toString()); 2562 if (mapped.shape) noteEl.setAttribute("head.shape", mapped.shape); 2563 } else { 2564 const meiPitch = getMEIFromMidi(note.midi); 2565 noteEl.setAttribute("pname", meiPitch.pname); 2566 noteEl.setAttribute("oct", meiPitch.oct.toString()); 2567 if (meiPitch.accid) noteEl.setAttribute("accid", meiPitch.accid); 2568 } 2569 if (note.tie) noteEl.setAttribute("tie", note.tie); 2570 noteEl.setAttribute("tmp-midi-idx", note._originalIdx.toString()); 2571 chord.appendChild(noteEl); 2572 }); 2573 layer.appendChild(chord); 2574 } else { 2575 const note = notes[0]; 2576 const noteEl = doc.createElementNS(ns, "note"); 2577 noteEl.setAttribute("xml:id", "note-" + Math.random().toString(36).substr(2, 9)); 2578 const { dur: noteDur, dots: noteDots } = getMeiDurAttr(note._beatDuration); 2579 noteEl.setAttribute("dur", noteDur); 2580 if (noteDots) noteEl.setAttribute("dots", noteDots); 2581 noteEl.setAttribute("tmp-start-beat", beatInMeasure.toString()); 2582 noteEl.setAttribute("tmp-beat-duration", note._beatDuration.toString()); 2583 if (track.channel === 9) { 2584 const mapped = DRUM_MAP[note.midi] || { pname: "c", oct: 5, shape: "diamond" }; 2585 noteEl.setAttribute("pname", mapped.pname); 2586 noteEl.setAttribute("oct", mapped.oct.toString()); 2587 if (mapped.shape) noteEl.setAttribute("head.shape", mapped.shape); 2588 } else { 2589 const meiPitch = getMEIFromMidi(note.midi); 2590 noteEl.setAttribute("pname", meiPitch.pname); 2591 noteEl.setAttribute("oct", meiPitch.oct.toString()); 2592 if (meiPitch.accid) noteEl.setAttribute("accid", meiPitch.accid); 2593 } 2594 if (note.tie) noteEl.setAttribute("tie", note.tie); 2595 noteEl.setAttribute("tmp-midi-idx", note._originalIdx.toString()); 2596 layer.appendChild(noteEl); 2597 } 2598 }); 2599 }); 2600 }); 2601 const layersAll = Array.from(doc.querySelectorAll("layer")); 2602 layersAll.forEach((layer) => { 2603 const children = Array.from(layer.children); 2604 let tupletGroup = []; 2605 let tupletSum = 0; 2606 const processTupletGroup = () => { 2607 if (tupletGroup.length > 0 && Math.abs(tupletSum - 1) < 0.1) { 2608 const tupletEl = doc.createElementNS(ns, "tuplet"); 2609 tupletEl.setAttribute("num", "3"); 2610 tupletEl.setAttribute("numbase", "2"); 2611 layer.insertBefore(tupletEl, tupletGroup[0]); 2612 tupletGroup.forEach((el) => tupletEl.appendChild(el)); 2613 } 2614 tupletGroup = []; 2615 tupletSum = 0; 2616 };
2617 children.forEach((child) => { 2618 const beatDurStr = child.getAttribute("tmp-beat-duration"); 2619 if (beatDurStr) { 2620 const beatDur = parseFloat(beatDurStr); 2621 const isTriplet = Math.abs(beatDur - 0.333) < 0.05 || Math.abs(beatDur - 0.666) < 0.05; 2622 if (isTriplet) { 2623 tupletGroup.push(child); 2624 tupletSum += beatDur; 2625 if (tupletSum >= 0.95) { 2626 processTupletGroup(); 2627 } 2628 } else { 2629 processTupletGroup(); 2630 } 2631 } else { 2632 processTupletGroup(); 2633 } 2634 }); 2635 processTupletGroup(); 2636 }); 2637 consolidateMeiRests(doc); 2638 const allNotes = Array.from(doc.querySelectorAll("note")); 2639 const tieStartNotes = allNotes.filter((n) => n.getAttribute("tie") === "i" || n.getAttribute("tie") === "m"); 2640 tieStartNotes.forEach((startNote) => { 2641 const startId = startNote.getAttribute("xml:id"); 2642 const pname = startNote.getAttribute("pname"); 2643 const oct = startNote.getAttribute("oct"); 2644 const tmpIdx = startNote.getAttribute("tmp-midi-idx"); 2645 const startIndex = allNotes.indexOf(startNote); 2646 let endNote = null; 2647 for (let i = startIndex + 1; i < allNotes.length; i++) { 2648 const n = allNotes[i]; 2649 if (n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tmp-midi-idx") === tmpIdx) { 2650 if (n.getAttribute("tie") === "m" || n.getAttribute("tie") === "t") { 2651 endNote = n; 2652 break; 2653 } 2654 } 2655 } 2656 if (endNote && startId && endNote.getAttribute("xml:id")) { 2657 const tieEl = doc.createElementNS(ns, "tie"); 2658 tieEl.setAttribute("startid", "#" + startId); 2659 tieEl.setAttribute("endid", "#" + endNote.getAttribute("xml:id")); 2660 const m = startNote.closest("measure"); 2661 if (m) m.appendChild(tieEl); 2662 } 2663 }); 2664 allNotes.forEach((n) => { 2665 n.removeAttribute("tmp-midi-idx"); 2666 n.removeAttribute("tmp-beat-duration"); 2667 }); 2668 const allChords = Array.from(doc.querySelectorAll("chord")); 2669 allChords.forEach((c) => c.removeAttribute("tmp-beat-duration")); 2670 const serializer = new XMLSerializer(); 2671 return serializer.serializeToString(doc); 2672}; 2673const getMidiNotesFromBase64 = (base64Midi, originalPpq = 480) => { 2674 const binaryString = window.atob(base64Midi); 2675 const len = binaryString.length; 2676 const bytes = new Uint8Array(len); 2677 for (let i = 0; i < len; i++) { 2678 bytes[i] = binaryString.charCodeAt(i); 2679 } 2680 const parsedMidi = new Midi.Midi(bytes.buffer); 2681 const notes = []; 2682 parsedMidi.tracks.forEach((track) => { 2683 track.notes.forEach((note) => { 2684 const scalingFactor = originalPpq / parsedMidi.header.ppq; 2685 const ticks = Math.round(note.ticks * scalingFactor); 2686 const durationTicks = Math.round(note.durationTicks * scalingFactor); 2687 notes.push({ 2688 midi: note.midi, 2689 ticks, 2690 durationTicks, 2691 velocity: note.velocity, 2692 voice: note.voice || 1 2693 }); 2694 }); 2695 }); 2696 return notes; 2697}; 2698const getPitchFromY = (meiDoc, yOffset, snappedBeat) => { 2699 const step = 38 - yOffset; 2700 const pitchMap = ["c", "d", "e", "f", "g", "a", "b"]; 2701 const pitch = pitchMap[(step % 7 + 7) % 7]; 2702 const oct = Math.floor(step / 7); 2703 return `${pitch}${oct}`; 2704}; 2705const ScoreSyncManager = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ 2706 __proto__: null, 2707 DRUM_MAP, 2708 REVERSE_DRUM_MAP, 2709 consolidateMeiRests, 2710 convertMidiToMEI, 2711 createEmptyMEI, 2712 generateRestsForDuration, 2713 getBeats: getBeats$1, 2714 getDurationInBeats: getDurationInBeats$1, 2715 getMEIFromMidi, 2716 getMeiDurAttr, 2717 getMidiFromMEI: getMidiFromMEI$1, 2718 getMidiNotesFromBase64, 2719 getPitchFromY, 2720 wrapMeiTuplets 2721}, Symbol.toStringTag, { value: "Module" })); 2722const useScoreEditorEngine = (props) => { 2723 const { initialMei, dimensions, zoom, isPercussion, onNoteAdded, onNoteDeleted, onMeiUpdate, showMeasureNumbers = true, currentPage = 1, layoutMode = "linear" } = props; 2724 const [toolkit, setToolkit] = reactExports.useState(null); 2725 const [svg, setSvg] = reactExports.useState(""); 2726 const [isReady, setIsReady] = reactExports.useState(false); 2727 const currentMeiRef = reactExports.useRef(initialMei || createEmptyMEI("Title", isPercussion)); 2728 const onMeiUpdateRef = reactExports.useRef(onMeiUpdate); 2729 const [historyState, setHistoryState] = reactExports.useState(() => { 2730 const startMei = initialMei || createEmptyMEI("Title", isPercussion); 2731 return { 2732 history: [startMei], 2733 index: 0 2734 }; 2735 }); 2736 const pushToHistory = reactExports.useCallback((mei) => { 2737 setHistoryState((prev) => { 2738 if (prev.index >= 0 && prev.history[prev.index] === mei) return prev; 2739 const newHistory = prev.history.slice(0, prev.index + 1); 2740 let updatedHistory = [...newHistory, mei]; 2741 let newIndex = newHistory.length; 2742 if (updatedHistory.length > 100) { 2743 updatedHistory = updatedHistory.slice(updatedHistory.length - 100); 2744 newIndex = updatedHistory.length - 1; 2745 } 2746 return { 2747 history: updatedHistory, 2748 index: newIndex 2749 }; 2750 }); 2751 }, []); 2752 reactExports.useEffect(() => { 2753 onMeiUpdateRef.current = onMeiUpdate; 2754 }, [onMeiUpdate]); 2755 const updateSvg = reactExports.useCallback(() => { 2756 if (!toolkit) return; 2757 const pageCount = toolkit.getPageCount(); 2758 if (pageCount > 0) { 2759 const pageToRender = Math.min(pageCount, Math.max(1, currentPage)); 2760 const render = toolkit.renderToSVG(pageToRender, {}); 2761 setSvg(render); 2762 } 2763 }, [toolkit, currentPage]); 2764 reactExports.useEffect(() => { 2765 let mounted = true; 2766 const injectScript = () => { 2767 return new Promise((resolve, reject) => { 2768 const existing = document.querySelector('script[src*="verovio"]'); 2769 if (window.verovio) { 2770 resolve(); 2771 return; 2772 } 2773 if (existing) { 2774 resolve(); 2775 return; 2776 } 2777 const script = document.createElement("script"); 2778 script.src = "/libs/verovio/verovio-toolkit-wasm.js"; 2779 script.async = true; 2780 script.onload = () => { 2781 script.dataset.loaded = "true"; 2782 resolve(); 2783 }; 2784 script.onerror = () => reject(new Error("Failed to download verovio-toolkit-wasm.js")); 2785 document.head.appendChild(script); 2786 }); 2787 }; 2788 const initVerovio = async () => {
2789 try { 2790 await injectScript(); 2791 } catch (e) { 2792 console.error("Verovio script download failed:", e); 2793 return; 2794 } 2795 let attempts = 0; 2796 while (attempts < 100) { 2797 if (!mounted) return; 2798 const v2 = window.verovio; 2799 if (v2 && typeof v2 === "object") break; 2800 await new Promise((r) => setTimeout(r, 100)); 2801 attempts++; 2802 } 2803 const v = window.verovio; 2804 if (!v) { 2805 console.error("Verovio not loaded on window after timeout."); 2806 return; 2807 } 2808 try { 2809 let module = v.module; 2810 if (module && typeof module.then === "function") module = await module; 2811 else if (typeof module === "function") module = await module(); 2812 if (module && !module.cwrap) { 2813 let waitAttempts = 0; 2814 while (!module.cwrap && waitAttempts < 100) { 2815 if (!mounted) return; 2816 await new Promise((r) => setTimeout(r, 50)); 2817 waitAttempts++; 2818 } 2819 } 2820 const ToolkitClass = v.toolkit; 2821 let tk = null; 2822 if (!module || !module.cwrap) { 2823 try { 2824 tk = new ToolkitClass(); 2825 } catch (err) { 2826 console.error("No-args instantiation failed:", err); 2827 } 2828 } 2829 if (!tk) { 2830 let initAttempts = 0; 2831 while (initAttempts < 10) { 2832 try { 2833 tk = new ToolkitClass(module); 2834 if (tk && typeof tk.getVersion === "function") break; 2835 } catch (err) { 2836 if (initAttempts >= 9) throw err; 2837 await new Promise((r) => setTimeout(r, 200)); 2838 initAttempts++; 2839 } 2840 } 2841 } 2842 if (layoutMode === "page") { 2843 tk.setOptions({ 2844 pageHeight: 2970, 2845 pageWidth: 2100, 2846 pageMarginLeft: 100, 2847 pageMarginRight: 100, 2848 pageMarginTop: 100, 2849 pageMarginBottom: 100, 2850 scale: 35, 2851 spacingLinear: 0.3, 2852 spacingNonLinear: 0.45, 2853 spacingDurDetection: true, 2854 breaks: "encoded", 2855 header: "none", 2856 footer: "none", 2857 justifyLastSystem: true, 2858 minLastJustification: 0, 2859 adjustPageHeight: true, 2860 adjustPageWidth: false, 2861 svgBoundingBoxes: true, 2862 mnumInterval: showMeasureNumbers ? 1 : 0 2863 }); 2864 } else { 2865 tk.setOptions({ 2866 scale: 40, 2867 font: "Leland", 2868 header: "auto", 2869 footer: "none", 2870 pageMarginTop: 195, 2871 pageMarginBottom: 195, 2872 pageMarginLeft: 150, 2873 pageMarginRight: 150, 2874 pageWidth: 2100, 2875 pageHeight: 2970, 2876 adjustPageHeight: true, 2877 adjustPageWidth: false, 2878 svgBoundingBoxes: true, 2879 spacingSystem: 14, 2880 spacingStaff: 10, 2881 justifyVertically: true, 2882 breaks: "line", 2883 mnumInterval: showMeasureNumbers ? 1 : 0 2884 }); 2885 } 2886 if (mounted) { 2887 setToolkit(tk); 2888 } 2889 } catch (err) { 2890 console.error("Failed to init Verovio Editor Toolkit", err); 2891 } 2892 }; 2893 initVerovio(); 2894 return () => { 2895 mounted = false; 2896 }; 2897 }, []); 2898 reactExports.useEffect(() => { 2899 if (!toolkit) return; 2900 toolkit.setOptions({ 2901 mnumInterval: showMeasureNumbers ? 1 : 0 2902 }); 2903 toolkit.redoLayout(); 2904 updateSvg(); 2905 }, [toolkit, showMeasureNumbers, updateSvg]); 2906 reactExports.useEffect(() => { 2907 if (!toolkit) return; 2908 let processedMei = currentMeiRef.current; 2909 if (processedMei && !processedMei.includes("<label>")) { 2910 processedMei = processedMei.replace(/(<staffDef\b[^>]*>)/g, "$1<label> </label>"); 2911 currentMeiRef.current = processedMei; 2912 } 2913 const success = toolkit.loadData(processedMei); 2914 if (success === true || success === 1) { 2915 setIsReady(true); 2916 updateSvg(); 2917 setHistoryState({ 2918 history: [processedMei], 2919 index: 0 2920 }); 2921 } 2922 }, [toolkit]); 2923 reactExports.useEffect(() => { 2924 if (!toolkit) return; 2925 let targetMei = initialMei || createEmptyMEI(); 2926 if (targetMei && !targetMei.includes("<label>")) { 2927 targetMei = targetMei.replace(/(<staffDef\b[^>]*>)/g, "$1<label> </label>"); 2928 } 2929 if (targetMei !== currentMeiRef.current) { 2930 currentMeiRef.current = targetMei; 2931 const success = toolkit.loadData(targetMei); 2932 if (success === true || success === 1) { 2933 setIsReady(true); 2934 updateSvg(); 2935 setHistoryState((prev) => { 2936 if (prev.history.includes(targetMei)) return prev; 2937 return { 2938 history: [targetMei], 2939 index: 0 2940 }; 2941 }); 2942 } 2943 } 2944 }, [initialMei, toolkit, updateSvg]); 2945 reactExports.useEffect(() => { 2946 if (!toolkit || !isReady) return; 2947 toolkit.setOptions({ scale: layoutMode === "page" ? 19 : 45 }); 2948 toolkit.redoLayout(); 2949 updateSvg(); 2950 }, [toolkit, isReady, updateSvg, layoutMode]); 2951 const performAction = reactExports.useCallback((action) => { 2952 var _a; 2953 if (!toolkit) return false;
2954 try { 2955 const res = toolkit.edit(action); 2956 if (res) { 2957 const mei = toolkit.getMEI(); 2958 const parser = new DOMParser(); 2959 const doc = parser.parseFromString(mei, "application/xml"); 2960 consolidateMeiRests(doc); 2961 wrapMeiTuplets(doc); 2962 const serializer = new XMLSerializer(); 2963 const newMei = serializer.serializeToString(doc); 2964 toolkit.loadData(newMei); 2965 currentMeiRef.current = newMei; 2966 updateSvg(); 2967 (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, newMei); 2968 pushToHistory(newMei); 2969 return true; 2970 } 2971 } catch (e) { 2972 console.error("Verovio Editor Action failed", e); 2973 } 2974 return false; 2975 }, [toolkit, updateSvg, pushToHistory]); 2976 const insertNote = reactExports.useCallback((elementId, actionType, noteData) => { 2977 const action = { 2978 action: actionType || "insert", 2979 param: { 2980 startid: elementId, 2981 elementType: noteData.elementType || "note", 2982 ...noteData 2983 } 2984 }; 2985 return performAction(action); 2986 }, [performAction]); 2987 const updateMei = reactExports.useCallback((modifier) => { 2988 var _a; 2989 if (!toolkit) return false; 2990 try { 2991 const parser = new DOMParser(); 2992 const doc = parser.parseFromString(currentMeiRef.current, "application/xml"); 2993 modifier(doc); 2994 consolidateMeiRests(doc); 2995 wrapMeiTuplets(doc); 2996 const serializer = new XMLSerializer(); 2997 const newMei = serializer.serializeToString(doc); 2998 if (newMei.includes('dur="32"')) { 2999 console.log("EDITOR_LOG: dur=32 is present in newMei!"); 3000 } else { 3001 console.log("EDITOR_LOG: dur=32 is NOT present in newMei!"); 3002 } 3003 const success = toolkit.loadData(newMei); 3004 if (success === true || success === 1) { 3005 currentMeiRef.current = newMei; 3006 updateSvg(); 3007 (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, newMei); 3008 pushToHistory(newMei); 3009 return true; 3010 } 3011 } catch (err) { 3012 console.error("Failed to update MEI", err); 3013 } 3014 return false; 3015 }, [toolkit, updateSvg, pushToHistory]); 3016 const deleteElement = reactExports.useCallback((elementId) => { 3017 const action = { 3018 action: "delete", 3019 param: { startid: elementId } 3020 }; 3021 const success = performAction(action); 3022 if (success) return true; 3023 return updateMei((doc) => { 3024 const getXmlElById = (xmlDoc, id) => { 3025 return xmlDoc.getElementById(id) || xmlDoc.querySelector(`[id="${id}"]`) || xmlDoc.querySelector(`[xml\\:id="${id}"]`) || xmlDoc.querySelector(`[*|id="${id}"]`) || Array.from(xmlDoc.getElementsByTagName("*")).find((el) => el.getAttribute("xml:id") === id) || null; 3026 }; 3027 const noteEl = getXmlElById(doc, elementId); 3028 if (noteEl) { 3029 const parent = noteEl.parentNode; 3030 noteEl.remove(); 3031 if (parent && parent.tagName === "chord") { 3032 const remainingNotes = Array.from(parent.querySelectorAll("note")); 3033 if (remainingNotes.length === 1) { 3034 const singleNote = remainingNotes[0]; 3035 const chordDur = parent.getAttribute("dur"); 3036 const chordDots = parent.getAttribute("dots"); 3037 if (chordDur) singleNote.setAttribute("dur", chordDur); 3038 if (chordDots) singleNote.setAttribute("dots", chordDots); 3039 const chordParent = parent.parentNode; 3040 if (chordParent) { 3041 chordParent.insertBefore(singleNote, parent); 3042 parent.remove(); 3043 } 3044 } else if (remainingNotes.length === 0) { 3045 parent.remove(); 3046 } 3047 } 3048 } 3049 }); 3050 }, [performAction, updateMei]); 3051 const setAttributes = reactExports.useCallback((elementId, attributes) => { 3052 const action = { 3053 action: "set", 3054 param: { 3055 elementId, 3056 attributes 3057 } 3058 }; 3059 return performAction(action); 3060 }, [performAction]); 3061 const undo = reactExports.useCallback(() => { 3062 if (!toolkit) return false; 3063 let undone = false; 3064 setHistoryState((prev) => { 3065 var _a; 3066 if (prev.index > 0) { 3067 const newIndex = prev.index - 1; 3068 const prevMei = prev.history[newIndex]; 3069 toolkit.loadData(prevMei); 3070 currentMeiRef.current = prevMei; 3071 updateSvg(); 3072 (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, prevMei); 3073 undone = true; 3074 return { 3075 ...prev, 3076 index: newIndex 3077 }; 3078 } 3079 return prev; 3080 }); 3081 return undone; 3082 }, [toolkit, updateSvg]); 3083 const redo = reactExports.useCallback(() => { 3084 if (!toolkit) return false;
3085 let redone = false; 3086 setHistoryState((prev) => { 3087 var _a; 3088 if (prev.index < prev.history.length - 1) { 3089 const newIndex = prev.index + 1; 3090 const nextMei = prev.history[newIndex]; 3091 toolkit.loadData(nextMei); 3092 currentMeiRef.current = nextMei; 3093 updateSvg(); 3094 (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, nextMei); 3095 redone = true; 3096 return { 3097 ...prev, 3098 index: newIndex 3099 }; 3100 } 3101 return prev; 3102 }); 3103 return redone; 3104 }, [toolkit, updateSvg]); 3105 const canUndo = historyState.index > 0; 3106 const canRedo = historyState.index < historyState.history.length - 1; 3107 const changeClef = reactExports.useCallback((staffNum, clefShape, clefLine) => { 3108 return updateMei((doc) => { 3109 const staffDefs = Array.from(doc.querySelectorAll("staffDef")); 3110 staffDefs.forEach((sd) => { 3111 if (sd.getAttribute("n") === staffNum.toString()) { 3112 sd.setAttribute("clef.shape", clefShape); 3113 sd.setAttribute("clef.line", clefLine.toString()); 3114 } 3115 }); 3116 }); 3117 }, [updateMei]); 3118 const insertBarline = reactExports.useCallback((barlineType, selectedNoteId) => { 3119 return updateMei((doc) => { 3120 let measureEl = null; 3121 if (selectedNoteId) { 3122 const noteEl = doc.getElementById(selectedNoteId) || doc.querySelector(`[xml\\:id="${selectedNoteId}"]`); 3123 if (noteEl) { 3124 measureEl = noteEl.closest("measure"); 3125 } 3126 } 3127 if (!measureEl) { 3128 const measures = Array.from(doc.querySelectorAll("measure")); 3129 measureEl = measures[measures.length - 1]; 3130 } 3131 if (measureEl) { 3132 measureEl.setAttribute("right", barlineType); 3133 } 3134 }); 3135 }, [updateMei]); 3136 return { 3137 toolkit, 3138 isReady, 3139 svg, 3140 currentMei: currentMeiRef.current, 3141 insertNote, 3142 deleteElement, 3143 setAttributes, 3144 updateMei, 3145 updateSvg, 3146 undo, 3147 redo, 3148 canUndo, 3149 canRedo, 3150 changeClef, 3151 insertBarline 3152 }; 3153}; 3154const getSnappingOffset = (clientY, staffNode) => { 3155 const staffLines = Array.from(staffNode.querySelectorAll("path")); 3156 if (staffLines.length < 5) return 0; 3157 const pathsWithWidth = staffLines.map((line) => { 3158 const rect = line.getBoundingClientRect(); 3159 return { line, width: rect.width, rect }; 3160 }).sort((a, b) => b.width - a.width); 3161 const rects = pathsWithWidth.slice(0, 5).map((item) => item.rect).sort((a, b) => a.top - b.top); 3162 if (rects.length === 5) { 3163 const topLineY = rects[0].top; 3164 const bottomLineY = rects[4].top; 3165 const avgGap = (bottomLineY - topLineY) / 4; 3166 const stepSize = avgGap / 2; 3167 const relativeY = clientY - topLineY; 3168 return Math.round(relativeY / stepSize); 3169 } 3170 return 0; 3171}; 3172const getMidiFromMEI = (pname, oct, accid) => { 3173 const basePitches = { 3174 "c": 0, 3175 "d": 2, 3176 "e": 4, 3177 "f": 5, 3178 "g": 7, 3179 "a": 9, 3180 "b": 11 3181 }; 3182 let midi = (oct + 1) * 12 + basePitches[pname.toLowerCase()]; 3183 if (accid === "s") midi += 1; 3184 if (accid === "f") midi -= 1; 3185 if (accid === "ss") midi += 2; 3186 if (accid === "ff") midi -= 2; 3187 return midi; 3188}; 3189const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 3190const getNoteName = (midiNote) => { 3191 const octave = Math.floor(midiNote / 12) - 1; 3192 const name = noteNames[midiNote % 12]; 3193 return `${name}${octave}`; 3194}; 3195const GM_PERCUSSION_MAP = { 3196 35: "Acoustic Bass Drum", 3197 36: "Bass Drum 1", 3198 37: "Side Stick", 3199 38: "Acoustic Snare", 3200 39: "Hand Clap", 3201 40: "Electric Snare", 3202 41: "Low Floor Tom", 3203 42: "Closed Hi Hat", 3204 43: "High Floor Tom", 3205 44: "Pedal Hi-Hat", 3206 45: "Low Tom", 3207 46: "Open Hi-Hat", 3208 47: "Low-Mid Tom", 3209 48: "Hi-Mid Tom", 3210 49: "Crash Cymbal 1", 3211 50: "High Tom", 3212 51: "Ride Cymbal 1", 3213 52: "Chinese Cymbal", 3214 53: "Ride Bell", 3215 54: "Tambourine", 3216 55: "Splash Cymbal", 3217 56: "Cowbell", 3218 57: "Crash Cymbal 2", 3219 58: "Vibraslap", 3220 59: "Ride Cymbal 2", 3221 60: "Hi Bongo", 3222 61: "Low Bongo", 3223 62: "Mute Hi Conga", 3224 63: "Open Hi Conga", 3225 64: "Low Conga", 3226 65: "High Timbale", 3227 66: "Low Timbale", 3228 67: "High Agogo", 3229 68: "Low Agogo", 3230 69: "Cabasa", 3231 70: "Maracas", 3232 71: "Short Whistle", 3233 72: "Long Whistle", 3234 73: "Short Guiro", 3235 74: "Long Guiro", 3236 75: "Claves", 3237 76: "Hi Wood Block", 3238 77: "Low Wood Block", 3239 78: "Mute Cuica", 3240 79: "Open Cuica", 3241 80: "Mute Triangle", 3242 81: "Open Triangle" 3243}; 3244const getBeats = (dur, dots = null) => { 3245 const val = parseInt(dur || "4", 10); 3246 let base = isNaN(val) ? 1 : 4 / val; 3247 if (dots === "1") base *= 1.5; 3248 return base; 3249}; 3250const getStartTimeInLayer = (layer, targetEl) => { 3251 let eventEl = targetEl; 3252 if (targetEl.parentElement && targetEl.parentElement.tagName === "chord") { 3253 eventEl = targetEl.parentElement; 3254 }
3255 let elapsed = 0; 3256 const children = Array.from(layer.children); 3257 for (const child of children) { 3258 if (child === eventEl) { 3259 break; 3260 } 3261 if (child.tagName === "note" || child.tagName === "chord" || child.tagName === "rest" || child.tagName === "mRest") { 3262 const durAttr = child.getAttribute("dur") || "4"; 3263 const dotsAttr = child.getAttribute("dots") || null; 3264 elapsed += getBeats(durAttr, dotsAttr); 3265 } 3266 } 3267 return elapsed; 3268}; 3269const ScoreEditorCanvas = ({ 3270 svg, 3271 currentTool, 3272 zoom, 3273 mei, 3274 isPlaying, 3275 engine, 3276 currentVoice, 3277 onElementClick, 3278 onElementRightClick, 3279 onElementDrag, 3280 onMouseUp, 3281 currentAccidental, 3282 isPercussion, 3283 entryMode = "note", 3284 isDotted = false, 3285 dotsCount = 0, 3286 isTriplet = false, 3287 currentArticulation = null, 3288 selectedNoteId = null, 3289 showGrid = true 3290}) => { 3291 let editorContext = null; 3292 try { 3293 editorContext = useEditor(); 3294 } catch (e) { 3295 } 3296 const setHint = (editorContext == null ? void 0 : editorContext.setHint) || (() => { 3297 }); 3298 const playbackMode = (editorContext == null ? void 0 : editorContext.playbackMode) || "song"; 3299 const playlistClips = (editorContext == null ? void 0 : editorContext.playlistClips) || []; 3300 const activePatternId = (editorContext == null ? void 0 : editorContext.activePatternId) || null; 3301 const activePattern = (editorContext == null ? void 0 : editorContext.activePattern) || null; 3302 const currentTime = (editorContext == null ? void 0 : editorContext.currentTime) || 0; 3303 const containerRef = reactExports.useRef(null); 3304 const playheadRef = reactExports.useRef(null); 3305 const currentTimeRef = reactExports.useRef(currentTime); 3306 reactExports.useEffect(() => { 3307 currentTimeRef.current = currentTime; 3308 }, [currentTime]); 3309 const dragTargetRef = reactExports.useRef(null); 3310 const dragStartPosRef = reactExports.useRef(null); 3311 const [ghostPos, setGhostPos] = reactExports.useState(null); 3312 const [activeGrid, setActiveGrid] = reactExports.useState([]); 3313 const beatsPerMeasure = React.useMemo(() => { 3314 if (!mei) return 4; 3315 try { 3316 const parser = new DOMParser(); 3317 const doc = parser.parseFromString(mei, "application/xml"); 3318 const scoreDef = doc.querySelector("scoreDef"); 3319 let count = 4; 3320 let unit = 4; 3321 if (scoreDef) { 3322 const meterCount = scoreDef.getAttribute("meter.count"); 3323 const meterUnit = scoreDef.getAttribute("meter.unit"); 3324 if (meterCount) count = parseInt(meterCount, 10) || 4; 3325 if (meterUnit) unit = parseInt(meterUnit, 10) || 4; 3326 } else { 3327 const staffDef = doc.querySelector("staffDef"); 3328 if (staffDef) { 3329 const meterCount = staffDef.getAttribute("meter.count"); 3330 const meterUnit = staffDef.getAttribute("meter.unit"); 3331 if (meterCount) count = parseInt(meterCount, 10) || 4; 3332 if (meterUnit) unit = parseInt(meterUnit, 10) || 4; 3333 } 3334 } 3335 return count * (4 / unit); 3336 } catch (e) { 3337 console.error("Failed to parse meter from MEI", e); 3338 } 3339 return 4; 3340 }, [mei]); 3341 reactExports.useEffect(() => { 3342 if (!containerRef.current || !svg) return; 3343 const elements = containerRef.current.querySelectorAll(".selected-note"); 3344 elements.forEach((el) => { 3345 el.classList.remove("selected-note"); 3346 const paths = el.querySelectorAll("path, ellipse, line, rect, use, polygon"); 3347 paths.forEach((p) => p.removeAttribute("fill")); 3348 }); 3349 if (selectedNoteId) { 3350 const escapedId = selectedNoteId.replace(/(:|\.|\[|\]|,|=|@)/g, "\\$1"); 3351 const selectedEl = containerRef.current.querySelector(`[id="${escapedId}"]`) || containerRef.current.querySelector(`[id="${selectedNoteId}"]`); 3352 if (selectedEl) { 3353 selectedEl.classList.add("selected-note"); 3354 const paths = selectedEl.querySelectorAll("path, ellipse, line, rect, use, polygon"); 3355 paths.forEach((p) => p.setAttribute("fill", "#4169E1")); 3356 } 3357 } 3358 }, [svg, selectedNoteId]); 3359 const meiDocRef = reactExports.useRef(null); 3360 reactExports.useEffect(() => { 3361 if (mei) { 3362 try { 3363 const parser = new DOMParser(); 3364 meiDocRef.current = parser.parseFromString(mei, "text/xml"); 3365 } catch (e) { 3366 console.error("Failed to parse MEI XML:", e); 3367 meiDocRef.current = null; 3368 } 3369 } else { 3370 meiDocRef.current = null; 3371 } 3372 return () => { 3373 setHint(null); 3374 }; 3375 }, [mei, setHint]);
3376 const computeSnap = (clientX, measureNode, gridStep) => { 3377 const rect = measureNode.getBoundingClientRect(); 3378 let startX = rect.left + 50; 3379 const elements = Array.from(measureNode.querySelectorAll(".note, .rest, .mRest, .chord")); 3380 if (elements.length > 0) { 3381 let minLeft = Infinity; 3382 elements.forEach((el) => { 3383 const elRect = el.getBoundingClientRect(); 3384 if (elRect.left < minLeft && elRect.width > 0) minLeft = elRect.left + elRect.width / 2; 3385 }); 3386 if (minLeft !== Infinity) startX = minLeft; 3387 } 3388 const endX = rect.right - 10; 3389 const usableWidth = endX - startX; 3390 let beat = 0; 3391 if (usableWidth > 0 && clientX > startX) { 3392 beat = (clientX - startX) / usableWidth * beatsPerMeasure; 3393 } 3394 const snappedBeat = Math.round(beat / gridStep) * gridStep; 3395 const clampedBeat = Math.max(0, Math.min(beatsPerMeasure - gridStep, snappedBeat)); 3396 const snappedX = startX + clampedBeat / beatsPerMeasure * usableWidth; 3397 const gridXs = []; 3398 if (showGrid) { 3399 for (let b = 0; b <= beatsPerMeasure; b += gridStep) { 3400 if (b + gridStep <= beatsPerMeasure || b === beatsPerMeasure) { 3401 gridXs.push(startX + b / beatsPerMeasure * usableWidth); 3402 } 3403 } 3404 } 3405 return { clampedBeat, snappedX, gridXs, rect }; 3406 }; 3407 reactExports.useEffect(() => { 3408 if (!containerRef.current) return; 3409 const svgElement = containerRef.current.querySelector("svg"); 3410 if (!svgElement) return; 3411 const tiesAndSlurs = Array.from(svgElement.querySelectorAll("g.tie, g.slur")); 3412 tiesAndSlurs.forEach((el) => { 3413 const parent = el.parentNode; 3414 if (parent) { 3415 parent.insertBefore(el, parent.firstChild); 3416 } 3417 }); 3418 const style = document.getElementById("score-editor-styles") || document.createElement("style"); 3419 style.id = "score-editor-styles"; 3420 let voiceStyle = ""; 3421 if (currentVoice !== "All" && currentVoice !== void 0) { 3422 const layerSelector = currentVoice === 1 ? 'g.layer:not([n="1"])[n]' : `g.layer:not([n="${currentVoice}"])`; 3423 voiceStyle = ` 3424 .score-editor-canvas-container svg ${layerSelector} .note, 3425 .score-editor-canvas-container svg ${layerSelector} .rest, 3426 .score-editor-canvas-container svg ${layerSelector} .mRest, 3427 .score-editor-canvas-container svg ${layerSelector} .chord { 3428 opacity: 0.25; 3429 pointer-events: none; 3430 } 3431 `; 3432 } 3433 style.innerHTML = ` 3434 .score-editor-canvas-container svg { width: 100%; height: auto; display: block; } 3435 .score-editor-canvas-container svg g.measure, .score-editor-canvas-container svg g.staff, .score-editor-canvas-container svg g.layer { pointer-events: bounding-box; cursor: inherit; } 3436 .score-editor-canvas-container svg .bounding-box { pointer-events: all; fill: transparent; cursor: inherit; } 3437 ${voiceStyle} 3438 `; 3439 if (!document.getElementById("score-editor-styles")) { 3440 document.head.appendChild(style); 3441 } 3442 if (currentTool === "erase") { 3443 svgElement.style.cursor = "crosshair"; 3444 } else if (currentTool.startsWith("note_")) { 3445 svgElement.style.cursor = "none"; 3446 } else { 3447 svgElement.style.cursor = "default"; 3448 } 3449 }, [svg, currentTool, currentVoice]); 3450 reactExports.useEffect(() => { 3451 if (!isPlaying || !engine || !containerRef.current) return; 3452 let animationFrameId; 3453 const updatePlayhead = () => { 3454 var _a, _b, _c; 3455 try { 3456 const timeSeconds = engine.isPlaying() ? engine.getCurrentTime() : currentTimeRef.current; 3457 if (playheadRef.current && containerRef.current) { 3458 const ticks = engine.secondsToTicks(timeSeconds); 3459 let showPlayhead = false; 3460 let targetTicks = 0; 3461 let patternLength = 0; 3462 if (activePattern) { 3463 const maxNoteTick = activePattern.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0); 3464 const ppq2 = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96; 3465 const ticksPerBar = ppq2 * 4; 3466 patternLength = activePattern.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar); 3467 } 3468 if (playbackMode === "pat") { 3469 showPlayhead = true; 3470 targetTicks = patternLength > 0 ? ticks % patternLength : ticks; 3471 } else { 3472 const playingClip = playlistClips.find( 3473 (clip) => clip.patternId === activePatternId && !clip.isMuted && ticks >= clip.startTick && ticks < clip.startTick + clip.durationTicks 3474 ); 3475 if (playingClip) { 3476 showPlayhead = true; 3477 targetTicks = patternLength > 0 ? (ticks - playingClip.startTick + playingClip.offsetTicks) % patternLength : ticks - playingClip.startTick + playingClip.offsetTicks; 3478 } 3479 } 3480 if (!showPlayhead) { 3481 playheadRef.current.style.display = "none"; 3482 animationFrameId = requestAnimationFrame(updatePlayhead); 3483 return; 3484 } 3485 const timeSignatures = engine.midi.header.timeSignatures; 3486 const ppq = engine.midi.header.ppq; 3487 let measureCount = 0; 3488 let lastTick = 0; 3489 let num = 4, den = 4; 3490 for (const ts of timeSignatures) { 3491 if (ts.ticks > targetTicks) break; 3492 const ticksDiff2 = ts.ticks - lastTick;
3493 const ticksPerMeasure2 = ppq * (4 / den) * num; 3494 if (ticksPerMeasure2 > 0) { 3495 measureCount += Math.floor(ticksDiff2 / ticksPerMeasure2); 3496 } 3497 num = ts.timeSignature[0]; 3498 den = ts.timeSignature[1]; 3499 lastTick = ts.ticks; 3500 } 3501 const ticksDiff = targetTicks - lastTick; 3502 const ticksPerMeasure = ppq * (4 / den) * num; 3503 let measureProgress = 0; 3504 if (ticksPerMeasure > 0) { 3505 measureCount += Math.floor(ticksDiff / ticksPerMeasure); 3506 measureProgress = ticksDiff % ticksPerMeasure / ticksPerMeasure; 3507 } 3508 const measureNum = measureCount + 1; 3509 const measureNode = containerRef.current.querySelector(`g.measure[n="${measureNum}"]`); 3510 if (measureNode) { 3511 const rect = measureNode.getBoundingClientRect(); 3512 const containerRect = containerRef.current.getBoundingClientRect(); 3513 let startX = rect.left + 50; 3514 const elements = Array.from(measureNode.querySelectorAll(".note, .rest, .mRest, .chord")); 3515 if (elements.length > 0) { 3516 let minLeft = Infinity; 3517 elements.forEach((el) => { 3518 const elRect = el.getBoundingClientRect(); 3519 if (elRect.left < minLeft && elRect.width > 0) minLeft = elRect.left + elRect.width / 2; 3520 }); 3521 if (minLeft !== Infinity) startX = minLeft; 3522 } 3523 const endX = rect.right - 10; 3524 const playheadX = startX + measureProgress * (endX - startX); 3525 const localX = (playheadX - containerRect.left) / zoom; 3526 const localTop = (rect.top - containerRect.top) / zoom; 3527 const localBottom = (rect.bottom - containerRect.top) / zoom; 3528 playheadRef.current.style.display = "block"; 3529 playheadRef.current.style.left = `${localX}px`; 3530 playheadRef.current.style.top = `${localTop}px`; 3531 playheadRef.current.style.height = `${localBottom - localTop}px`; 3532 const scrollContainer = (_c = containerRef.current.parentElement) == null ? void 0 : _c.parentElement; 3533 if (scrollContainer) { 3534 const systemNode = measureNode.closest("g.system"); 3535 const containerRect2 = scrollContainer.getBoundingClientRect(); 3536 if (systemNode) { 3537 const sysRect = systemNode.getBoundingClientRect(); 3538 const isSystemVisible = sysRect.top >= containerRect2.top + 20 && sysRect.bottom <= containerRect2.bottom - 20; 3539 if (!isSystemVisible) { 3540 const relativeTop = sysRect.top - containerRect2.top + scrollContainer.scrollTop; 3541 const targetTop = relativeTop - 40; 3542 scrollContainer.scrollTo({ 3543 top: Math.max(0, targetTop), 3544 behavior: "smooth" 3545 }); 3546 } 3547 } else { 3548 const relativeTop = rect.top - containerRect2.top + scrollContainer.scrollTop;
3549 const targetTop = relativeTop - scrollContainer.clientHeight / 2; 3550 scrollContainer.scrollTo({ 3551 top: Math.max(0, targetTop), 3552 behavior: "smooth" 3553 }); 3554 } 3555 } 3556 } else { 3557 playheadRef.current.style.display = "none"; 3558 } 3559 } 3560 } catch (e) { 3561 } 3562 animationFrameId = requestAnimationFrame(updatePlayhead); 3563 }; 3564 animationFrameId = requestAnimationFrame(updatePlayhead); 3565 return () => { 3566 cancelAnimationFrame(animationFrameId); 3567 if (playheadRef.current) { 3568 playheadRef.current.style.display = "none"; 3569 } 3570 }; 3571 }, [isPlaying, engine, zoom, playbackMode, playlistClips, activePatternId, activePattern]); 3572 const handleMouseDown = (e) => { 3573 const target = e.target; 3574 let gNode = target.closest("g[id]"); 3575 let elementId = gNode == null ? void 0 : gNode.getAttribute("id"); 3576 let className = (gNode == null ? void 0 : gNode.getAttribute("class")) || ""; 3577 if (!elementId && containerRef.current) { 3578 const measures = Array.from(containerRef.current.querySelectorAll("g.measure")); 3579 let closestMeasure = null; 3580 let minDistance = Infinity; 3581 for (const measure of measures) { 3582 const rect = measure.getBoundingClientRect(); 3583 const isWithinX = e.clientX >= rect.left && e.clientX <= rect.right; 3584 const isWithinY = e.clientY >= rect.top && e.clientY <= rect.bottom; 3585 if (isWithinX && isWithinY) { 3586 closestMeasure = measure; 3587 break; 3588 } 3589 const centerX = rect.left + rect.width / 2; 3590 const centerY = rect.top + rect.height / 2; 3591 const dist = Math.hypot(e.clientX - centerX, e.clientY - centerY); 3592 if (dist < minDistance) { 3593 minDistance = dist; 3594 closestMeasure = measure; 3595 } 3596 } 3597 if (closestMeasure) { 3598 gNode = closestMeasure; 3599 elementId = gNode.getAttribute("id"); 3600 className = gNode.getAttribute("class") || ""; 3601 } 3602 } 3603 if (!elementId) return; 3604 if (currentTool === "select" && className.includes("note")) { 3605 dragTargetRef.current = elementId; 3606 dragStartPosRef.current = { x: e.clientX, y: e.clientY }; 3607 gNode.style.opacity = "0.5"; 3608 } 3609 let yOffset = void 0; 3610 if (currentTool.startsWith("note_") && containerRef.current) { 3611 let staffNode = target.closest("g.staff"); 3612 if (!staffNode) { 3613 const measureNode = target.closest("g.measure"); 3614 if (measureNode) { 3615 staffNode = measureNode.querySelector("g.staff"); 3616 } 3617 } 3618 if (!staffNode) { 3619 const staves = Array.from(containerRef.current.querySelectorAll("g.staff")); 3620 if (staves.length > 0) { 3621 let closestStaff = staves[0]; 3622 let minDistance = Infinity; 3623 for (const staff of staves) { 3624 const rect = staff.getBoundingClientRect(); 3625 const staffCenter = rect.top + rect.height / 2; 3626 const dist = Math.abs(e.clientY - staffCenter); 3627 if (dist < minDistance) { 3628 minDistance = dist; 3629 closestStaff = staff; 3630 } 3631 } 3632 staffNode = closestStaff; 3633 } 3634 } 3635 if (staffNode) { 3636 yOffset = getSnappingOffset(e.clientY, staffNode); 3637 } 3638 } 3639 let snappedBeat = void 0; 3640 if (currentTool.startsWith("note_") && containerRef.current) { 3641 const measureNode = target.closest("g.measure") || (className.includes("measure") ? gNode : null); 3642 if (measureNode) { 3643 const durMap = { 3644 "note_whole": "1", 3645 "note_half": "2", 3646 "note_quarter": "4", 3647 "note_eighth": "8", 3648 "note_sixteenth": "16" 3649 }; 3650 const dur = durMap[currentTool] || "4"; 3651 let beatDur = getDurationInBeats(dur); 3652 if (isTriplet) { 3653 beatDur /= 1.5; 3654 } 3655 if (isDotted) { 3656 beatDur *= 1.5; 3657 } 3658 const gridStep = showGrid ? beatDur >= 1 ? 1 : beatDur : 1e-3; 3659 const snapInfo = computeSnap(e.clientX, measureNode, gridStep); 3660 snappedBeat = snapInfo.clampedBeat; 3661 } 3662 } 3663 onElementClick(elementId, e, yOffset, snappedBeat); 3664 }; 3665 const handleContextMenu = (e) => { 3666 e.preventDefault(); 3667 const target = e.target; 3668 const gNode = target.closest("g[id]"); 3669 const elementId = gNode == null ? void 0 : gNode.getAttribute("id"); 3670 if (!elementId) return; 3671 const className = (gNode == null ? void 0 : gNode.getAttribute("class")) || ""; 3672 onElementRightClick(elementId, className, e); 3673 }; 3674 const handleMouseMove = (e) => { 3675 if (dragTargetRef.current && dragStartPosRef.current) ; 3676 let hintSet = false;
3677 if (containerRef.current) { 3678 const target = e.target; 3679 const gNode = target.closest("g[id]"); 3680 const elementId = gNode == null ? void 0 : gNode.getAttribute("id"); 3681 if (elementId && meiDocRef.current) { 3682 const getXmlElById = (doc, id) => { 3683 return doc.querySelector(`[id="${id}"]`) || doc.querySelector(`[xml\\:id="${id}"]`) || doc.querySelector(`[*|id="${id}"]`) || Array.from(doc.getElementsByTagName("*")).find((el) => el.getAttribute("xml:id") === id) || null; 3684 }; 3685 const noteEl = getXmlElById(meiDocRef.current, elementId); 3686 if (noteEl && noteEl.tagName === "note") { 3687 const pname = noteEl.getAttribute("pname") || "c"; 3688 const oct = parseInt(noteEl.getAttribute("oct") || "4", 10); 3689 const accid = noteEl.getAttribute("accid") || void 0; 3690 const midiVal = getMidiFromMEI(pname, oct, accid); 3691 const noteNameStr = isPercussion ? GM_PERCUSSION_MAP[midiVal] || `Drum ${midiVal}` : getNoteName(midiVal); 3692 const layer = noteEl.closest("layer"); 3693 const measure = noteEl.closest("measure"); 3694 const measureNum = measure ? parseInt(measure.getAttribute("n") || "1", 10) : 1; 3695 let elapsedBeats = 0; 3696 if (layer) { 3697 let eventEl = noteEl; 3698 if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") { 3699 eventEl = noteEl.parentElement; 3700 } 3701 elapsedBeats = getStartTimeInLayer(layer, eventEl); 3702 } 3703 const durAttr = noteEl.getAttribute("dur") || "4"; 3704 const dotsAttr = noteEl.getAttribute("dots") || null; 3705 const durationInBeats = getBeats(durAttr, dotsAttr); 3706 const bar = measureNum; 3707 const step = Math.floor(elapsedBeats * 4) + 1; 3708 const tick = Math.floor(elapsedBeats * 4 % 1 * 24); 3709 const posStr = `${bar}:${String(step).padStart(2, "0")}:${String(tick).padStart(2, "0")}`; 3710 const durBar = Math.floor(durationInBeats / 4); 3711 const durStep = Math.floor(durationInBeats % 4 * 4); 3712 const durTick = Math.floor(durationInBeats % 4 * 4 % 1 * 24); 3713 const durStr = `${durBar}:${String(durStep).padStart(2, "0")}:${String(durTick).padStart(2, "0")}`; 3714 setHint({ 3715 label: isPercussion ? `Percussion Note` : `Note: ${noteNameStr}`, 3716 positionText: `${posStr} for ${durStr}`, 3717 noteText: `${noteNameStr} / ${midiVal}`, 3718 iconType: "note" 3719 }); 3720 hintSet = true; 3721 } 3722 } 3723 if (!hintSet) { 3724 const measureNode = target.closest("g.measure"); 3725 let staffNode = target.closest("g.staff"); 3726 if (!staffNode && measureNode) { 3727 staffNode = measureNode.querySelector("g.staff"); 3728 } 3729 if (measureNode && staffNode) { 3730 const offset = getSnappingOffset(e.clientY, staffNode); 3731 const measureNum = parseInt(measureNode.getAttribute("n") || "1", 10); 3732 const durMap = { 3733 "note_whole": "1", 3734 "note_half": "2", 3735 "note_quarter": "4", 3736 "note_eighth": "8", 3737 "note_sixteenth": "16", 3738 "rest_whole": "1", 3739 "rest_half": "2", 3740 "rest_quarter": "4", 3741 "rest_eighth": "8", 3742 "rest_sixteenth": "16" 3743 }; 3744 const dur = durMap[currentTool] || "4"; 3745 let beatDur = getDurationInBeats(dur); 3746 if (isTriplet) { 3747 beatDur /= 1.5; 3748 } 3749 if (isDotted) { 3750 beatDur *= 1.5; 3751 } 3752 const gridStep = showGrid ? beatDur >= 1 ? 1 : beatDur : 1e-3; 3753 const snapInfo = computeSnap(e.clientX, measureNode, gridStep); 3754 let midiVal = 60; 3755 if (isPercussion) { 3756 if (currentAccidental && ["kick", "snare", "hihat", "cymbal"].includes(currentAccidental)) { 3757 if (currentAccidental === "kick") midiVal = 36; 3758 else if (currentAccidental === "snare") midiVal = 38; 3759 else if (currentAccidental === "hihat") midiVal = 42; 3760 else if (currentAccidental === "cymbal") midiVal = 49; 3761 } else { 3762 midiVal = 38; 3763 } 3764 } else { 3765 const step2 = 38 - offset; 3766 const pitchMap = ["c", "d", "e", "f", "g", "a", "b"]; 3767 const pitch = pitchMap[(step2 % 7 + 7) % 7]; 3768 const oct = Math.floor(step2 / 7); 3769 const accid = currentAccidental === "sharp" ? "s" : currentAccidental === "flat" ? "f" : currentAccidental === "natural" ? "n" : void 0; 3770 midiVal = getMidiFromMEI(pitch, oct, accid); 3771 } 3772 const noteNameStr = isPercussion ? GM_PERCUSSION_MAP[midiVal] || `Drum ${midiVal}` : getNoteName(midiVal); 3773 const bar = measureNum; 3774 const step = Math.floor(snapInfo.clampedBeat * 4) + 1; 3775 const tick = Math.floor(snapInfo.clampedBeat * 4 % 1 * 24); 3776 const posStr = `${bar}:${String(step).padStart(2, "0")}
3776:${String(tick).padStart(2, "0")}`; 3777 setHint({ 3778 label: `Score: ${currentTool.startsWith("rest_") ? "Place Rest" : currentTool.startsWith("note_") ? "Place Note" : currentTool}`, 3779 positionText: posStr, 3780 noteText: `${noteNameStr} / ${midiVal}`, 3781 iconType: currentTool 3782 }); 3783 hintSet = true; 3784 } 3785 } 3786 } 3787 if (!hintSet) { 3788 setHint(null); 3789 } 3790 if (currentTool.startsWith("note_") && containerRef.current) { 3791 const isOverPaper = containerRef.current.contains(e.target); 3792 if (isOverPaper) { 3793 containerRef.current.style.cursor = "none"; 3794 const target = e.target; 3795 let staffNode = target.closest("g.staff"); 3796 if (!staffNode) { 3797 const measureNode = target.closest("g.measure"); 3798 if (measureNode) { 3799 staffNode = measureNode.querySelector("g.staff"); 3800 } 3801 } 3802 if (!staffNode) { 3803 const staves = Array.from(containerRef.current.querySelectorAll("g.staff")); 3804 if (staves.length > 0) { 3805 let closestStaff = staves[0]; 3806 let minDistance = Infinity; 3807 for (const staff of staves) { 3808 const rect = staff.getBoundingClientRect(); 3809 const staffCenter = rect.top + rect.height / 2; 3810 const dist = Math.abs(e.clientY - staffCenter); 3811 if (dist < minDistance) { 3812 minDistance = dist; 3813 closestStaff = staff; 3814 } 3815 } 3816 staffNode = closestStaff; 3817 } 3818 } 3819 const containerRect = containerRef.current.getBoundingClientRect(); 3820 if (staffNode) { 3821 const offset = getSnappingOffset(e.clientY, staffNode); 3822 const staffLines = Array.from(staffNode.querySelectorAll(":scope > path")); 3823 const rects = staffLines.map((line) => line.getBoundingClientRect()).sort((a, b) => a.top - b.top); 3824 if (rects.length === 5) { 3825 const topLineY = rects[0].top; 3826 const bottomLineY = rects[4].top; 3827 const avgGap = (bottomLineY - topLineY) / 4; 3828 const stepSize = avgGap / 2; 3829 const snappedY = topLineY + offset * stepSize; 3830 let snappedX = e.clientX; 3831 const measureNode = staffNode.closest("g.measure"); 3832 if (measureNode) { 3833 const durMap = { 3834 "note_whole": "1", 3835 "note_half": "2", 3836 "note_quarter": "4", 3837 "note_eighth": "8", 3838 "note_sixteenth": "16" 3839 }; 3840 const dur = durMap[currentTool] || "4"; 3841 let beatDur = getDurationInBeats(dur); 3842 if (isTriplet) { 3843 beatDur /= 1.5; 3844 } 3845 if (isDotted) { 3846 beatDur *= 1.5; 3847 } 3848 const gridStep = showGrid ? beatDur >= 1 ? 1 : beatDur : 1e-3; 3849 const snapInfo = computeSnap(e.clientX, measureNode, gridStep); 3850 snappedX = snapInfo.snappedX; 3851 const localGrid = snapInfo.gridXs.map((gx) => ({ 3852 x: (gx - containerRect.left) / zoom, 3853 top: (snapInfo.rect.top - containerRect.top) / zoom, 3854 bottom: (snapInfo.rect.bottom - containerRect.top) / zoom 3855 }));
3856 setActiveGrid(localGrid); 3857 } else { 3858 setActiveGrid([]); 3859 } 3860 const localX2 = (snappedX - containerRect.left) / zoom; 3861 const localY2 = (snappedY - containerRect.top) / zoom; 3862 setGhostPos({ x: localX2, y: localY2, visible: true, offset }); 3863 return; 3864 } 3865 } 3866 const localX = (e.clientX - containerRect.left) / zoom; 3867 const localY = (e.clientY - containerRect.top) / zoom; 3868 setGhostPos({ x: localX, y: localY, visible: true, offset: 0 }); 3869 setActiveGrid([]); 3870 } else { 3871 containerRef.current.style.cursor = "default"; 3872 setGhostPos(null); 3873 setActiveGrid([]); 3874 } 3875 } else { 3876 if (containerRef.current) { 3877 containerRef.current.style.cursor = currentTool === "erase" ? "crosshair" : "default"; 3878 } 3879 setGhostPos(null); 3880 setActiveGrid([]); 3881 } 3882 }; 3883 const handleMouseUp = (e) => { 3884 if (onMouseUp) onMouseUp(); 3885 if (dragTargetRef.current && dragStartPosRef.current) { 3886 const dy = e.clientY - dragStartPosRef.current.y; 3887 if (Math.abs(dy) > 10) { 3888 onElementDrag(dragTargetRef.current, dy, e.clientY); 3889 } 3890 if (containerRef.current) { 3891 const gNode = containerRef.current.querySelector(`g[id="${dragTargetRef.current}"]`); 3892 if (gNode) gNode.style.opacity = "1"; 3893 } 3894 dragTargetRef.current = null; 3895 dragStartPosRef.current = null; 3896 } 3897 }; 3898 const handleMouseLeave = (e) => { 3899 handleMouseUp(e); 3900 setGhostPos(null); 3901 setActiveGrid([]); 3902 setHint(null); 3903 if (containerRef.current) { 3904 containerRef.current.style.cursor = "default"; 3905 } 3906 }; 3907 const renderGhostSVG = (tool) => { 3908 const color = "#4169E1"; 3909 const offset = (ghostPos == null ? void 0 : ghostPos.offset) || 0; 3910 const renderLedgerLines = () => { 3911 const lines = []; 3912 if (offset <= -2) { 3913 for (let L = -2; L >= offset; L -= 2) { 3914 const cy = (L - offset) * 4.5; 3915 lines.push(
3916 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-12", y1: cy, x2: "12", y2: cy, stroke: color, strokeWidth: "1.5" }, `ledger-${L}`) 3917 ); 3918 } 3919 } else if (offset >= 10) { 3920 for (let L = 10; L <= offset; L += 2) { 3921 const cy = (L - offset) * 4.5; 3922 lines.push( 3923 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-12", y1: cy, x2: "12", y2: cy, stroke: color, strokeWidth: "1.5" }, `ledger-${L}`) 3924 ); 3925 } 3926 } 3927 return lines; 3928 }; 3929 const isRest = entryMode === "rest"; 3930 const renderDot = () => { 3931 const numDots = dotsCount > 0 ? dotsCount : isDotted ? 1 : 0; 3932 if (numDots === 0) return null; 3933 if (numDots === 2) { 3934 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 3935 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "0", r: "1.5", fill: color }), 3936 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "16", cy: "0", r: "1.5", fill: color }) 3937 ] }); 3938 } 3939 return /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "0", r: "1.5", fill: color }); 3940 }; 3941 const renderArticulation = () => { 3942 if (!currentArticulation || isRest) return null; 3943 const yPos = 10; 3944 switch (currentArticulation) { 3945 case "staccato": 3946 return /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "0", cy: yPos, r: "1.5", fill: color }); 3947 case "accent": 3948 return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: `M -4 ${yPos - 2} L 4 ${yPos} L -4 ${yPos + 2}`, fill: "none", stroke: color, strokeWidth: "1.5" }); 3949 case "tenuto": 3950 return /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-5", y1: yPos, x2: "5", y2: yPos, stroke: color, strokeWidth: "1.5" }); 3951 case "fermata": 3952 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(0, ${yPos - 2})`, children: [
3953 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -6 0 Q 0 -8 6 0", fill: "none", stroke: color, strokeWidth: "1.5" }), 3954 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "0", cy: "-2", r: "1", fill: color }) 3955 ] }); 3956 default: 3957 return null; 3958 } 3959 }; 3960 if (isRest) { 3961 return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "60", viewBox: "-12 -30 24 60", style: { display: "block" }, children: [ 3962 renderLedgerLines(), 3963 tool === "note_whole" && /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "-5", y: "-5", width: "10", height: "6", fill: color }), 3964 tool === "note_half" && /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "-5", y: "-1", width: "10", height: "6", fill: color }), 3965 tool === "note_quarter" && /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -3 -8 L 1 -2 L -2 4 L 2 10", stroke: color, strokeWidth: "2", fill: "none" }), 3966 tool === "note_eighth" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
3967 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "-2", cy: "-4", r: "2", fill: color }), 3968 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -2 -4 Q 2 -2 -2 6 L 1 12", stroke: color, strokeWidth: "1.5", fill: "none" }) 3969 ] }), 3970 tool === "note_sixteenth" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 3971 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "-2", cy: "-8", r: "2", fill: color }), 3972 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -2 -8 Q 2 -6 -2 2", stroke: color, strokeWidth: "1.5", fill: "none" }), 3973 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "-3", cy: "-2", r: "2", fill: color }), 3974 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -3 -2 Q 1 0 -3 8 L 0 14", stroke: color, strokeWidth: "1.5", fill: "none" }) 3975 ] }), 3976 renderDot() 3977 ] }); 3978 } 3979 if (tool === "note_whole") { 3980 return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "60", viewBox: "-12 -30 24 60", style: { display: "block" }, children: [ 3981 renderLedgerLines(),
3982 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "0", cy: "0", rx: "8", ry: "5", transform: "rotate(-20)", stroke: color, strokeWidth: "2", fill: "none" }), 3983 renderDot(), 3984 renderArticulation() 3985 ] }); 3986 } 3987 const isHollow = tool === "note_half"; 3988 return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "60", viewBox: "-12 -30 24 60", style: { display: "block" }, children: [ 3989 renderLedgerLines(), 3990 /* @__PURE__ */ jsxRuntimeExports.jsx( 3991 "ellipse", 3992 { 3993 cx: "0", 3994 cy: "0", 3995 rx: "7", 3996 ry: "4.5", 3997 transform: "rotate(-20)", 3998 stroke: color, 3999 strokeWidth: isHollow ? 1.5 : 0, 4000 fill: isHollow ? "none" : color 4001 } 4002 ), 4003 /* @__PURE__ */ jsxRuntimeExports.jsx( 4004 "line", 4005 { 4006 x1: "6", 4007 y1: "0", 4008 x2: "6", 4009 y2: "-22", 4010 stroke: color, 4011 strokeWidth: "1.5" 4012 } 4013 ), 4014 tool === "note_eighth" && /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 6 -22 Q 12 -15 11 -8 Q 9 -12 6 -14", fill: color }), 4015 tool === "note_sixteenth" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
4016 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 6 -22 Q 12 -15 11 -8 Q 9 -12 6 -14", fill: color }), 4017 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 6 -16 Q 12 -9 11 -2 Q 9 -6 6 -8", fill: color }) 4018 ] }), 4019 renderDot(), 4020 renderArticulation() 4021 ] }); 4022 }; 4023 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4024 "div", 4025 { 4026 ref: containerRef, 4027 className: "score-editor-canvas-container relative bg-[#FFFDF6] shadow-2xl rounded-sm border border-zinc-200", 4028 style: { minWidth: "800px", minHeight: "1132px" }, 4029 onMouseDown: handleMouseDown, 4030 onContextMenu: handleContextMenu, 4031 onMouseMove: handleMouseMove, 4032 onMouseUp: handleMouseUp, 4033 onMouseLeave: handleMouseLeave, 4034 children: [ 4035 /* @__PURE__ */ jsxRuntimeExports.jsx( 4036 "div", 4037 { 4038 dangerouslySetInnerHTML: { __html: svg || '<div class="text-zinc-400 p-8 text-center">Loading Engine...</div>' } 4039 } 4040 ), 4041 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: playheadRef, style: { 4042 position: "absolute", 4043 display: "none", 4044 width: "2px", 4045 backgroundColor: "rgba(255, 69, 58, 0.8)", 4046 boxShadow: "0 0 4px rgba(255, 69, 58, 0.5)", 4047 pointerEvents: "none", 4048 zIndex: 999997, 4049 transition: "none" 4050 } }),
4051 ghostPos && currentTool.startsWith("note_") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { 4052 position: "absolute", 4053 left: ghostPos.x, 4054 top: ghostPos.y, 4055 transform: "translate(-50%, -50%)", 4056 pointerEvents: "none", 4057 opacity: ghostPos.visible ? 0.6 : 0, 4058 zIndex: 999999 4059 }, children: renderGhostSVG(currentTool) }), 4060 currentTool.startsWith("note_") && activeGrid.map((gridLine, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { 4061 position: "absolute", 4062 left: gridLine.x, 4063 top: gridLine.top, 4064 height: gridLine.bottom - gridLine.top, 4065 borderLeft: "2px dashed rgba(65, 105, 225, 0.4)", 4066 pointerEvents: "none", 4067 zIndex: 999998 4068 } }, `grid-${idx}`)) 4069 ] 4070 } 4071 ); 4072}; 4073const getDurationInBeats = (verovioDur) => { 4074 switch (verovioDur) { 4075 case "1": 4076 return 4; 4077 case "2": 4078 return 2; 4079 case "4": 4080 return 1; 4081 case "8": 4082 return 0.5; 4083 case "16": 4084 return 0.25; 4085 default: 4086 return 1; 4087 } 4088}; 4089export { 4090 EditorProvider as E, 4091 ScoreEditorCanvas as S, 4092 useScoreEditorEngine as a, 4093 getMidiFromMEI$1 as b, 4094 convertMidiToMEI as c, 4095 getMeiDurAttr as d, 4096 getBeats$1 as e, 4097 getDurationInBeats$1 as f, 4098 getMidiNotesFromBase64 as g, 4099 ScoreSyncManager as h, 4100 useEditor as u 4101};
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.