<!-- SS · part 39 -->
ction stopRec() { if (mr && mr.state === 'recording') mr.stop(); else { ES.stop(); stopUI(); } }
  function finish() {
    ES.stop(); stopUI(); var type = (mr && mr.mimeType) || (chunks[0] && chunks[0].type) || 'audio/webm', blob = new Blob(chunks, { type: type });
    if (blob.size < 2000) { $('#es-r-status').textContent = 'Nothing was recorded.'; return; }
    decode(blob).then(function (buf) {
      var n = STEMS.filter(function (s) { return s.kind === 'mic'; }).length + 1, lat = +$('#es-r-lat').value / 1000;
      addStem(blob, buf, { name: 'Take ' + n, kind: 'mic', start: recAt, trimIn: Math.min(lat, buf.duration / 2), fx: { preset: 'polish', verb: 18, echo: 0, comp: 50, level: true } });
      $('#es-r-status').textContent = 'Take ' + n + ' saved on this device (' + buf.duration.toFixed(1) + 's). Mute or delete takes you don\'t want.';
    }).catch(function (e) { $('#es-r-status').textContent = e.message; });
  }
  $('#es-r-rec').addEventListener('click', function () { if (recOn) stopRec(); else startRec(); });
  $('#es-r-lat').addEventListener('input', function () { this._set = 1; $('#es-r-lat-v').textContent = this.value + ' ms'; });
  // ---------- boot: load saved stems, register with EQ + Wave Studio
  function boot() {
    renderStems();
    if (window.ESEQ) ESEQ.setStemApi({
      list: function () { return STEMS; },
      getEq: function (id) { var s = byId(id); if (s && !s.eq) s.eq = ESEQ.flat(); return s && s.eq; },
      setEq: function (id, e) { var s = byId(id); if (s) { s.eq = e; persist(s); } },
      rewire: function (id) { if (ES.isPlaying()) ES.play(ES.curPos()); }
    });
    idb('readonly', function (st) { return st.getAll(); }).then(function (rows) {
      if (!rows || !rows.length) return;
      return Tone.start().catch(function () {}).then(function () {
        return Promise.all(rows.map(function (r) { return decode(r.blob).then(function (buf) { var s = Object.assign({}, r.meta, { blob: r.blob, buf: buf, peaks: peaksOf(buf, 400).p }); if (!s.fx) s.fx = { preset: 'dry', verb: 0, echo: 0, comp: 0, level: true }; return s; }, function () { return null; }); }));
      }).then(function (list) { STEMS = (list || []).filter(Boolean).sort(function (a, b) { return (a.created || 0) - (b.created || 0); }); renderStems(); changed(); });
    }).catch(function () {});
  }
  if (window.ES && !ES) { ES = window.ES; boot(); }
  window.ESREC = { add: function (blob, buf, o) { return addStem(blob, buf, o); }, list: function () { return STEMS; } };
})();


===== FILE: public/ss/studio-rec.js.bak-20261009-sing =====
/* Epistles Studio — Record & Stems: mic takes (count-in, metronome, latency fix), uploads, trim/move/fade/volume/FX, stored in this browser (IndexedDB). Free. */
(function () {
  'use strict';
  var root = document.getElementById('es'); if (!root || !document.getElementById('es-stems')) return;
  function $(s) { return document.querySelector(s); }
  var ES = null, STEMS = [], MAXB = 25 * 1048576;
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
  function toast(m) { if (ES) ES.toast(m, 4000); }
  function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
  // ---------- IndexedDB (blobs survive reloads, stay on this device)
  var dbp = null;
  function db() { if (dbp) return dbp; dbp = new Promise(function (res, rej) { if (!window.indexedDB) return rej(new Error('no idb')); var r = indexedDB.open('es_stems', 1); r.onupgradeneeded = function () { r.result.createObjectStore('stems', { keyPath: 'id' }); }; r.onsuccess = function () { res(r.result); }; r.onerror = function () { rej(r.error); }; }); return dbp; }
  function idb(mode, fn) { return db().then(function (d) { return new Promise(function (res, rej) { var tx = d.transaction('stems', mode), st = tx.objectStore('stems'), out = fn(st); tx.oncomplete = function () { res(out && out.result); }; tx.onerror = function () { rej(tx.error); }; }); }); }
  function meta(s) { var m = {}; ['id', 'name', 'kind', 'start', 'trimIn', 'trimOut', 'fadeIn', 'fadeOut', 'vol', 'mute', 'fx', 'eq', 'norm', 'from', 'created'].forEach(function (k) { m[k] = s[k]; }); return m; }
  var putT = {};
  function persist(s, withBlob) { clearTimeout(putT[s.id]); putT[s.id] = setTimeout(function () { idb('readwrite', function (st) { return st.put({ id: s.id, meta: meta(s), blob: s.blob }); }).catch(function () {}); }, withBlob ? 0 : 300); }
  // ---------- stem model
  var FXP = { dry: { verb: 0, echo: 0, comp: 0 }, polish: { verb: 18, echo: 0, comp: 50 }, radio: { verb: 5, echo: 0, comp: 80 }, room: { verb: 35, echo: 0, comp: 30 }, echo: { verb: 15, echo: 40, comp: 30 } };
  function decode(blob) { return blob.arrayBuffer().then(function (ab) { return new Promise(function (res, rej) { Tone.getContext().rawContext.decodeAudioData(ab, res, function () { rej(new Error('Could not read 