Spaces:
Running
Running
| import WaveSurfer from "https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js"; | |
| // Per-channel render config: speaker 1 (blue) on top, speaker 2 (green) on | |
| // bottom for stereo separated outputs. Mono noisy mixtures only use the first | |
| // (blue) entry — wavesurfer ignores unused channel configs. | |
| const CH_HEIGHT = 40; | |
| const SPK1 = { | |
| waveColor: "#8a9bbf", | |
| progressColor: "#1f3a78", | |
| height: CH_HEIGHT, | |
| }; | |
| const SPK2 = { | |
| waveColor: "#bf8a9b", | |
| progressColor: "#8b1a3a", | |
| height: CH_HEIGHT, | |
| }; | |
| let currentWs = null; | |
| let currentBtn = null; | |
| function setIdle(btn) { | |
| btn.textContent = "▶"; | |
| btn.classList.remove("playing"); | |
| btn.setAttribute("aria-label", "Play"); | |
| } | |
| function setPlaying(btn) { | |
| btn.textContent = "⏸"; | |
| btn.classList.add("playing"); | |
| btn.setAttribute("aria-label", "Pause"); | |
| } | |
| function initWaveform(container) { | |
| if (container.dataset.initialized) return; | |
| container.dataset.initialized = "1"; | |
| // Wrap [button | waveform] inside the original parent cell. | |
| const parent = container.parentNode; | |
| const wrapper = document.createElement("div"); | |
| wrapper.className = "player"; | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.className = "play-btn"; | |
| setIdle(btn); | |
| parent.insertBefore(wrapper, container); | |
| wrapper.appendChild(btn); | |
| wrapper.appendChild(container); | |
| const ws = WaveSurfer.create({ | |
| container, | |
| height: CH_HEIGHT, | |
| barWidth: 2, | |
| barGap: 1, | |
| barRadius: 1, | |
| cursorColor: "#111111", | |
| cursorWidth: 1, | |
| normalize: true, | |
| interact: true, | |
| splitChannels: [SPK1, SPK2], | |
| url: container.dataset.src, | |
| }); | |
| ws.on("decode", () => { | |
| const data = ws.getDecodedData(); | |
| if (data && data.numberOfChannels >= 2) { | |
| container.classList.add("stereo"); | |
| if (!container.querySelector(".ch-label")) { | |
| const l1 = document.createElement("span"); | |
| l1.className = "ch-label ch-label-1"; | |
| l1.textContent = "S1"; | |
| const l2 = document.createElement("span"); | |
| l2.className = "ch-label ch-label-2"; | |
| l2.textContent = "S2"; | |
| container.appendChild(l1); | |
| container.appendChild(l2); | |
| } | |
| } else { | |
| container.classList.add("mono"); | |
| if (!container.querySelector(".ch-label")) { | |
| const l = document.createElement("span"); | |
| l.className = "ch-label ch-label-mono"; | |
| l.textContent = "MIX"; | |
| container.appendChild(l); | |
| } | |
| } | |
| }); | |
| btn.addEventListener("click", () => { | |
| if (currentWs && currentWs !== ws) currentWs.pause(); | |
| ws.playPause(); | |
| }); | |
| ws.on("play", () => { | |
| if (currentWs && currentWs !== ws) { | |
| currentWs.pause(); | |
| if (currentBtn) setIdle(currentBtn); | |
| } | |
| currentWs = ws; | |
| currentBtn = btn; | |
| setPlaying(btn); | |
| }); | |
| ws.on("pause", () => setIdle(btn)); | |
| ws.on("finish", () => setIdle(btn)); | |
| ws.on("error", () => { | |
| btn.disabled = true; | |
| btn.title = "Failed to load"; | |
| }); | |
| } | |
| // Lazy init: only decode files as their row scrolls into view. | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| for (const e of entries) { | |
| if (e.isIntersecting) { | |
| initWaveform(e.target); | |
| observer.unobserve(e.target); | |
| } | |
| } | |
| }, | |
| { rootMargin: "300px" } | |
| ); | |
| document.querySelectorAll(".waveform").forEach((el) => observer.observe(el)); | |