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