Spaces:
Running
Running
File size: 3,343 Bytes
a68a7b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 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));
|