notation-editor / index.html
namelessai's picture
Update index.html
759be12 verified
Raw
History Blame Contribute Delete
12.2 kB
<!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>