// ==UserScript== // @name MP3 to Piano for MPP // @namespace butter.lot // @version 1.0.1 // @description Play piano with MP3 file harmonics on Multiplayer Piano // @author MrButtersLot // @license Beerware // @match *://multiplayerpiano.net/* // @grant none // @downloadURL https://update.greasyfork.icu/scripts/556338/MP3%20to%20Piano%20for%20MPP.user.js // @updateURL https://update.greasyfork.icu/scripts/556338/MP3%20to%20Piano%20for%20MPP.meta.js // ==/UserScript== // "THE BEER-WARE LICENSE" (Revision 42): // As long as you retain this notice you can do whatever you want with this stuff. // If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. (function() { 'use strict'; // ============= AUDIO ANALYSIS ============= const PIANO_MIN_MIDI = 21; // A0 const PIANO_MAX_MIDI = 108; // C8 function frequencyToMidi(frequency) { return Math.round(12 * Math.log2(frequency / 440) + 69); } function midiToNoteName(midi) { const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; const octave = Math.floor(midi / 12) - 1; const note = notes[midi % 12]; return `${note}${octave}`; } function detectHarmonics(frequencyData, sampleRate, fftSize, threshold = 30) { const harmonics = []; const binWidth = sampleRate / fftSize; let maxMagnitude = 0; for (let i = 0; i < frequencyData.length; i++) { if (frequencyData[i] > maxMagnitude) maxMagnitude = frequencyData[i]; } if (maxMagnitude < threshold) return []; for (let i = 2; i < frequencyData.length / 2; i++) { const magnitude = frequencyData[i]; const dynamicThreshold = Math.max(threshold, maxMagnitude * 0.25); if (magnitude > dynamicThreshold) { const frequency = i * binWidth; if (frequency >= 50 && frequency <= 4000) { const midi = frequencyToMidi(frequency); if (midi >= PIANO_MIN_MIDI && midi <= PIANO_MAX_MIDI) { harmonics.push({ frequency, magnitude, midi, noteName: midiToNoteName(midi) }); } } } } return harmonics.sort((a, b) => b.magnitude - a.magnitude); } function analyzeAudio(analyser, maxHarmonics = 8, sensitivity = 30) { const frequencyData = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(frequencyData); let sum = 0; for (let i = 0; i < frequencyData.length; i++) { sum += frequencyData[i]; } const avgLevel = sum / frequencyData.length; const harmonics = detectHarmonics( frequencyData, analyser.context.sampleRate, analyser.fftSize, sensitivity ); return { harmonics: harmonics.slice(0, maxHarmonics), audioLevel: avgLevel }; } // ============= MP3 TO PIANO ENGINE ============= class MP3ToPiano { constructor() { this.isPlaying = false; this.audioContext = null; this.analyser = null; this.audioSource = null; this.audioBuffer = null; this.animationFrame = null; this.activeNotes = new Map(); this.sensitivity = 30; this.maxHarmonics = 12; this.onHarmonicsUpdate = null; this.onAudioLevelUpdate = null; this.onPlaybackUpdate = null; this.startTime = 0; this.pauseTime = 0; } async loadMP3(file) { try { const arrayBuffer = await file.arrayBuffer(); this.audioContext = new AudioContext(); this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); return true; } catch (error) { console.error('Error loading MP3:', error); return false; } } start() { if (!this.audioBuffer || !this.audioContext) return false; try { this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = 8192; this.analyser.smoothingTimeConstant = 0.6; this.analyser.minDecibels = -80; this.analyser.maxDecibels = -10; this.audioSource = this.audioContext.createBufferSource(); this.audioSource.buffer = this.audioBuffer; this.audioSource.connect(this.analyser); // Don't connect to destination - we only want piano output, not original audio this.audioSource.onended = () => { this.stop(); if (this.onPlaybackUpdate) { this.onPlaybackUpdate('ended'); } }; const offset = this.pauseTime || 0; this.audioSource.start(0, offset); this.startTime = this.audioContext.currentTime - offset; this.isPlaying = true; this.startAnalysis(); return true; } catch (error) { console.error('Error starting playback:', error); return false; } } pause() { if (!this.isPlaying) return; this.pauseTime = this.audioContext.currentTime - this.startTime; this.stop(); } stop() { if (this.animationFrame) { cancelAnimationFrame(this.animationFrame); this.animationFrame = null; } if (this.audioSource) { try { this.audioSource.stop(); } catch (e) { // Already stopped } this.audioSource.disconnect(); this.audioSource = null; } this.activeNotes.forEach(({ key }) => { if (window.MPP && window.MPP.release) { MPP.release(key); } }); this.activeNotes.clear(); this.isPlaying = false; } reset() { this.stop(); this.pauseTime = 0; this.startTime = 0; } getCurrentTime() { if (!this.audioContext) return 0; if (this.isPlaying) { return this.audioContext.currentTime - this.startTime; } return this.pauseTime; } getDuration() { return this.audioBuffer ? this.audioBuffer.duration : 0; } startAnalysis() { const analyze = () => { if (!this.analyser || !this.isPlaying) return; const result = analyzeAudio(this.analyser, this.maxHarmonics, this.sensitivity); const filteredHarmonics = result.harmonics; if (this.onAudioLevelUpdate) { this.onAudioLevelUpdate(result.audioLevel); } if (this.onHarmonicsUpdate) { this.onHarmonicsUpdate(filteredHarmonics, this.activeNotes.size); } if (this.onPlaybackUpdate) { const currentTime = this.getCurrentTime(); const duration = this.getDuration(); this.onPlaybackUpdate('playing', currentTime, duration); } const newActiveNotes = new Map(); filteredHarmonics.forEach(harmonic => { const key = Object.keys(MPP.piano.keys)[harmonic.midi - 21]; if (key) { let volume = harmonic.magnitude / 255; volume = Math.min(Math.max(volume, 0.1), 1); newActiveNotes.set(harmonic.midi, { key, volume }); } }); this.activeNotes.forEach(({ key }, midi) => { if (!newActiveNotes.has(midi)) { if (window.MPP && window.MPP.release) { MPP.release(key); } } }); newActiveNotes.forEach(({ key, volume }, midi) => { if (window.MPP && window.MPP.press) { MPP.press(key, volume); } }); this.activeNotes = newActiveNotes; this.animationFrame = requestAnimationFrame(analyze); }; analyze(); } } // ============= UI STYLES ============= const styles = ` .voice-piano-window { position: fixed; top: 80px; left: 20px; width: 400px; background: #2d2d2d; border: 2px solid #8b5cf6; border-radius: 8px; box-shadow: 0 5px 20px rgba(139, 92, 246, 0.3); color: #eee; font-family: sans-serif; font-size: 14px; z-index: 850; display: none; } .voice-piano-window.visible { display: block; } .voice-piano-header { padding: 10px 12px; background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); cursor: move; border-top-left-radius: 6px; border-top-right-radius: 6px; border-bottom: 1px solid #7c3aed; user-select: none; font-weight: 600; display: flex; align-items: center; gap: 8px; } .voice-piano-header svg { width: 18px; height: 18px; fill: currentColor; } .voice-piano-content { padding: 16px; display: flex; flex-direction: column; gap: 16px; } .voice-piano-file-input { display: none; } .voice-piano-file-label { background: #8b5cf6; border: 1px solid #7c3aed; color: #fff; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.2s; justify-content: center; text-align: center; } .voice-piano-file-label:hover { background: #7c3aed; box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4); } .voice-piano-file-label svg { width: 16px; height: 16px; fill: currentColor; } .voice-piano-file-name { padding: 8px 12px; background: #222; border: 1px solid #444; border-radius: 6px; font-size: 12px; color: #999; text-align: center; font-style: italic; } .voice-piano-controls { display: flex; gap: 8px; align-items: center; justify-content: center; } .voice-piano-btn { background: #8b5cf6; border: 1px solid #7c3aed; color: #fff; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.2s; flex: 1; justify-content: center; } .voice-piano-btn:hover { background: #7c3aed; box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4); } .voice-piano-btn:disabled { background: #555; border-color: #444; cursor: not-allowed; opacity: 0.5; } .voice-piano-btn.playing { background: #dc2626; border-color: #b91c1c; } .voice-piano-btn.playing:hover:not(:disabled) { background: #b91c1c; } .voice-piano-btn svg { width: 16px; height: 16px; fill: currentColor; } .voice-piano-playback { background: #222; border: 1px solid #444; border-radius: 6px; padding: 12px; } .voice-piano-time { display: flex; justify-content: space-between; font-size: 12px; color: #999; margin-bottom: 8px; } .voice-piano-progress { height: 6px; background: #333; border-radius: 3px; overflow: hidden; } .voice-piano-progress-bar { height: 100%; background: linear-gradient(90deg, #8b5cf6, #6d28d9); transition: width 0.1s; } .voice-piano-slider-group { display: flex; flex-direction: column; gap: 8px; } .voice-piano-slider-label { display: flex; justify-content: space-between; font-size: 13px; color: #ccc; } .voice-piano-slider { width: 100%; height: 6px; border-radius: 3px; background: #444; outline: none; -webkit-appearance: none; } .voice-piano-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #8b5cf6; cursor: pointer; transition: all 0.2s; } .voice-piano-slider::-webkit-slider-thumb:hover { background: #7c3aed; box-shadow: 0 0 8px rgba(139, 92, 246, 0.6); } .voice-piano-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: #8b5cf6; cursor: pointer; border: none; transition: all 0.2s; } .voice-piano-harmonics { max-height: 240px; overflow-y: auto; background: #222; border: 1px solid #444; border-radius: 6px; padding: 8px; } .voice-piano-harmonics::-webkit-scrollbar { width: 8px; } .voice-piano-harmonics::-webkit-scrollbar-track { background: #333; border-radius: 4px; } .voice-piano-harmonics::-webkit-scrollbar-thumb { background: #8b5cf6; border-radius: 4px; } .voice-piano-harmonic { display: flex; justify-content: space-between; align-items: center; padding: 8px; margin-bottom: 6px; background: #2d2d2d; border: 1px solid #444; border-radius: 4px; font-size: 12px; } .voice-piano-harmonic-left { display: flex; align-items: center; gap: 10px; } .voice-piano-pulse { width: 6px; height: 6px; border-radius: 50%; background: #8b5cf6; animation: pulse 1s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.8); } } .voice-piano-note { font-family: monospace; font-weight: 600; color: #8b5cf6; min-width: 40px; } .voice-piano-freq { color: #999; font-size: 11px; } .voice-piano-magnitude { display: flex; align-items: center; gap: 8px; } .voice-piano-bar { width: 60px; height: 4px; background: #444; border-radius: 2px; overflow: hidden; } .voice-piano-bar-fill { height: 100%; background: linear-gradient(90deg, #8b5cf6, #6d28d9); transition: width 0.1s; } .voice-piano-status { text-align: center; padding: 12px; background: #222; border: 1px solid #444; border-radius: 6px; color: #999; font-style: italic; font-size: 13px; } .voice-piano-level { margin-top: 8px; height: 8px; background: #333; border-radius: 4px; overflow: hidden; position: relative; } .voice-piano-level-bar { height: 100%; background: linear-gradient(90deg, #22c55e, #16a34a); transition: width 0.1s; box-shadow: 0 0 10px rgba(34, 197, 94, 0.5); } .voice-piano-info { background: #1a1a1a; border: 1px solid #444; border-radius: 6px; padding: 12px; font-size: 12px; color: #999; line-height: 1.6; } .voice-piano-info strong { color: #8b5cf6; } `; // ============= UI CREATION ============= const ICON_MUSIC = ``; const ICON_UPLOAD = ``; const ICON_PLAY = ``; const ICON_PAUSE = ``; const ICON_STOP = ``; function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } const playerHTML = `
${ICON_MUSIC} MP3 to Piano
Sensitivity 30
Max Notes 12
Load an MP3 file to begin
How it works: Load an MP3 file and the system will analyze its frequencies in real-time, playing corresponding piano keys as the music plays.
`; const toggleButtonHTML = `
MP3 to Piano
`; // ============= INITIALIZATION ============= document.head.insertAdjacentHTML('beforeend', ``); document.body.insertAdjacentHTML('beforeend', playerHTML); const buttonsContainer = document.querySelector('#buttons'); if (buttonsContainer) { buttonsContainer.insertAdjacentHTML('beforeend', toggleButtonHTML); } else { document.body.insertAdjacentHTML('beforeend', toggleButtonHTML); } // ============= UI ELEMENTS ============= const ui = { window: document.getElementById('voice-piano-window'), header: document.querySelector('.voice-piano-header'), fileInput: document.getElementById('voice-piano-file-input'), fileName: document.getElementById('voice-piano-file-name'), playBtn: document.getElementById('voice-piano-play-btn'), stopBtn: document.getElementById('voice-piano-stop-btn'), menuBtn: document.getElementById('voice-piano-menu-btn'), sensitivitySlider: document.getElementById('voice-piano-sensitivity'), sensitivityValue: document.getElementById('voice-piano-sensitivity-value'), maxHarmonicsSlider: document.getElementById('voice-piano-max-harmonics'), maxHarmonicsValue: document.getElementById('voice-piano-max-harmonics-value'), status: document.getElementById('voice-piano-status'), harmonicsContainer: document.getElementById('voice-piano-harmonics'), levelBar: document.getElementById('voice-piano-level-bar'), playback: document.getElementById('voice-piano-playback'), currentTime: document.getElementById('voice-piano-current-time'), duration: document.getElementById('voice-piano-duration'), progressBar: document.getElementById('voice-piano-progress-bar') }; const engine = new MP3ToPiano(); // ============= EVENT HANDLERS ============= engine.onHarmonicsUpdate = (harmonics, activeCount) => { const statusText = activeCount > 0 ? `Playing... ${activeCount} note${activeCount !== 1 ? 's' : ''} active` : 'Playing... Analyzing audio'; ui.status.childNodes[0].textContent = statusText; if (harmonics.length === 0) { ui.harmonicsContainer.innerHTML = '
Analyzing audio...
'; } else { ui.harmonicsContainer.innerHTML = harmonics.map(h => `
${h.noteName} ${h.frequency.toFixed(1)} Hz
${Math.round(h.magnitude / 255 * 100)}%
`).join(''); } }; engine.onAudioLevelUpdate = (level) => { const percentage = Math.min(100, (level / 128) * 100); ui.levelBar.style.width = `${percentage}%`; }; engine.onPlaybackUpdate = (status, currentTime, duration) => { if (status === 'playing' && currentTime !== undefined && duration !== undefined) { ui.currentTime.textContent = formatTime(currentTime); ui.duration.textContent = formatTime(duration); const percentage = (currentTime / duration) * 100; ui.progressBar.style.width = `${percentage}%`; } else if (status === 'ended') { ui.playBtn.innerHTML = `${ICON_PLAY}Play`; ui.playBtn.classList.remove('playing'); ui.status.innerHTML = 'Playback finished
'; ui.levelBar = document.getElementById('voice-piano-level-bar'); ui.harmonicsContainer.style.display = 'none'; } }; ui.fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) { ui.status.textContent = 'Loading MP3...'; const success = await engine.loadMP3(file); if (success) { ui.fileName.textContent = file.name; ui.fileName.style.display = 'block'; ui.playBtn.disabled = false; ui.stopBtn.disabled = false; ui.status.textContent = 'MP3 loaded! Click Play to start.'; ui.playback.style.display = 'block'; ui.duration.textContent = formatTime(engine.getDuration()); ui.currentTime.textContent = '0:00'; console.log('[MP3 to Piano] MP3 loaded successfully:', file.name); } else { ui.status.textContent = 'Error loading MP3. Try another file.'; } } }); ui.playBtn.addEventListener('click', () => { if (!engine.isPlaying) { const success = engine.start(); if (success) { ui.playBtn.innerHTML = `${ICON_PAUSE}Pause`; ui.playBtn.classList.add('playing'); ui.status.innerHTML = 'Playing...
'; ui.levelBar = document.getElementById('voice-piano-level-bar'); ui.harmonicsContainer.style.display = 'block'; } } else { engine.pause(); ui.playBtn.innerHTML = `${ICON_PLAY}Play`; ui.playBtn.classList.remove('playing'); ui.status.textContent = 'Paused'; ui.harmonicsContainer.style.display = 'none'; } }); ui.stopBtn.addEventListener('click', () => { engine.reset(); ui.playBtn.innerHTML = `${ICON_PLAY}Play`; ui.playBtn.classList.remove('playing'); ui.status.textContent = 'Stopped'; ui.harmonicsContainer.style.display = 'none'; ui.currentTime.textContent = '0:00'; ui.progressBar.style.width = '0%'; ui.levelBar.style.width = '0%'; }); ui.sensitivitySlider.addEventListener('input', (e) => { const value = e.target.value; ui.sensitivityValue.textContent = value; engine.sensitivity = parseInt(value); }); ui.maxHarmonicsSlider.addEventListener('input', (e) => { const value = e.target.value; ui.maxHarmonicsValue.textContent = value; engine.maxHarmonics = parseInt(value); }); ui.menuBtn.addEventListener('click', () => { ui.window.classList.toggle('visible'); }); // ============= DRAGGABLE WINDOW ============= let isDragging = false; let offsetX, offsetY; ui.header.addEventListener('mousedown', (e) => { isDragging = true; const rect = ui.window.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (isDragging) { ui.window.style.left = `${e.clientX - offsetX}px`; ui.window.style.top = `${e.clientY - offsetY}px`; } }); document.addEventListener('mouseup', () => { isDragging = false; }); console.log('[MP3 to Piano] Loaded successfully! Click the "MP3 to Piano" button to open.'); })();