| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Modern Music Editor</title> |
| <script src="https://www.verovio.org/javascript/latest/verovio-toolkit.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/midi.js/0.3.0/midi.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> |
| <style> |
| :root { |
| --primary: #1a1a1a; |
| --secondary: #2d2d2d; |
| --accent: #00ffdd; |
| --text: #ffffff; |
| } |
| |
| body { |
| margin: 0; |
| padding: 20px; |
| background-color: var(--primary); |
| color: var(--text); |
| font-family: 'Arial', sans-serif; |
| min-height: 100vh; |
| } |
| |
| .toolbar { |
| background: var(--secondary); |
| padding: 15px; |
| border-radius: 10px; |
| margin-bottom: 20px; |
| display: flex; |
| gap: 15px; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| flex-wrap: wrap; |
| } |
| |
| button { |
| background: linear-gradient(145deg, var(--accent), #00ccbb); |
| color: var(--primary); |
| border: none; |
| padding: 10px 20px; |
| border-radius: 5px; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| font-weight: bold; |
| min-width: 120px; |
| } |
| |
| button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 2px 8px rgba(0, 255, 221, 0.4); |
| } |
| |
| input[type="file"] { |
| display: none; |
| } |
| |
| .editor-container { |
| background: var(--secondary); |
| border-radius: 10px; |
| padding: 20px; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| overflow: auto; |
| margin-bottom: 20px; |
| } |
| |
| #notation-container { |
| background: white; |
| border-radius: 5px; |
| min-height: 500px; |
| padding: 20px; |
| transition: transform 0.3s ease; |
| } |
| |
| .status-bar { |
| padding: 10px; |
| background: var(--secondary); |
| border-radius: 5px; |
| color: var(--accent); |
| position: fixed; |
| bottom: 20px; |
| left: 20px; |
| right: 20px; |
| } |
| |
| svg { |
| width: 100%; |
| height: auto; |
| } |
| |
| .controls { |
| display: flex; |
| gap: 15px; |
| align-items: center; |
| margin-left: auto; |
| } |
| |
| input[type="range"] { |
| width: 150px; |
| accent-color: var(--accent); |
| } |
| </style> |
| </head> |
| <body> |
| <div class="toolbar"> |
| <button onclick="document.getElementById('fileInput').click()">📁 Open MusicXML</button> |
| <button onclick="document.getElementById('midiInput').click()">🎹 Import MIDI</button> |
| <div class="controls"> |
| <button id="playButton" onclick="togglePlay()">▶ Play</button> |
| <button onclick="stopPlayback()">⏹ Stop</button> |
| <input type="range" id="zoom" min="10" max="200" value="100"> |
| <span id="zoomValue">100%</span> |
| </div> |
| <input type="file" id="fileInput" accept=".xml,.musicxml"> |
| <input type="file" id="midiInput" accept=".mid,.midi"> |
| </div> |
|
|
| <div class="editor-container"> |
| <div id="notation-container"></div> |
| </div> |
|
|
| <div class="status-bar" id="status"> |
| Ready |
| </div> |
|
|
| <script> |
| const vrvToolkit = new verovio.toolkit(); |
| let midiSequence = []; |
| let isPlaying = false; |
| let currentBPM = 120; |
| |
| // Initialize Verovio |
| vrvToolkit.setOptions({ |
| scale: 50, |
| pageMarginTop: 20, |
| pageMarginBottom: 20, |
| pageMarginLeft: 20, |
| pageMarginRight: 20, |
| spacingStaff: 1.2 |
| }); |
| |
| // Initialize Tone.js |
| const synth = new Tone.PolySynth(Tone.Synth, { |
| oscillator: { type: 'sine' }, |
| envelope: { |
| attack: 0.02, |
| decay: 0.1, |
| sustain: 0.3, |
| release: 0.4 |
| } |
| }).toDestination(); |
| |
| // Setup Transport |
| Tone.Transport.bpm.value = currentBPM; |
| |
| // File Handlers |
| document.getElementById('fileInput').addEventListener('change', function(e) { |
| const file = e.target.files[0]; |
| if (file) handleMusicXML(file); |
| }); |
| |
| document.getElementById('midiInput').addEventListener('change', function(e) { |
| const file = e.target.files[0]; |
| if (file) handleMIDI(file); |
| }); |
| |
| async function handleMusicXML(file) { |
| try { |
| const xmlData = await readFile(file); |
| vrvToolkit.loadData(xmlData); |
| renderNotation(); |
| updateStatus(`Loaded: ${file.name}`); |
| } catch (error) { |
| updateStatus(`Error loading MusicXML: ${error.message}`, true); |
| } |
| } |
| |
| async function handleMIDI(file) { |
| try { |
| const arrayBuffer = await readFile(file, 'arrayBuffer'); |
| MIDI.parse(arrayBuffer, function(midi) { |
| midiSequence = processMIDI(midi); |
| updateStatus(`Imported MIDI: ${file.name}`); |
| convertMIDIToMusicXML(midi); |
| }); |
| } catch (error) { |
| updateStatus(`Error loading MIDI: ${error.message}`, true); |
| } |
| } |
| |
| function processMIDI(midiData) { |
| const tracks = []; |
| midiData.tracks.forEach(track => { |
| const notes = []; |
| let currentTime = 0; |
| |
| track.forEach(event => { |
| currentTime += event.deltaTime; |
| |
| if (event.type === "channel" && event.subtype === "noteOn") { |
| notes.push({ |
| time: currentTime / midiData.header.ticksPerBeat, |
| note: Tone.Frequency(event.noteNumber, "midi").toNote(), |
| velocity: event.velocity / 127, |
| duration: (event.duration || 1) / midiData.header.ticksPerBeat |
| }); |
| } |
| }); |
| |
| tracks.push(notes); |
| }); |
| return tracks; |
| } |
| |
| function convertMIDIToMusicXML(midiData) { |
| // Basic conversion logic (simplified) |
| let xml = `<?xml version="1.0" encoding="UTF-8"?> |
| <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> |
| <score-partwise version="4.0"> |
| <part-list> |
| <score-part id="P1"> |
| <part-name>MIDI Import</part-name> |
| </score-part> |
| </part-list> |
| <part id="P1"> |
| <measure number="1"> |
| <attributes> |
| <divisions>${midiData.header.ticksPerBeat}</divisions> |
| <time> |
| <beats>4</beats> |
| <beat-type>4</beat-type> |
| </time> |
| <key> |
| <fifths>0</fifths> |
| </key> |
| <clef> |
| <sign>G</sign> |
| <line>2</line> |
| </clef> |
| </attributes>`; |
| |
| midiData.tracks[0].forEach(event => { |
| if (event.type === "channel" && event.subtype === "noteOn") { |
| xml += ` |
| <note> |
| <pitch> |
| <step>${Tone.Frequency(event.noteNumber, "midi").toNote().slice(0, -1)}</step> |
| <octave>${Tone.Frequency(event.noteNumber, "midi").toNote().slice(-1)}</octave> |
| </pitch> |
| <duration>${event.duration || 1}</duration> |
| <type>quarter</type> |
| </note>`; |
| } |
| }); |
| |
| xml += ` |
| </measure> |
| </part> |
| </score-partwise>`; |
| |
| vrvToolkit.loadData(xml); |
| renderNotation(); |
| } |
| |
| function togglePlay() { |
| if (!isPlaying) { |
| isPlaying = true; |
| document.getElementById('playButton').textContent = "⏸ Pause"; |
| |
| Tone.Transport.cancel(); |
| |
| midiSequence.forEach(track => { |
| track.forEach(({ time, note, velocity, duration }) => { |
| synth.triggerAttackRelease( |
| note, |
| duration, |
| `+${time}`, |
| velocity |
| ); |
| }); |
| }); |
| |
| Tone.Transport.start(); |
| updateStatus("Playing..."); |
| } else { |
| isPlaying = false; |
| document.getElementById('playButton').textContent = "▶ Play"; |
| Tone.Transport.pause(); |
| updateStatus("Paused"); |
| } |
| } |
| |
| function stopPlayback() { |
| isPlaying = false; |
| Tone.Transport.stop(); |
| synth.releaseAll(); |
| document.getElementById('playButton').textContent = "▶ Play"; |
| updateStatus("Stopped"); |
| } |
| |
| function renderNotation() { |
| const svg = vrvToolkit.renderToSVG(1); |
| document.getElementById('notation-container').innerHTML = svg; |
| } |
| |
| function updateStatus(message, isError = false) { |
| const statusBar = document.getElementById('status'); |
| statusBar.textContent = message; |
| statusBar.style.color = isError ? '#ff4444' : var('--accent'); |
| } |
| |
| // Utility Functions |
| function readFile(file, readAs = 'text') { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => resolve(reader.result); |
| reader.onerror = () => reject(new Error('Error reading file')); |
| |
| if (readAs === 'arrayBuffer') { |
| reader.readAsArrayBuffer(file); |
| } else { |
| reader.readAsText(file); |
| } |
| }); |
| } |
| |
| // Zoom Control |
| document.getElementById('zoom').addEventListener('input', function(e) { |
| const scaleValue = parseInt(e.target.value); |
| vrvToolkit.setOptions({ scale: scaleValue / 2 }); |
| renderNotation(); |
| document.getElementById('zoomValue').textContent = `${scaleValue}%`; |
| }); |
| |
| // Initial Sample Score |
| vrvToolkit.loadData(`<?xml version="1.0" encoding="UTF-8"?> |
| <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> |
| <score-partwise> |
| <part-list> |
| <score-part id="P1"> |
| <part-name>Sample</part-name> |
| </score-part> |
| </part-list> |
| <part id="P1"> |
| <measure number="1"> |
| <attributes> |
| <divisions>1</divisions> |
| <key> |
| <fifths>0</fifths> |
| </key> |
| <time> |
| <beats>4</beats> |
| <beat-type>4</beat-type> |
| </time> |
| <clef> |
| <sign>G</sign> |
| <line>2</line> |
| </clef> |
| </attributes> |
| <note> |
| <pitch> |
| <step>C</step> |
| <octave>4</octave> |
| </pitch> |
| <duration>1</duration> |
| <type>quarter</type> |
| </note> |
| </measure> |
| </part> |
| </score-partwise>`); |
| renderNotation(); |
| </script> |
| </body> |
| </html> |