<!-- SS · part 49 -->
 es-btn-sm" data-w="cancel">Cancel</button></div>' +
    '<div class="es-wv-bar es-wv-out"><button type="button" data-w="stem">Save as new stem</button><button type="button" data-w="song">Save to My Songs</button><button type="button" data-w="wav">Download WAV</button></div>' +
    '<div class="es-status" id="es-wv-status">Load a stem, a take, your current mix or a saved song. Drag on the wave to select; effects apply to the selection (or everything).</div>';
  var cv = $('#es-wv-cv'), status = $('#es-wv-status');
  function say(m) { status.textContent = m; }
  function t2s(t) { var m = Math.floor(t / 60), s = t - m * 60; return m + ':' + (s < 10 ? '0' : '') + s.toFixed(2); }
  function lcd() { var b = W.buf; if (!b) { $('#es-wv-lcd').textContent = 'NO WAVE LOADED'; return; } var sr = b.sampleRate, a = W.sel[0] / sr, z = W.sel[1] / sr; $('#es-wv-lcd').textContent = (W.name || 'WAVE').toUpperCase().slice(0, 22) + '  ' + t2s(b.duration) + '  ' + b.numberOfChannels + 'CH ' + (sr / 1000) + 'K  ' + (z > a ? 'SEL ' + t2s(a) + '–' + t2s(z) + ' (' + (z - a).toFixed(2) + 's)' : 'CUR ' + t2s(W.cur / sr)); }
  var pk = null;
  function draw() {
    var w = cv.clientWidth, h = cv.clientHeight; if (!w) return; var dpr = window.devicePixelRatio || 1; cv.width = w * dpr; cv.height = h * dpr; var g = cv.getContext('2d'); g.setTransform(dpr, 0, 0, dpr, 0, 0);
    g.fillStyle = '#020302'; g.fillRect(0, 0, w, h); g.strokeStyle = '#102010'; for (var y = 0; y < h; y += 12) { g.beginPath(); g.moveTo(0, y + .5); g.lineTo(w, y + .5); g.stroke(); }
    var b = W.buf; lcd(); if (!b) return;
    var v0 = W.view[0], vl = Math.max(1, W.view[1] - v0), ch = b.numberOfChannels, lh = h / ch;
    if (W.sel[1] > W.sel[0]) { var sx = (W.sel[0] - v0) / vl * w, sw = (W.sel[1] - W.sel[0]) / vl * w; g.fillStyle = 'rgba(227,18,44,.28)'; g.fillRect(sx, 0, sw, h); }
    for (var c = 0; c < ch; c++) {
      var d = b.getChannelData(c), mid = lh * c + lh / 2; g.fillStyle = c ? '#e8c36a' : '#ff3048';
      for (var x = 0; x < w; x++) { var i0 = Math.floor(v0 + x / w * vl), i1 = Math.floor(v0 + (x + 1) / w * vl), mn = 0, mx = 0, st = Math.max(1, Math.floor((i1 - i0) / 200)); for (var i = i0; i < i1 && i < d.length; i += st) { if (d[i] < mn) mn = d[i]; if (d[i] > mx) mx = d[i]; } g.fillRect(x, mid - mx * lh / 2, 1, Math.max(1, (mx - mn) * lh / 2)); }
      g.fillStyle = '#1d3a1d'; g.fillRect(0, mid, w, 1);
    }
    var cx = (W.cur - v0) / vl * w; g.fillStyle = '#fff'; g.fillRect(cx, 0, 1, h);
    if (W.ph != null) { var px = (W.ph - v0) / vl * w; g.fillStyle = '#2fbf5b'; g.fillRect(px, 0, 2, h); }
  }
  // selection by pointer
  var drag = null;
  function sAt(e) { var r = cv.getBoundingClientRect(), x = Math.max(0, Math.min(r.width, e.clientX - r.left)); return Math.round(W.view[0] + x / r.width * (W.view[1] - W.view[0])); }
  cv.addEventListener('pointerdown', function (e) { if (!W.buf) return; cv.setPointerCapture(e.pointerId); drag = sAt(e); W.cur = drag; W.sel = [drag, drag]; draw(); });
  cv.addEventListener('pointermove', function (e) { if (drag == null) return; var s = sAt(e); W.sel = [Math.min(drag, s), Math.max(drag, s)]; draw(); });
  cv.addEventListener('pointerup', function () { drag = null; });
  cv.style.touchAction = 'none';
  function range() { var b = W.buf; return W.sel[1] > W.sel[0] ? [W.sel[0], W.sel[1]] : [0, b.length]; }
  function setBuf(nb, keepSel) { W.undo.push(W.buf); if (W.undo.length > 15) W.undo.shift(); W.redo = []; W.buf = nb; if (!keepSel) W.sel = [0, 0]; W.cur = Math.min(W.cur, nb.length); W.view = [0, nb.length]; draw(); }
  // playback
  var player = null, phRaf = 0;
  function stopPlay() { if (player) { try { player.stop(); } catch (e) {} player = null; } cancelAnimationFrame(phRaf); W.ph = null; draw(); }
  function playBuf(b, from, to, mapStart) {
    stopPlay(); ES.stop(); var c = ctx(), s = c.createBufferSource(); s.buffer = b; s.connect(c.destination);
    var t0 = c.currentTime, off = from / b.sampleRate, dur = (to - from) / b.sampleRate; s.start(t0 + 0.02, off, dur); player = s; s.onended = function () { if (player === s) stopPlay(); };
    (function f() { if (player !== s) return; W.ph = (mapStart != null ? mapStart : from) + (c.currentTime - t0) * b.sampleRate; draw(); phRaf = requestAnimationFrame(f); })();
  }
  // ---- effects
  var CSRF = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
  function srv(seg, fx, amt) {
    var fd = new FormData(); fd.append('fx', fx); fd.append('amount', String(amt)); fd.append('audio', ES.wavEncode(seg, seg.sampleRate), 'seg.wav');
    return fetch('/api/ss/fx', { method: 'POST', credentials: 'same-origin', headers: { 'x-csrf-token': CSRF }, body: fd }).then(function (r) {
      if (!r.ok) return r.json().catch(function () { return {}; }).then(function (j) { throw new Error(j.error || 'Effect failed (' + r.status + ')'); });
      return r.arrayBuffer();
    }).then(function (ab) { return new Promise(function (res, rej) { 