X Space Volume Control

Adds volume control slider and audio compressor to X (Twitter) Spaces panel

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         X Space Volume Control
// @version      1.0.2.2602260411
// @description  Adds volume control slider and audio compressor to X (Twitter) Spaces panel
// @author       NoCompromise
// @namespace    nocompromise
// @license      MIT
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  var W = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
  var TAG = '[X Space Volume Control]';
  var VER = '1.0.2.2602260411';

  var STORAGE_KEY = 'x-space-vol';
  var DEFAULT_VOL = 1.0;
  var DEBOUNCE_MS = 400;
  var VOL_ATTR = 'data-x-space-vol';

  var COMP_STORAGE_KEY = 'x-space-comp';
  var COMP_DEFAULTS = {
    enabled: false,
    threshold: -50,
    knee: 40,
    ratio: 12,
    attack: 0,
    release: 0.25,
    gain: 1
  };

  var LIVE_SVG = 'M6.662 18H';
  var REC_SVG  = 'M5.747 12c';

  var COMP_BARS = 'M850 202.3C877.7 202.3 900 224.6 900 252.3V745.5C900 773.2 877.7 795.5 850 795.5S800 773.2 800 745.5V252.3C800 224.6 822.3 202.3 850 202.3ZM570 167.8C597.7 167.8 620 190.1 620 217.8V780C620 807.7 597.7 830 570 830S520 807.7 520 780V217.8C520 190.1 542.3 167.8 570 167.8ZM710 264.4C737.7 264.4 760 286.7 760 314.4V683.3C760 711 737.7 733.3 710 733.3S660 711 660 683.3V314.4C660 286.7 682.3 264.4 710 264.4ZM430 98.1C457.7 98.1 480 120.4 480 148.1V849.6C480 877.3 457.7 899.6 430 899.6S380 877.3 380 849.6V148.1C380 120.4 402.3 98.1 430 98.1ZM290 217.2C317.7 217.2 340 239.5 340 267.2V730.5C340 758.2 317.7 780.5 290 780.5S240 758.2 240 730.5V267.2C240 239.5 262.3 217.2 290 217.2ZM150 299.6C177.7 299.6 200 321.9 200 349.6V648.1C200 675.8 177.7 698.1 150 698.1S100 675.8 100 648.1V349.6C100 321.9 122.3 299.6 150 299.6Z';

  var GEAR_PATH = 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94L14.4 2.81c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41L9.25 5.35c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.22-.07.47.12.61l2.03 1.58c-.05.3-.07.63-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.57 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.03-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z';

  var _isKo = /^ko/i.test((navigator.language || navigator.userLanguage || 'en'));
  var I18N = {
    mute: _isKo ? '음소거' : 'Mute',
    unmute: _isKo ? '음소거 해제' : 'Unmute',
    compOn: _isKo ? '오디오 컴프레서 활성화' : 'Enable Audio Compressor',
    compOff: _isKo ? '오디오 컴프레서 비활성화' : 'Disable Audio Compressor',
    compSettings: _isKo ? '컴프레서 설정' : 'Compressor Settings'
  };

  var _vol = -1;

  function loadVol() {
    if (_vol >= 0) return _vol;
    try {
      var v = parseFloat(localStorage.getItem(STORAGE_KEY));
      _vol = isNaN(v) ? DEFAULT_VOL : Math.max(0, Math.min(1, v));
    } catch (_) { _vol = DEFAULT_VOL; }
    return _vol;
  }

  function saveVol(v) {
    _vol = v;
    try { localStorage.setItem(STORAGE_KEY, String(v)); } catch (_) {}
  }

  var _compS = null;

  function loadCompSettings() {
    if (_compS) return _compS;
    try {
      var raw = localStorage.getItem(COMP_STORAGE_KEY);
      if (raw) {
        var p = JSON.parse(raw);
        if (typeof p === 'number') p = {};
        _compS = {
          enabled: !!p.enabled,
          threshold: typeof p.threshold === 'number' ? p.threshold : COMP_DEFAULTS.threshold,
          knee: typeof p.knee === 'number' ? p.knee : COMP_DEFAULTS.knee,
          ratio: typeof p.ratio === 'number' ? p.ratio : COMP_DEFAULTS.ratio,
          attack: typeof p.attack === 'number' ? p.attack : COMP_DEFAULTS.attack,
          release: typeof p.release === 'number' ? p.release : COMP_DEFAULTS.release,
          gain: typeof p.gain === 'number' ? p.gain : COMP_DEFAULTS.gain
        };
      } else {
        _compS = copyDefaults();
      }
    } catch (_) {
      _compS = copyDefaults();
    }
    return _compS;
  }

  function saveCompSettings() {
    try { localStorage.setItem(COMP_STORAGE_KEY, JSON.stringify(_compS)); } catch (_) {}
  }

  function copyDefaults() {
    return { enabled: false, threshold: COMP_DEFAULTS.threshold, knee: COMP_DEFAULTS.knee,
      ratio: COMP_DEFAULTS.ratio, attack: COMP_DEFAULTS.attack,
      release: COMP_DEFAULTS.release, gain: COMP_DEFAULTS.gain };
  }

  var _gains = [];
  var _trackedEls = [];
  var _origConnect = null;
  var _origAC = null;

  var _chainComps = [];

  var _compCtx = null;
  var _compNode = null;
  var _compGainNode = null;
  var _compVolNode = null;
  var _compSources = [];
  var _compEnabled = false;
  var _compGen = 0;
  var _vcTime = 0;

  function createChain(ctx, realDest) {
    var cn = _origConnect || W.AudioNode.prototype.connect;
    var g = ctx.createGain();
    g.__xsvc = true;
    g.gain.value = loadVol();
    cn.call(g, realDest);
    ctx.__xsvcG = g;
    _gains.push(g);
    try {
      var s = loadCompSettings();
      var cg = ctx.createGain();
      cg.__xsvc = true;
      cg.gain.value = _compEnabled ? s.gain : 1;
      cn.call(cg, g);
      var c = ctx.createDynamicsCompressor();
      c.__xsvc = true;
      if (_compEnabled) {
        c.threshold.value = s.threshold;
        c.knee.value = s.knee;
        c.ratio.value = s.ratio;
        c.attack.value = s.attack;
        c.release.value = s.release;
      } else {
        c.threshold.value = 0; c.ratio.value = 1;
        c.knee.value = 0; c.attack.value = 0; c.release.value = 0;
      }
      cn.call(c, cg);
      ctx.__xsvcC = c;
      _chainComps.push({ comp: c, gain: cg });
    } catch (_) {
      ctx.__xsvcC = g;
    }
  }

  function hookAudioContext() {
    var AC = W.AudioContext || W.webkitAudioContext;
    if (!AC) return;
    if (typeof W.AudioNode !== 'undefined' && !_origConnect) {
      _origConnect = W.AudioNode.prototype.connect;
    }
    var OrigAC = AC;
    function WrappedAC() {
      var ctx = (arguments.length === 0) ? new OrigAC() : new OrigAC(arguments[0]);
      try {
        var realDest = ctx.destination;
        createChain(ctx, realDest);
        Object.defineProperty(ctx, 'destination', {
          get: function () { return this.__xsvcC || this.__xsvcG || realDest; },
          configurable: true, enumerable: true
        });
      } catch (_) {}
      return ctx;
    }
    WrappedAC.prototype = OrigAC.prototype;
    try {
      var props = Object.getOwnPropertyNames(OrigAC);
      for (var i = 0; i < props.length; i++) {
        var p = props[i];
        if (p !== 'prototype' && p !== 'length' && p !== 'name') {
          try { WrappedAC[p] = OrigAC[p]; } catch (_) {}
        }
      }
    } catch (_) {}
    W.AudioContext = WrappedAC;
    if (W.webkitAudioContext === OrigAC) W.webkitAudioContext = WrappedAC;
  }

  function hookDestination() {
    try {
      var AC = W.AudioContext || W.webkitAudioContext;
      if (!AC) return;
      var proto = (typeof W.BaseAudioContext !== 'undefined')
        ? W.BaseAudioContext.prototype : AC.prototype;
      var desc = Object.getOwnPropertyDescriptor(proto, 'destination');
      if (!desc || !desc.get) return;
      var origGet = desc.get;
      Object.defineProperty(proto, 'destination', {
        get: function () {
          var real = origGet.call(this);
          if (typeof W.OfflineAudioContext !== 'undefined' &&
              this instanceof W.OfflineAudioContext) return real;
          if (this.__xsvcInit) return real;
          if (!this.__xsvcG) {
            this.__xsvcInit = true;
            try { createChain(this, real); }
            catch (_) { return real; }
            finally { this.__xsvcInit = false; }
          }
          return this.__xsvcC || this.__xsvcG;
        },
        configurable: true, enumerable: desc.enumerable
      });
    } catch (_) {}
  }

  function hookConnect() {
    if (typeof W.AudioNode === 'undefined') return;
    if (!_origConnect) _origConnect = W.AudioNode.prototype.connect;
    W.AudioNode.prototype.connect = function (dest) {
      if (dest instanceof W.AudioDestinationNode && !this.__xsvc) {
        var ctx = dest.context;
        if (!ctx.__xsvcG) {
          try { createChain(ctx, dest); }
          catch (_) { return _origConnect.apply(this, arguments); }
        }
        return _origConnect.call(this, ctx.__xsvcC || ctx.__xsvcG);
      }
      return _origConnect.apply(this, arguments);
    };
  }

  function hookSrcObject() {
    try {
      var desc = Object.getOwnPropertyDescriptor(W.HTMLMediaElement.prototype, 'srcObject');
      if (!desc || !desc.set) return;
      var origSet = desc.set;
      var origGet = desc.get;
      Object.defineProperty(W.HTMLMediaElement.prototype, 'srcObject', {
        get: origGet,
        set: function (val) {
          var result = origSet.call(this, val);
          if (val) {
            trackEl(this);
            enforceElState(this);
          }
          return result;
        },
        configurable: true, enumerable: desc.enumerable
      });
    } catch (_) {}
  }

  function ensureCompCtx() {
    if (_compCtx) return true;
    if (!_origAC) return false;
    try {
      _compCtx = new _origAC();
      _compVolNode = _compCtx.createGain();
      _compVolNode.__xsvc = true;
      _compVolNode.gain.value = loadVol();
      _origConnect.call(_compVolNode, _compCtx.destination);
      _gains.push(_compVolNode);
      _compNode = _compCtx.createDynamicsCompressor();
      _compNode.__xsvc = true;
      _compGainNode = _compCtx.createGain();
      _compGainNode.__xsvc = true;
      var s = loadCompSettings();
      _compNode.threshold.value = s.threshold;
      _compNode.knee.value = s.knee;
      _compNode.ratio.value = s.ratio;
      _compNode.attack.value = s.attack;
      _compNode.release.value = s.release;
      _compGainNode.gain.value = s.gain;
      _origConnect.call(_compNode, _compGainNode);
      _origConnect.call(_compGainNode, _compVolNode);
      return true;
    } catch (_) { return false; }
  }

  function routeElement(el) {
    if (el.__xsvcCompSrc || !_compCtx) return;
    try {
      var src = _compCtx.createMediaElementSource(el);
      src.__xsvc = true;
      el.__xsvcCompSrc = src;
      el.volume = 1;
      _compSources.push({ el: el, src: src });
      if (_compEnabled) {
        _origConnect.call(src, _compNode);
      } else {
        _origConnect.call(src, _compVolNode);
      }
    } catch (_) {}
  }

  function enforceElState(el) {
    if (_compEnabled && _compCtx && !el.__xsvcCompSrc) {
      if (_compCtx.state === 'running') {
        routeElement(el);
      } else if (_compCtx.state === 'suspended') {
        var gen = _compGen;
        try {
          _compCtx.resume().then(function () {
            if (_compEnabled && _compGen === gen && !el.__xsvcCompSrc) routeElement(el);
          });
        } catch (_) {}
      }
    }
    if (el.__xsvcCompSrc) {
      if (el.volume !== 1) el.volume = 1;
    } else {
      var v = loadVol();
      if (Math.abs(el.volume - v) > 0.01) {
        _vcTime = Date.now();
        el.volume = v;
      }
    }
  }

  function trackEl(el) {
    for (var i = 0; i < _trackedEls.length; i++) {
      if (_trackedEls[i] === el) return;
    }
    _trackedEls.push(el);
    if (!el.__xsvcVCL) {
      el.__xsvcVCL = true;
      var fn = function () { enforceElState(el); };
      el.addEventListener('volumechange', fn);
      el.addEventListener('playing', fn);
    }
  }

  function doCompRouting() {
    for (var i = 0; i < _trackedEls.length; i++) {
      if (!_trackedEls[i].__xsvcCompSrc) routeElement(_trackedEls[i]);
    }
    for (var j = 0; j < _compSources.length; j++) {
      try {
        _compSources[j].src.disconnect();
        _origConnect.call(_compSources[j].src, _compNode);
      } catch (_) {}
    }
    updateAllComps();
  }

  function enableCompressor() {
    if (!ensureCompCtx()) return;
    _compEnabled = true;
    var gen = ++_compGen;
    var s = loadCompSettings(); s.enabled = true; saveCompSettings();
    if (_compCtx.state === 'suspended') {
      var tryEn, tmr;
      tryEn = function () {
        clearTimeout(tmr);
        _compCtx.removeEventListener('statechange', tryEn);
        if (!_compEnabled || _compGen !== gen) return;
        if (_compCtx.state !== 'suspended') doCompRouting();
      };
      tmr = setTimeout(tryEn, 150);
      try {
        _compCtx.addEventListener('statechange', tryEn);
        _compCtx.resume();
      } catch (_) {}
      return;
    }
    doCompRouting();
  }

  function disableCompressor() {
    _compEnabled = false;
    ++_compGen;
    if (_compCtx && _compCtx.state === 'suspended') {
      try { _compCtx.resume(); } catch (_) {}
    }
    if (_compVolNode) {
      for (var i = 0; i < _compSources.length; i++) {
        try {
          _compSources[i].src.disconnect();
          _origConnect.call(_compSources[i].src, _compVolNode);
        } catch (_) {}
      }
    }
    bypassChainComps();
    var s = loadCompSettings(); s.enabled = false; saveCompSettings();
  }

  function updateAllComps() {
    var s = loadCompSettings();
    if (_compNode) {
      _compNode.threshold.value = s.threshold;
      _compNode.knee.value = s.knee;
      _compNode.ratio.value = s.ratio;
      _compNode.attack.value = s.attack;
      _compNode.release.value = s.release;
    }
    if (_compGainNode) _compGainNode.gain.value = s.gain;
    for (var i = _chainComps.length - 1; i >= 0; i--) {
      try {
        var cc = _chainComps[i];
        if (cc.comp.context.state === 'closed') { _chainComps.splice(i, 1); continue; }
        cc.comp.threshold.value = s.threshold;
        cc.comp.knee.value = s.knee;
        cc.comp.ratio.value = s.ratio;
        cc.comp.attack.value = s.attack;
        cc.comp.release.value = s.release;
        cc.gain.gain.value = s.gain;
      } catch (_) { _chainComps.splice(i, 1); }
    }
  }

  function bypassChainComps() {
    for (var i = _chainComps.length - 1; i >= 0; i--) {
      try {
        var cc = _chainComps[i];
        if (cc.comp.context.state === 'closed') { _chainComps.splice(i, 1); continue; }
        cc.comp.threshold.value = 0; cc.comp.ratio.value = 1;
        cc.comp.knee.value = 0; cc.comp.attack.value = 0; cc.comp.release.value = 0;
        cc.gain.gain.value = 1;
      } catch (_) { _chainComps.splice(i, 1); }
    }
  }

  function applyGainVol(vol) {
    for (var i = _gains.length - 1; i >= 0; i--) {
      try {
        if (_gains[i].context.state === 'closed') { _gains.splice(i, 1); continue; }
        _gains[i].gain.value = vol;
      } catch (_) { _gains.splice(i, 1); }
    }
  }

  function hookPlay() {
    var orig = W.HTMLMediaElement.prototype.play;
    W.HTMLMediaElement.prototype.play = function () {
      trackEl(this);
      enforceElState(this);
      return orig.apply(this, arguments);
    };
  }

  function hookAudioCtor() {
    var _A = W.Audio;
    if (!_A) return;
    try {
      W.Audio = function () {
        var a = arguments.length ? new _A(arguments[0]) : new _A();
        trackEl(a);
        enforceElState(a);
        return a;
      };
      W.Audio.prototype = _A.prototype;
    } catch (_) {}
  }

  function setupMediaListeners() {
    document.addEventListener('playing', function (e) {
      if (e.target instanceof W.HTMLMediaElement) {
        trackEl(e.target);
        enforceElState(e.target);
      }
    }, true);

    document.addEventListener('volumechange', function (e) {
      if (Date.now() - _vcTime < 200) return;
      var el = e.target;
      if (!(el instanceof W.HTMLMediaElement) || el.muted) return;
      enforceElState(el);
    }, true);
  }

  function applyMediaVol(vol) {
    _vcTime = Date.now();
    var tags = ['audio', 'video'];
    for (var t = 0; t < tags.length; t++) {
      var els = document.getElementsByTagName(tags[t]);
      for (var i = 0; i < els.length; i++) {
        if (!els[i].__xsvcCompSrc && els[i].volume !== vol) els[i].volume = vol;
      }
    }
    for (var j = _trackedEls.length - 1; j >= 0; j--) {
      try {
        var el = _trackedEls[j];
        if (!el.__xsvcCompSrc && el.volume !== vol) el.volume = vol;
      } catch (_) { _trackedEls.splice(j, 1); }
    }
  }

  function applyVolAll(vol) {
    applyGainVol(vol);
    applyMediaVol(vol);
  }

  function findAnchorBtn(prefix) {
    var paths = document.querySelectorAll('svg path[d]');
    for (var i = 0; i < paths.length; i++) {
      var d = paths[i].getAttribute('d');
      if (d && d.lastIndexOf(prefix, 0) === 0) {
        var el = paths[i];
        for (var j = 0; j < 8; j++) {
          el = el.parentElement;
          if (!el) break;
          if (el.tagName === 'BUTTON' || el.tagName === 'A') return el;
        }
      }
    }
    return null;
  }

  function findInner(button) {
    var el = button;
    for (var i = 0; i < 12; i++) {
      el = el.parentElement;
      if (!el || !el.parentElement) continue;
      try {
        var cs = W.getComputedStyle(el.parentElement);
        if ((cs.position === 'absolute' || cs.position === 'fixed') &&
            parseFloat(cs.bottom) === 0) {
          return el;
        }
      } catch (_) {}
    }
    el = button;
    for (var k = 0; k < 4; k++) { if (el.parentElement) el = el.parentElement; }
    return el;
  }

  function detectPanel() {
    var btn = findAnchorBtn(LIVE_SVG);
    if (btn) return { btn: btn, type: 'live' };
    btn = findAnchorBtn(REC_SVG);
    if (btn) return { btn: btn, type: 'rec' };
    return null;
  }

  var _cssOk = false;

  function injectCSS() {
    if (_cssOk) return;
    _cssOk = true;
    var s = '[' + VOL_ATTR + '] input[type=range]';
    var c =
      s + '{-webkit-appearance:none;appearance:none;background:transparent;margin:0;' +
          'touch-action:none;}' +
      s + '::-webkit-slider-runnable-track{height:4px;background:rgb(56,68,77);border-radius:2px;}' +
      s + '::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;' +
          'background:rgb(29,155,240);margin-top:-5px;cursor:pointer;border:none;}' +
      s + '::-webkit-slider-thumb:hover{background:rgb(26,140,216);}' +
      s + '::-moz-range-track{height:4px;background:rgb(56,68,77);border-radius:2px;border:none;}' +
      s + '::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:rgb(29,155,240);' +
          'cursor:pointer;border:none;}' +
      s + '::-moz-range-thumb:hover{background:rgb(26,140,216);}';
    var el = document.createElement('style');
    el.textContent = c;
    (document.head || document.documentElement).appendChild(el);
  }

  function spkIcon(vol) {
    var c = 'rgb(239,243,244)';
    if (vol <= 0)
      return '<svg viewBox="0 0 24 24" width="18" height="18" fill="' + c + '"><path d="M3.63 3.63c-.39.39-.39 1.02 0 1.41L7.29 8.7 7 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-4.17l4.18 4.18c-.49.37-1.02.68-1.59.91-.36.15-.58.53-.58.92 0 .72.73 1.18 1.39.91.8-.33 1.55-.77 2.22-1.31l1.34 1.34c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L5.05 3.63c-.39-.39-1.02-.39-1.42 0zM19 12c0 .82-.15 1.61-.41 2.34l1.53 1.53c.56-1.17.88-2.48.88-3.87 0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zm-8.71-6.29l-.17.17L12 7.76V6.41c0-.89-1.08-1.33-1.71-.7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v1.79l2.48 2.48c.01-.08.02-.16.02-.24z"/></svg>';
    if (vol < 0.5)
      return '<svg viewBox="0 0 24 24" width="18" height="18" fill="' + c + '"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>';
    return '<svg viewBox="0 0 24 24" width="18" height="18" fill="' + c + '"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>';
  }

  function compIcon(enabled) {
    var bars = enabled ? 'rgb(239,243,244)' : 'rgb(113,118,123)';
    var dot  = enabled ? 'rgb(29,155,240)' : '#838285';
    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-300 -300 1600 1600" width="18" height="18">' +
      '<path fill="' + bars + '" d="' + COMP_BARS + '"/>' +
      '<circle r="160" cx="900" cy="800" fill="' + dot + '"/></svg>';
  }

  function gearIcon(active) {
    var c = active ? 'rgb(29,155,240)' : 'rgb(139,152,165)';
    return '<svg viewBox="0 0 24 24" width="16" height="16" fill="' + c + '"><path d="' + GEAR_PATH + '"/></svg>';
  }

  function addWheel(el, slider, step) {
    el.addEventListener('wheel', function (e) {
      e.preventDefault();
      var d = e.deltaY < 0 ? step : -step;
      var mn = parseFloat(slider.min), mx = parseFloat(slider.max);
      var v = Math.round(Math.max(mn, Math.min(mx, parseFloat(slider.value) + d)) * 1e4) / 1e4;
      slider.value = String(v);
      slider.dispatchEvent(new Event('input', { bubbles: true }));
    }, { passive: false });
  }

  function mkTooltip(parent, text) {
    var tip = document.createElement('div');
    tip.textContent = text;
    tip.style.cssText = 'position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);' +
      'background:rgba(46,48,51,.95);border:1px solid rgba(0,0,0,.5);border-radius:6px;' +
      'box-shadow:0 2px 6px rgba(0,0,0,.5);color:#dfe2ea;font-size:12px;line-height:1.5;' +
      'padding:5px 9px;pointer-events:none;white-space:nowrap;z-index:1000;' +
      'opacity:0;transition:opacity .15s ease;' +
      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
    parent.style.position = 'relative';
    parent.appendChild(tip);
    parent.addEventListener('mouseenter', function () { tip.style.opacity = '1'; });
    parent.addEventListener('mouseleave', function () { tip.style.opacity = '0'; });
    return tip;
  }

  function fmtCompVal(key, v, unit) {
    if (key === 'ratio') return v.toFixed(1);
    if (key === 'attack' || key === 'release') return v.toFixed(2) + ' ' + unit;
    return String(Math.round(v)) + ' ' + unit;
  }

  function mkSlider(vol) {
    injectCSS();
    var cs = loadCompSettings();

    var wrapper = document.createElement('div');
    wrapper.setAttribute(VOL_ATTR, '1');

    var w = document.createElement('div');
    w.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 12px;' +
      'user-select:none;box-sizing:border-box;border-bottom:1px solid rgb(47,51,54);';

    var vg = document.createElement('div');
    vg.style.cssText = 'display:flex;align-items:center;gap:6px;flex:1;min-width:0;';

    var vic = document.createElement('div');
    vic.style.cssText = 'flex-shrink:0;width:18px;height:18px;cursor:pointer;' +
      'display:flex;align-items:center;justify-content:center;';
    vic.innerHTML = spkIcon(vol);
    var vTip = mkTooltip(vic, vol > 0 ? I18N.mute : I18N.unmute);

    var vsl = document.createElement('input');
    vsl.type = 'range'; vsl.min = '0'; vsl.max = '100';
    vsl.value = String(Math.round(vol * 100));
    vsl.setAttribute('aria-label', 'Space Volume');
    vsl.style.cssText = 'flex:1;height:4px;cursor:pointer;min-width:40px;';

    var vpc = document.createElement('span');
    vpc.style.cssText = 'flex-shrink:0;font-size:12px;color:rgb(139,152,165);' +
      'min-width:28px;text-align:right;' +
      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
    vpc.textContent = Math.round(vol * 100) + '%';

    vg.appendChild(vic); vg.appendChild(vsl); vg.appendChild(vpc);

    var sep = document.createElement('div');
    sep.style.cssText = 'width:1px;height:16px;background:rgb(56,68,77);flex-shrink:0;';

    var cg = document.createElement('div');
    cg.style.cssText = 'display:flex;align-items:center;gap:6px;flex:1;min-width:0;';

    var sic = document.createElement('div');
    sic.style.cssText = 'flex-shrink:0;width:16px;height:16px;cursor:pointer;' +
      'display:flex;align-items:center;justify-content:center;';
    sic.innerHTML = gearIcon(false);
    var sTip = mkTooltip(sic, I18N.compSettings);

    var cic = document.createElement('div');
    cic.style.cssText = 'flex-shrink:0;width:18px;height:18px;cursor:pointer;' +
      'display:flex;align-items:center;justify-content:center;';
    cic.innerHTML = compIcon(_compEnabled);
    var cTip = mkTooltip(cic, _compEnabled ? I18N.compOff : I18N.compOn);

    var gsl = document.createElement('input');
    gsl.type = 'range'; gsl.min = '0'; gsl.max = '200';
    gsl.value = String(Math.round(cs.gain * 100));
    gsl.setAttribute('aria-label', 'Compressor Gain');
    gsl.style.cssText = 'flex:1;height:4px;cursor:pointer;min-width:40px;';

    var gpc = document.createElement('span');
    gpc.style.cssText = 'flex-shrink:0;font-size:12px;color:rgb(139,152,165);' +
      'min-width:32px;text-align:right;' +
      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
    gpc.textContent = Math.round(cs.gain * 100) + '%';

    cg.appendChild(sic); cg.appendChild(cic); cg.appendChild(gsl); cg.appendChild(gpc);

    w.appendChild(vg); w.appendChild(sep); w.appendChild(cg);
    wrapper.appendChild(w);

    var sp = document.createElement('div');
    sp.style.cssText = 'padding:8px 12px;border-bottom:1px solid rgb(47,51,54);display:none;';

    var spTitle = document.createElement('div');
    spTitle.style.cssText = 'font-size:11px;font-weight:600;color:rgb(139,152,165);margin-bottom:6px;' +
      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
    spTitle.textContent = I18N.compSettings;
    sp.appendChild(spTitle);

    var compParams = [
      { key: 'threshold', label: 'Threshold', min: -100, max: 0, step: 1, unit: 'dB' },
      { key: 'knee',      label: 'Knee',      min: 0,    max: 40, step: 1, unit: 'dB' },
      { key: 'ratio',     label: 'Ratio',     min: 1,    max: 20, step: 0.5, unit: '' },
      { key: 'attack',    label: 'Attack',    min: 0,    max: 1,  step: 0.01, unit: 's' },
      { key: 'release',   label: 'Release',   min: 0,    max: 1,  step: 0.01, unit: 's' }
    ];

    var sSt = 0;
    var settingsSliders = [];
    for (var pi = 0; pi < compParams.length; pi++) {
      (function (p) {
        var row = document.createElement('div');
        row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:4px;';

        var lbl = document.createElement('span');
        lbl.style.cssText = 'font-size:11px;color:rgb(139,152,165);width:60px;flex-shrink:0;' +
          'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
        lbl.textContent = p.label;

        var sl = document.createElement('input');
        sl.type = 'range';
        sl.min = String(p.min); sl.max = String(p.max); sl.step = String(p.step);
        sl.value = String(cs[p.key]);
        sl.style.cssText = 'flex:1;height:4px;cursor:pointer;min-width:40px;';

        var vl = document.createElement('span');
        vl.style.cssText = 'font-size:11px;color:rgb(139,152,165);width:52px;text-align:right;flex-shrink:0;' +
          'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
        vl.textContent = fmtCompVal(p.key, cs[p.key], p.unit);

        sl.addEventListener('input', function () {
          var v = parseFloat(this.value);
          vl.textContent = fmtCompVal(p.key, v, p.unit);
          var s = loadCompSettings();
          s[p.key] = v;
          if (_compEnabled) updateAllComps();
          if (sSt) clearTimeout(sSt);
          sSt = setTimeout(saveCompSettings, 250);
        });

        addWheel(row, sl, p.step);
        settingsSliders.push({ sl: sl, vl: vl, p: p });

        row.appendChild(lbl); row.appendChild(sl); row.appendChild(vl);
        sp.appendChild(row);
      })(compParams[pi]);
    }

    var rstBtn = document.createElement('button');
    rstBtn.textContent = _isKo ? '기본값 초기화' : 'Reset to Default';
    rstBtn.style.cssText = 'margin-top:4px;padding:3px 10px;border:1px solid rgb(56,68,77);border-radius:4px;' +
      'background:transparent;color:rgb(139,152,165);font-size:11px;cursor:pointer;' +
      'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';
    rstBtn.addEventListener('mouseenter', function () { this.style.background = 'rgb(39,44,48)'; });
    rstBtn.addEventListener('mouseleave', function () { this.style.background = 'transparent'; });
    rstBtn.addEventListener('click', function (e) {
      e.stopPropagation();
      var s = loadCompSettings();
      for (var ri = 0; ri < settingsSliders.length; ri++) {
        var si = settingsSliders[ri];
        var def = COMP_DEFAULTS[si.p.key];
        s[si.p.key] = def;
        si.sl.value = String(def);
        si.vl.textContent = fmtCompVal(si.p.key, def, si.p.unit);
      }
      s.gain = COMP_DEFAULTS.gain;
      gsl.value = String(Math.round(COMP_DEFAULTS.gain * 100));
      gpc.textContent = Math.round(COMP_DEFAULTS.gain * 100) + '%';
      if (_compGainNode) _compGainNode.gain.value = COMP_DEFAULTS.gain;
      for (var ci = 0; ci < _chainComps.length; ci++) {
        try { _chainComps[ci].gain.gain.value = COMP_DEFAULTS.gain; } catch (_) {}
      }
      if (_compEnabled) updateAllComps();
      saveCompSettings();
    });
    sp.appendChild(rstBtn);

    wrapper.appendChild(sp);

    var vPrev = vol > 0 ? vol : DEFAULT_VOL;
    var vTier = vol <= 0 ? 0 : vol < 0.5 ? 1 : 2;
    var vRaf = 0, vPend = vol, vSt = 0;

    vsl.addEventListener('input', function () {
      var v = parseInt(this.value, 10) / 100;
      if (v > 0) vPrev = v;
      _vol = v;
      vpc.textContent = Math.round(v * 100) + '%';
      var t = v <= 0 ? 0 : v < 0.5 ? 1 : 2;
      if (t !== vTier) { vTier = t; vic.innerHTML = spkIcon(v); vic.appendChild(vTip); }
      vTip.textContent = v > 0 ? I18N.mute : I18N.unmute;
      vPend = v;
      if (!vRaf) { vRaf = W.requestAnimationFrame(function () { vRaf = 0; applyVolAll(vPend); }); }
      if (vSt) clearTimeout(vSt);
      vSt = setTimeout(function () { saveVol(v); }, 250);
    });

    vsl.addEventListener('change', function () {
      var v = parseInt(this.value, 10) / 100;
      _vol = v; applyVolAll(v); saveVol(v);
    });

    vic.addEventListener('click', function (e) {
      e.stopPropagation();
      var cur = parseInt(vsl.value, 10) / 100;
      var target;
      if (cur > 0) { vPrev = cur; target = 0; }
      else { target = vPrev; }
      vsl.value = String(Math.round(target * 100));
      vpc.textContent = Math.round(target * 100) + '%';
      var t = target <= 0 ? 0 : target < 0.5 ? 1 : 2;
      if (t !== vTier) { vTier = t; vic.innerHTML = spkIcon(target); vic.appendChild(vTip); }
      vTip.textContent = target > 0 ? I18N.mute : I18N.unmute;
      _vol = target; applyVolAll(target); saveVol(target);
    });

    cic.addEventListener('click', function (e) {
      e.stopPropagation();
      if (_compEnabled) {
        disableCompressor();
        cic.innerHTML = compIcon(false);
        cic.appendChild(cTip);
        cTip.textContent = I18N.compOn;
      } else {
        enableCompressor();
        cic.innerHTML = compIcon(true);
        cic.appendChild(cTip);
        cTip.textContent = I18N.compOff;
      }
    });

    var gSt = 0;

    gsl.addEventListener('input', function () {
      var v = parseInt(this.value, 10) / 100;
      gpc.textContent = Math.round(v * 100) + '%';
      var s = loadCompSettings();
      s.gain = v;
      if (_compGainNode) _compGainNode.gain.value = v;
      for (var i = 0; i < _chainComps.length; i++) {
        try { _chainComps[i].gain.gain.value = v; } catch (_) {}
      }
      if (gSt) clearTimeout(gSt);
      gSt = setTimeout(saveCompSettings, 250);
    });

    gsl.addEventListener('change', function () {
      var v = parseInt(this.value, 10) / 100;
      var s = loadCompSettings();
      s.gain = v;
      if (_compGainNode) _compGainNode.gain.value = v;
      for (var i = 0; i < _chainComps.length; i++) {
        try { _chainComps[i].gain.gain.value = v; } catch (_) {}
      }
      saveCompSettings();
    });

    var spOpen = false;
    sic.addEventListener('click', function (e) {
      e.stopPropagation();
      spOpen = !spOpen;
      sp.style.display = spOpen ? 'block' : 'none';
      w.style.borderBottom = spOpen ? 'none' : '1px solid rgb(47,51,54)';
      sic.innerHTML = gearIcon(spOpen);
      sic.appendChild(sTip);
    });

    addWheel(vg, vsl, 2);
    addWheel(cg, gsl, 2);

    if (cs.enabled && !_compEnabled) enableCompressor();

    return wrapper;
  }

  function tryInsert() {
    if (document.querySelector('[' + VOL_ATTR + ']')) return true;
    var p = detectPanel();
    if (!p) return false;
    var inner = findInner(p.btn);
    if (!inner) return false;
    var vol = loadVol();
    var slider = mkSlider(vol);
    if (inner.firstChild) {
      inner.insertBefore(slider, inner.firstChild);
    } else {
      inner.appendChild(slider);
    }
    if (p.type === 'rec') {
      var outer = inner.parentElement;
      if (outer) {
        var sibling = outer.previousElementSibling;
        if (sibling) {
          var pb = parseFloat(W.getComputedStyle(sibling).paddingBottom) || 0;
          sibling.style.paddingBottom = (pb + 32) + 'px';
        }
      }
    }
    applyVolAll(vol);
    console.info(TAG, 'v' + VER + ' (' + p.type + ') vol=' + Math.round(vol * 100) + '% comp=' + (_compEnabled ? 'ON' : 'OFF'));
    return true;
  }

  var _obs = null;
  var _dId = 0;

  function onMut() {
    if (_dId) return;
    _dId = setTimeout(function () {
      _dId = 0;
      if (!document.querySelector('[' + VOL_ATTR + ']')) {
        tryInsert();
      }
    }, DEBOUNCE_MS);
  }

  function startObs() {
    if (_obs) return;
    tryInsert();
    _obs = new MutationObserver(onMut);
    _obs.observe(document.body, { childList: true, subtree: true });
  }

  if (typeof W.AudioNode !== 'undefined') {
    _origConnect = W.AudioNode.prototype.connect;
  }
  _origAC = W.AudioContext || W.webkitAudioContext || null;

  hookAudioContext();
  hookDestination();
  hookConnect();

  hookSrcObject();
  hookPlay();
  hookAudioCtor();

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', function () {
      setupMediaListeners();
      startObs();
    });
  } else {
    setupMediaListeners();
    startObs();
  }

  var _push = W.history.pushState;
  var _repl = W.history.replaceState;

  W.history.pushState = function () {
    var r = _push.apply(this, arguments);
    onMut();
    return r;
  };

  W.history.replaceState = function () {
    var r = _repl.apply(this, arguments);
    onMut();
    return r;
  };

  W.addEventListener('popstate', onMut);

})();