Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Neon MIDI Player</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@magenta/[email protected]/es6/core.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@magenta/[email protected]/es6/midi_visualizer.js"></script> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700&display=swap'); | |
:root { | |
--primary: #6366f1; | |
--primary-dark: #4f46e5; | |
--secondary: #f43f5e; | |
--dark: #1e293b; | |
--light: #f8fafc; | |
} | |
body { | |
font-family: 'Montserrat', sans-serif; | |
background-color: #0f172a; | |
color: var(--light); | |
} | |
.glass-card { | |
background: rgba(255, 255, 255, 0.05); | |
backdrop-filter: blur(10px); | |
-webkit-backdrop-filter: blur(10px); | |
border-radius: 12px; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.2); | |
} | |
.neon-text { | |
text-shadow: 0 0 5px var(--primary), 0 0 10px var(--primary); | |
} | |
.piano-roll { | |
height: 300px; | |
overflow-y: auto; | |
background-color: rgba(15, 23, 42, 0.7); | |
} | |
.progress-bar { | |
height: 6px; | |
background: rgba(255, 255, 255, 0.1); | |
} | |
.progress-fill { | |
height: 100%; | |
background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
transition: width 0.1s linear; | |
} | |
.btn-primary { | |
background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
transition: all 0.3s ease; | |
} | |
.btn-primary:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); | |
} | |
.btn-secondary { | |
background: linear-gradient(135deg, var(--secondary), #e11d48); | |
transition: all 0.3s ease; | |
} | |
.btn-secondary:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 15px rgba(244, 63, 94, 0.4); | |
} | |
.keyboard { | |
display: flex; | |
height: 120px; | |
margin-top: 20px; | |
} | |
.white-key { | |
flex: 1; | |
background: white; | |
border: 1px solid #ccc; | |
border-radius: 0 0 5px 5px; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
z-index: 1; | |
position: relative; | |
} | |
.black-key { | |
width: 0.6; | |
height: 60%; | |
background: #333; | |
margin-left: -0.3; | |
margin-right: -0.3; | |
z-index: 2; | |
position: relative; | |
border-radius: 0 0 3px 3px; | |
} | |
.active-key { | |
background: linear-gradient(180deg, var(--primary), var(--secondary)); | |
} | |
.visualizer { | |
width: 100%; | |
height: 100%; | |
} | |
/* Custom scrollbar */ | |
::-webkit-scrollbar { | |
width: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: rgba(255, 255, 255, 0.05); | |
} | |
::-webkit-scrollbar-thumb { | |
background: var(--primary); | |
border-radius: 4px; | |
} | |
/* Animation for loading */ | |
@keyframes pulse { | |
0%, 100% { opacity: 0.6; } | |
50% { opacity: 1; } | |
} | |
.pulse { | |
animation: pulse 1.5s infinite; | |
} | |
</style> | |
</head> | |
<body class="min-h-screen flex flex-col"> | |
<div class="container mx-auto px-4 py-8 flex-1"> | |
<header class="text-center mb-10"> | |
<h1 class="text-4xl md:text-5xl font-bold mb-2 neon-text">Neon MIDI Player</h1> | |
<p class="text-lg text-gray-300">Play MIDI files with custom SoundFonts</p> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
<!-- Controls Section --> | |
<div class="glass-card p-6 lg:col-span-1"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-sliders-h mr-2"></i> Controls | |
</h2> | |
<div class="space-y-4"> | |
<div> | |
<label class="block text-sm font-medium mb-1">MIDI File</label> | |
<input type="file" id="midi-file" accept=".mid,.midi" class="hidden"> | |
<button id="load-midi-btn" class="w-full btn-primary text-white py-2 px-4 rounded-lg flex items-center justify-center"> | |
<i class="fas fa-file-import mr-2"></i> Load MIDI File | |
</button> | |
</div> | |
<div> | |
<label class="block text-sm font-medium mb-1">SoundFont</label> | |
<select id="soundfont-select" class="w-full bg-gray-800 border border-gray-700 rounded-lg py-2 px-3 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
<option value="acoustic_grand_piano">Acoustic Grand Piano</option> | |
<option value="electric_piano_1">Electric Piano 1</option> | |
<option value="electric_piano_2">Electric Piano 2</option> | |
<option value="honkytonk_piano">Honky-tonk Piano</option> | |
<option value="electric_guitar_clean">Electric Guitar (Clean)</option> | |
<option value="electric_guitar_jazz">Electric Guitar (Jazz)</option> | |
<option value="electric_bass_finger">Electric Bass (Finger)</option> | |
<option value="synth_bass_1">Synth Bass 1</option> | |
<option value="violin">Violin</option> | |
<option value="cello">Cello</option> | |
</select> | |
</div> | |
<div class="grid grid-cols-3 gap-2"> | |
<button id="play-btn" class="btn-primary text-white py-2 px-4 rounded-lg flex items-center justify-center"> | |
<i class="fas fa-play mr-2"></i> Play | |
</button> | |
<button id="pause-btn" class="bg-gray-700 text-white py-2 px-4 rounded-lg flex items-center justify-center"> | |
<i class="fas fa-pause mr-2"></i> Pause | |
</button> | |
<button id="stop-btn" class="btn-secondary text-white py-2 px-4 rounded-lg flex items-center justify-center"> | |
<i class="fas fa-stop mr-2"></i> Stop | |
</button> | |
</div> | |
<div> | |
<label class="block text-sm font-medium mb-1">Volume</label> | |
<input type="range" id="volume-slider" min="0" max="1" step="0.01" value="0.8" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<div> | |
<label class="block text-sm font-medium mb-1">Tempo</label> | |
<input type="range" id="tempo-slider" min="20" max="200" step="1" value="120" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> | |
<div class="flex justify-between text-xs text-gray-400"> | |
<span>Slow</span> | |
<span id="tempo-value">120 BPM</span> | |
<span>Fast</span> | |
</div> | |
</div> | |
<div class="pt-4 border-t border-gray-700"> | |
<div class="flex justify-between text-sm mb-1"> | |
<span>Status:</span> | |
<span id="status-text" class="text-indigo-300">Ready</span> | |
</div> | |
<div class="progress-bar rounded-full"> | |
<div id="progress-fill" class="progress-fill rounded-full" style="width: 0%"></div> | |
</div> | |
<div class="flex justify-between text-xs text-gray-400 mt-1"> | |
<span id="current-time">0:00</span> | |
<span id="total-time">0:00</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Visualizer Section --> | |
<div class="glass-card p-6 lg:col-span-2"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-music mr-2"></i> Visualizer | |
</h2> | |
<div class="piano-roll glass-card p-4 mb-4"> | |
<div id="visualizer" class="visualizer"> | |
<div class="flex items-center justify-center h-full text-gray-500"> | |
<div class="text-center"> | |
<i class="fas fa-music text-4xl mb-2"></i> | |
<p>No MIDI file loaded</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="keyboard hidden" id="keyboard-preview"> | |
<!-- White keys --> | |
<div class="white-key" data-note="C"></div> | |
<div class="white-key" data-note="D"></div> | |
<div class="white-key" data-note="E"></div> | |
<div class="white-key" data-note="F"></div> | |
<div class="white-key" data-note="G"></div> | |
<div class="white-key" data-note="A"></div> | |
<div class="white-key" data-note="B"></div> | |
<!-- Black keys --> | |
<div class="black-key" data-note="C#"></div> | |
<div class="black-key" data-note="D#"></div> | |
<div class="black-key" data-note="F#"></div> | |
<div class="black-key" data-note="G#"></div> | |
<div class="black-key" data-note="A#"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<footer class="text-center py-4 text-gray-400 text-sm"> | |
<p>Neon MIDI Player © 2023 | Powered by Tone.js and Magenta.js</p> | |
</footer> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Initialize Tone.js | |
Tone.start(); | |
// Elements | |
const loadMidiBtn = document.getElementById('load-midi-btn'); | |
const midiFileInput = document.getElementById('midi-file'); | |
const soundfontSelect = document.getElementById('soundfont-select'); | |
const playBtn = document.getElementById('play-btn'); | |
const pauseBtn = document.getElementById('pause-btn'); | |
const stopBtn = document.getElementById('stop-btn'); | |
const volumeSlider = document.getElementById('volume-slider'); | |
const tempoSlider = document.getElementById('tempo-slider'); | |
const tempoValue = document.getElementById('tempo-value'); | |
const statusText = document.getElementById('status-text'); | |
const progressFill = document.getElementById('progress-fill'); | |
const currentTime = document.getElementById('current-time'); | |
const totalTime = document.getElementById('total-time'); | |
const visualizer = document.getElementById('visualizer'); | |
const keyboardPreview = document.getElementById('keyboard-preview'); | |
// Variables | |
let player; | |
let midiData; | |
let sequence; | |
let isPlaying = false; | |
let currentSoundfont = 'acoustic_grand_piano'; | |
let visualizerSVG; | |
// Initialize player | |
async function initPlayer() { | |
player = new mm.SoundFontPlayer( | |
'https://storage.googleapis.com/magentadata/js/soundfonts/sgm_plus', | |
undefined, | |
undefined, | |
undefined, | |
Tone.context | |
); | |
await player.loadSoundFont(currentSoundfont); | |
player.setVolume(volumeSlider.value); | |
// Set up callbacks | |
player.onLoad = () => { | |
statusText.textContent = 'Ready'; | |
statusText.classList.remove('text-indigo-300'); | |
statusText.classList.add('text-green-400'); | |
}; | |
player.onError = (error) => { | |
statusText.textContent = `Error: ${error}`; | |
statusText.classList.remove('text-indigo-300'); | |
statusText.classList.add('text-red-400'); | |
}; | |
player.onPlaybackStarted = () => { | |
isPlaying = true; | |
statusText.textContent = 'Playing'; | |
statusText.classList.remove('text-indigo-300'); | |
statusText.classList.add('text-green-400'); | |
updatePlaybackControls(); | |
}; | |
player.onPlaybackPaused = () => { | |
isPlaying = false; | |
statusText.textContent = 'Paused'; | |
statusText.classList.remove('text-green-400'); | |
statusText.classList.add('text-yellow-400'); | |
updatePlaybackControls(); | |
}; | |
player.onPlaybackStopped = () => { | |
isPlaying = false; | |
statusText.textContent = 'Stopped'; | |
statusText.classList.remove('text-green-400'); | |
statusText.classList.add('text-indigo-300'); | |
progressFill.style.width = '0%'; | |
currentTime.textContent = '0:00'; | |
updatePlaybackControls(); | |
}; | |
player.onPlaybackProgress = (progress) => { | |
progressFill.style.width = `${progress * 100}%`; | |
if (player.currentTime && player.totalTime) { | |
currentTime.textContent = formatTime(player.currentTime); | |
totalTime.textContent = formatTime(player.totalTime); | |
} | |
}; | |
} | |
// Initialize visualizer | |
function initVisualizer() { | |
visualizerSVG = new mm.PianoRollSVGVisualizer( | |
null, | |
visualizer, | |
{ | |
noteHeight: 8, | |
pixelsPerTimeStep: 30, | |
noteSpacing: 1, | |
noteRGB: '125, 211, 252', | |
activeNoteRGB: '236, 72, 153', | |
minPitch: 21, | |
maxPitch: 108 | |
} | |
); | |
} | |
// Format time (seconds to MM:SS) | |
function formatTime(seconds) { | |
const mins = Math.floor(seconds / 60); | |
const secs = Math.floor(seconds % 60); | |
return `${mins}:${secs < 10 ? '0' : ''}${secs}`; | |
} | |
// Update playback controls | |
function updatePlaybackControls() { | |
if (isPlaying) { | |
playBtn.disabled = true; | |
playBtn.classList.remove('btn-primary'); | |
playBtn.classList.add('bg-gray-600'); | |
pauseBtn.disabled = false; | |
pauseBtn.classList.remove('bg-gray-700'); | |
pauseBtn.classList.add('btn-secondary'); | |
stopBtn.disabled = false; | |
} else { | |
playBtn.disabled = !midiData; | |
playBtn.classList.add('btn-primary'); | |
playBtn.classList.remove('bg-gray-600'); | |
pauseBtn.disabled = true; | |
pauseBtn.classList.add('bg-gray-700'); | |
pauseBtn.classList.remove('btn-secondary'); | |
stopBtn.disabled = !midiData; | |
} | |
} | |
// Load MIDI file | |
loadMidiBtn.addEventListener('click', () => { | |
midiFileInput.click(); | |
}); | |
midiFileInput.addEventListener('change', async (e) => { | |
const file = e.target.files[0]; | |
if (!file) return; | |
statusText.textContent = 'Loading MIDI...'; | |
statusText.classList.add('pulse'); | |
try { | |
const arrayBuffer = await file.arrayBuffer(); | |
midiData = new mm.MidiFile(arrayBuffer); | |
// Update visualizer | |
visualizerSVG = new mm.PianoRollSVGVisualizer( | |
midiData, | |
visualizer, | |
{ | |
noteHeight: 8, | |
pixelsPerTimeStep: 30, | |
noteSpacing: 1, | |
noteRGB: '125, 211, 252', | |
activeNoteRGB: '236, 72, 153', | |
minPitch: 21, | |
maxPitch: 108 | |
} | |
); | |
// Calculate total time | |
const totalSeconds = midiData.totalTime; | |
totalTime.textContent = formatTime(totalSeconds); | |
// Show keyboard preview | |
keyboardPreview.classList.remove('hidden'); | |
statusText.textContent = 'Ready'; | |
statusText.classList.remove('pulse'); | |
updatePlaybackControls(); | |
} catch (error) { | |
console.error('Error loading MIDI:', error); | |
statusText.textContent = `Error: ${error.message}`; | |
statusText.classList.remove('pulse'); | |
statusText.classList.add('text-red-400'); | |
} | |
}); | |
// Play MIDI | |
playBtn.addEventListener('click', () => { | |
if (!midiData) return; | |
player.loadSamples(midiData).then(() => { | |
player.start(midiData); | |
// Update visualizer to follow playback | |
if (visualizerSVG) { | |
visualizerSVG = new mm.PianoRollSVGVisualizer( | |
midiData, | |
visualizer, | |
{ | |
noteHeight: 8, | |
pixelsPerTimeStep: 30, | |
noteSpacing: 1, | |
noteRGB: '125, 211, 252', | |
activeNoteRGB: '236, 72, 153', | |
minPitch: 21, | |
maxPitch: 108 | |
} | |
); | |
visualizerSVG.redraw(player.currentTime); | |
} | |
}); | |
}); | |
// Pause MIDI | |
pauseBtn.addEventListener('click', () => { | |
if (!isPlaying) return; | |
player.pause(); | |
}); | |
// Stop MIDI | |
stopBtn.addEventListener('click', () => { | |
if (!isPlaying) return; | |
player.stop(); | |
}); | |
// Change volume | |
volumeSlider.addEventListener('input', () => { | |
if (player) { | |
player.setVolume(volumeSlider.value); | |
} | |
}); | |
// Change tempo | |
tempoSlider.addEventListener('input', () => { | |
const tempo = parseInt(tempoSlider.value); | |
tempoValue.textContent = `${tempo} BPM`; | |
if (player) { | |
player.setTempo(tempo); | |
} | |
}); | |
// Change soundfont | |
soundfontSelect.addEventListener('change', async () => { | |
currentSoundfont = soundfontSelect.value; | |
statusText.textContent = 'Loading SoundFont...'; | |
statusText.classList.add('pulse'); | |
try { | |
await player.loadSoundFont(currentSoundfont); | |
statusText.textContent = 'Ready'; | |
statusText.classList.remove('pulse'); | |
} catch (error) { | |
console.error('Error loading SoundFont:', error); | |
statusText.textContent = `Error: ${error.message}`; | |
statusText.classList.remove('pulse'); | |
statusText.classList.add('text-red-400'); | |
} | |
}); | |
// Keyboard preview interaction | |
keyboardPreview.addEventListener('mousedown', (e) => { | |
if (e.target.classList.contains('white-key') || e.target.classList.contains('black-key')) { | |
e.target.classList.add('active-key'); | |
// Play the note | |
const note = e.target.dataset.note; | |
const synth = new Tone.Synth().toDestination(); | |
synth.triggerAttackRelease(`${note}4`, "8n"); | |
} | |
}); | |
keyboardPreview.addEventListener('mouseup', (e) => { | |
if (e.target.classList.contains('white-key') || e.target.classList.contains('black-key')) { | |
e.target.classList.remove('active-key'); | |
} | |
}); | |
keyboardPreview.addEventListener('mouseleave', () => { | |
const activeKeys = keyboardPreview.querySelectorAll('.active-key'); | |
activeKeys.forEach(key => key.classList.remove('active-key')); | |
}); | |
// Initialize everything | |
initPlayer(); | |
initVisualizer(); | |
updatePlaybackControls(); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=NeoPy/testing-web" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |