Greasy Fork

来自缓存

Greasy Fork is available in English.

MP3 to Piano for MPP

Play piano with MP3 file harmonics on Multiplayer Piano

您需要先安装一个扩展,例如 篡改猴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.');
})();