Greasy Fork

Greasy Fork is available in English.

MP3 to Piano for MPP

Play piano with MP3 file harmonics on Multiplayer Piano

当前为 2025-11-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/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 = `<svg viewBox="0 0 24 24"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`;
  const ICON_UPLOAD = `<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`;
  const ICON_PLAY = `<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
  const ICON_PAUSE = `<svg viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>`;
  const ICON_STOP = `<svg viewBox="0 0 24 24"><rect x="5" y="5" width="14" height="14"/></svg>`;

  function formatTime(seconds) {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }

  const playerHTML = `
    <div id="voice-piano-window" class="voice-piano-window">
      <div class="voice-piano-header">
        ${ICON_MUSIC}
        <span>MP3 to Piano</span>
      </div>
      <div class="voice-piano-content">
        <input type="file" id="voice-piano-file-input" class="voice-piano-file-input" accept="audio/mp3,audio/mpeg">
        <label for="voice-piano-file-input" class="voice-piano-file-label">
          ${ICON_UPLOAD}
          <span>Load MP3 File</span>
        </label>

        <div id="voice-piano-file-name" class="voice-piano-file-name" style="display: none;">
          No file loaded
        </div>

        <div id="voice-piano-playback" class="voice-piano-playback" style="display: none;">
          <div class="voice-piano-time">
            <span id="voice-piano-current-time">0:00</span>
            <span id="voice-piano-duration">0:00</span>
          </div>
          <div class="voice-piano-progress">
            <div id="voice-piano-progress-bar" class="voice-piano-progress-bar" style="width: 0%"></div>
          </div>
        </div>

        <div class="voice-piano-controls">
          <button id="voice-piano-play-btn" class="voice-piano-btn" disabled>
            ${ICON_PLAY}
            <span>Play</span>
          </button>
          <button id="voice-piano-stop-btn" class="voice-piano-btn" disabled>
            ${ICON_STOP}
            <span>Stop</span>
          </button>
        </div>

        <div class="voice-piano-slider-group">
          <div class="voice-piano-slider-label">
            <span>Sensitivity</span>
            <span id="voice-piano-sensitivity-value">30</span>
          </div>
          <input type="range" id="voice-piano-sensitivity" class="voice-piano-slider" min="10" max="80" value="30">
        </div>

        <div class="voice-piano-slider-group">
          <div class="voice-piano-slider-label">
            <span>Max Notes</span>
            <span id="voice-piano-max-harmonics-value">12</span>
          </div>
          <input type="range" id="voice-piano-max-harmonics" class="voice-piano-slider" min="2" max="24" value="12">
        </div>

        <div id="voice-piano-status" class="voice-piano-status">
          Load an MP3 file to begin
          <div class="voice-piano-level">
            <div id="voice-piano-level-bar" class="voice-piano-level-bar" style="width: 0%"></div>
          </div>
        </div>

        <div id="voice-piano-harmonics" class="voice-piano-harmonics" style="display: none;">
        </div>

        <div class="voice-piano-info">
          <strong>How it works:</strong> Load an MP3 file and the system will analyze its frequencies in real-time, playing corresponding piano keys as the music plays.
        </div>
      </div>
    </div>
  `;

  const toggleButtonHTML = `<div class="ugly-button" id="voice-piano-menu-btn">MP3 to Piano</div>`;

  // ============= INITIALIZATION =============

  document.head.insertAdjacentHTML('beforeend', `<style>${styles}</style>`);
  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 = '<div class="voice-piano-status">Analyzing audio...</div>';
    } else {
      ui.harmonicsContainer.innerHTML = harmonics.map(h => `
        <div class="voice-piano-harmonic">
          <div class="voice-piano-harmonic-left">
            <div class="voice-piano-pulse"></div>
            <span class="voice-piano-note">${h.noteName}</span>
            <span class="voice-piano-freq">${h.frequency.toFixed(1)} Hz</span>
          </div>
          <div class="voice-piano-magnitude">
            <div class="voice-piano-bar">
              <div class="voice-piano-bar-fill" style="width: ${(h.magnitude / 255 * 100)}%"></div>
            </div>
            <span>${Math.round(h.magnitude / 255 * 100)}%</span>
          </div>
        </div>
      `).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}<span>Play</span>`;
      ui.playBtn.classList.remove('playing');
      ui.status.innerHTML = 'Playback finished<div class="voice-piano-level"><div id="voice-piano-level-bar" class="voice-piano-level-bar" style="width: 0%"></div></div>';
      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}<span>Pause</span>`;
        ui.playBtn.classList.add('playing');
        ui.status.innerHTML = 'Playing...<div class="voice-piano-level"><div id="voice-piano-level-bar" class="voice-piano-level-bar" style="width: 0%"></div></div>';
        ui.levelBar = document.getElementById('voice-piano-level-bar');
        ui.harmonicsContainer.style.display = 'block';
      }
    } else {
      engine.pause();
      ui.playBtn.innerHTML = `${ICON_PLAY}<span>Play</span>`;
      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}<span>Play</span>`;
    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.');
})();