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));