Equalizer

Press Alt+X to open/close. (Note: Does not work on DRM-protected sites like Netflix/Spotify).

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Equalizer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Press Alt+X to open/close. (Note: Does not work on DRM-protected sites like Netflix/Spotify).
// @author       HyakuAr
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

/* jshint esversion: 11 */

(function () {
  "use strict";

  const BANDS = [
    { freq: 32, label: "32", type: "lowshelf" },
    { freq: 64, label: "64", type: "peaking" },
    { freq: 125, label: "125", type: "peaking" },
    { freq: 250, label: "250", type: "peaking" },
    { freq: 500, label: "500", type: "peaking" },
    { freq: 1000, label: "1K", type: "peaking" },
    { freq: 2000, label: "2K", type: "peaking" },
    { freq: 4000, label: "4K", type: "peaking" },
    { freq: 8000, label: "8K", type: "peaking" },
    { freq: 16000, label: "16K", type: "highshelf" },
  ];
  const PRESETS = {
    Flat: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "Bass Boost": [10, 8, 4, 1, 0, 0, 0, 0, 0, 0],
    "Treble+": [0, 0, 0, 0, 0, 2, 4, 6, 8, 10],
    "V-Shape": [8, 5, 1, -2, -4, -4, -2, 1, 5, 8],
    Vocal: [-2, -2, 0, 4, 6, 6, 4, 0, -2, -2],
    Rock: [5, 3, 0, -2, 0, 3, 5, 6, 5, 4],
    Classical: [5, 4, 3, 2, 0, 0, 0, -2, -3, -4],
  };

  /* ── STATE ───────────────────────────────────────────── */
  const gains = BANDS.map(() => 0);
  let masterGain = 1;
  let eqEnabled = true;

  /* ── AUDIO INTERCEPTION ──────────────────────────────── */
  const OrigAudioContext = window.AudioContext || window.webkitAudioContext;
  if (!OrigAudioContext) return;

  const allCtx = [];
  const filterMap = new WeakMap();

  function buildChain(ctx) {
    if (filterMap.has(ctx)) return;
    try {
      const filters = BANDS.map((b, i) => {
        const f = ctx.createBiquadFilter();
        f.type = b.type;
        f.frequency.value = b.freq;
        f.Q.value = 1.4;
        f.gain.value = eqEnabled ? gains[i] : 0;
        return f;
      });
      const gainNode = ctx.createGain();
      gainNode.gain.value = eqEnabled ? masterGain : 1;
      for (let i = 0; i < filters.length - 1; i++)
        filters[i].connect(filters[i + 1]);
      filters[filters.length - 1].connect(gainNode);
      gainNode.connect(ctx.destination);
      filterMap.set(ctx, { filters, gainNode });
      allCtx.push(ctx);
    } catch (e) {}
  }

  function chainEntry(ctx) {
    const data = filterMap.get(ctx);
    return data ? data.filters[0] : ctx.destination;
  }

  function makeDummy() {
    return { connect() {}, disconnect() {} };
  }

  // Patch the constructor
  const PatchedCtx = function (...args) {
    const ctx = new OrigAudioContext(...args);
    buildChain(ctx);
    const _mes = ctx.createMediaElementSource.bind(ctx);
    ctx.createMediaElementSource = function (el) {
      const src = _mes(el);
      try {
        src.connect(chainEntry(ctx));
      } catch (e) {}
      return makeDummy();
    };
    const _mss = ctx.createMediaStreamSource.bind(ctx);
    ctx.createMediaStreamSource = function (stream) {
      const src = _mss(stream);
      try {
        src.connect(chainEntry(ctx));
      } catch (e) {}
      return makeDummy();
    };
    return ctx;
  };
  PatchedCtx.prototype = OrigAudioContext.prototype;

  try {
    Object.defineProperty(window, "AudioContext", {
      value: PatchedCtx,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(window, "webkitAudioContext", {
      value: PatchedCtx,
      writable: true,
      configurable: true,
    });
  } catch (e) {}

  // Hook HTMLMediaElement.play (catches YouTube's native <video>)
  const _play = HTMLMediaElement.prototype.play;
  HTMLMediaElement.prototype.play = function (...a) {
    attachToMediaEl(this);
    return _play.apply(this, a);
  };

  function attachToMediaEl(el) {
    if (el.__eqAttached) return;
    el.__eqAttached = true;
    try {
      const ctx = new OrigAudioContext();
      buildChain(ctx);
      const src = ctx.createMediaElementSource(el);
      src.connect(chainEntry(ctx));
    } catch (e) {
      el.__eqAttached = false;
    }
  }

  // MutationObserver to catch <video>/<audio> added dynamically (YouTube SPA)
  function watchDOM() {
    new MutationObserver((muts) => {
      for (const m of muts) {
        for (const n of m.addedNodes) {
          if (n.nodeType !== 1) continue;
          if (n.matches("video,audio")) attachToMediaEl(n);

          if (n.querySelectorAll) {
            n.querySelectorAll("video,audio").forEach(attachToMediaEl);
          }
        }
      }
    }).observe(document.documentElement, { childList: true, subtree: true });

    document.querySelectorAll("video,audio").forEach(attachToMediaEl);
  }

  if (document.documentElement) watchDOM();
  else document.addEventListener("DOMContentLoaded", watchDOM);

  /* ── SYNC ────────────────────────────────────────────── */
  function syncAll() {
    for (const ctx of allCtx) {
      const d = filterMap.get(ctx);
      if (!d) continue;
      for (let i = 0; i < d.filters.length; i++) {
        d.filters[i].gain.value = eqEnabled ? gains[i] : 0;
      }
      d.gainNode.gain.value = eqEnabled ? masterGain : 1;
    }
  }

  /* ── UI (Shadow DOM — invisible to the page) ─────────── */
  let panelOpen = false;
  let panelLocked = true;
  let shadowHost = null;
  let panelEl = null;
  let sliders = [];
  let valLabels = [];

  function h(tag, props = {}) {
    const el = document.createElement(tag);
    Object.assign(el, props);
    return el;
  }

  function updateValLabel(i, v) {
    const lbl = valLabels[i];
    if (!lbl) return;
    lbl.value = (v > 0 ? "+" : "") + parseFloat(v).toFixed(1);
    // Ayu Mirage colors: Green for positive, Red for negative, Cyan for 0
    lbl.style.color = v > 0 ? "#bae67e" : v < 0 ? "#f28779" : "#5ccfe6";
  }

  function buildUI() {
    if (shadowHost) return;

    shadowHost = document.createElement("div");
    Object.assign(shadowHost.style, {
      position: "fixed",
      top: "0",
      left: "0",
      width: "0",
      height: "0",
      zIndex: "2147483647",
      pointerEvents: "none",
    });
    document.documentElement.appendChild(shadowHost);

    const shadow = shadowHost.attachShadow({ mode: "open" });

    const style = document.createElement("style");
    style.textContent = `
        #wrap {
            position: fixed;
            bottom: 24px; right: 24px;
            width: 510px;
            background: #1f2430; /* Ayu Mirage Bg */
            border: 1px solid #242b38; /* Ayu Border */
            border-radius: 18px;
            padding: 20px 22px 16px;
            color: #cbccc6; /* Ayu Fg */
            font-family: 'Segoe UI', system-ui, sans-serif;
            font-size: 13px;
            box-shadow: 0 16px 60px rgba(0,0,0,0.5);
            display: none;
            pointer-events: all;
            user-select: none;
        }
        #wrap.open { display: block; animation: popIn .18s ease; }
        @keyframes popIn { from { opacity:0; transform: translateY(10px) scale(.97); } }

        #hdr { display:flex; align-items:center; gap:10px; margin-bottom:14px; cursor:default; }
        #hdr:active { cursor: default; }
        #hdr.draggable { cursor:grab; }
        #hdr.draggable:active { cursor:grabbing; }
        #title { font-weight:700; font-size:14px; flex:1; letter-spacing:.3px; }
        #lock-btn {
            font-size: 14px; cursor: pointer; opacity: 0.5;
            transition: opacity 0.15s; padding: 2px 4px;
        }
        #lock-btn:hover { opacity: 1; }
        .kbd {
            font-size:11px; color:#707a8c;
            background:#191e2a;
            border:1px solid #242b38;
            border-radius:6px; padding:2px 8px;
        }
        .row { display:flex; align-items:center; gap:8px; }
        .muted { font-size:12px; color:#707a8c; white-space:nowrap; }
        input[type=checkbox] { accent-color:#5ccfe6; width:15px; height:15px; cursor:pointer; }

        #presets { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:16px; }
        .pb {
            background:#191e2a;
            border:1px solid #242b38;
            border-radius:20px; color:#cbccc6;
            padding:3px 11px; font-size:11px; cursor:pointer; transition:background .1s;
        }
        .pb:hover { background:#242b38; }
        .pb.on { background:rgba(92, 207, 230, 0.15); border-color:#5ccfe6; color:#5ccfe6; }

        #bands { display:flex; gap:3px; align-items:flex-end; margin-bottom:2px; }
        .band { display:flex; flex-direction:column; align-items:center; gap:5px; flex:1; }
        
        .val { 
            font-size:11px; color:#5ccfe6; font-weight:600; text-align:center; 
            background: transparent; border: 1px solid transparent; width: 36px;
            outline: none; padding: 2px 0; font-family: inherit; transition: background 0.1s;
        }
        .val:focus { background: #242b38; border-radius: 4px; }
        .val::selection { background: rgba(92, 207, 230, 0.4); }

        .sl-wrap { height:130px; display:flex; align-items:center; justify-content:center; }
        
        /* Specific selector to ONLY hit the EQ band sliders, not master */
        .sl-wrap input[type=range] {
            writing-mode: vertical-lr; direction: rtl;
            -webkit-appearance: slider-vertical; appearance: slider-vertical;
            width:26px; height:118px; cursor:pointer; accent-color:#5ccfe6;
        }
        .blbl { font-size:11px; color:#707a8c; }

        #bot {
            display:flex; align-items:center; gap:10px;
            margin-top:14px; border-top:1px solid #242b38; padding-top:13px;
        }
        
        /* Master Volume explicitly overrides vertical styles */
        #mvol { 
            flex:1; accent-color:#d4bfff; /* Ayu Purple */ cursor:pointer;
            -webkit-appearance: auto; appearance: auto;
            writing-mode: horizontal-tb; direction: ltr;
            height: auto; width: auto; 
        }
        
        #mval { 
            font-size:12px; color:#d4bfff; font-weight:600; text-align:right;
            background: transparent; border: 1px solid transparent; width: 44px;
            outline: none; padding: 2px 0; font-family: inherit; transition: background 0.1s;
        }
        #mval:focus { background: #242b38; border-radius: 4px; }
        #mval::selection { background: rgba(212, 191, 255, 0.4); }

        #rst {
            background:rgba(242, 135, 121, 0.1); border:1px solid rgba(242, 135, 121, 0.25);
            border-radius:8px; color:#f28779; padding:4px 12px; font-size:11px; cursor:pointer;
        }
        #rst:hover { background:rgba(242, 135, 121, 0.2); }
        `;
    shadow.appendChild(style);

    panelEl = h("div", { id: "wrap" });

    // Header
    const hdr = h("div", { id: "hdr" });
    const lockBtn = h("span", { id: "lock-btn", textContent: "🔒" });
    const title = h("span", { id: "title", textContent: "Equalizer" });
    const kbd = h("span", { className: "kbd", textContent: "Alt + X" });
    const eqRow = h("div", { className: "row" });
    const eqLbl = h("span", { className: "muted", textContent: "EQ" });
    const eqChk = h("input", { type: "checkbox" });

    lockBtn.onclick = (e) => {
      e.stopPropagation();
      panelLocked = !panelLocked;
      lockBtn.textContent = panelLocked ? "🔒" : "🔓";
      hdr.classList.toggle("draggable", !panelLocked);
    };

    if (!panelLocked) hdr.classList.add("draggable");

    eqChk.checked = true;
    eqChk.onchange = () => {
      eqEnabled = eqChk.checked;
      syncAll();
    };
    eqRow.append(eqLbl, eqChk);
    hdr.append(lockBtn, title, kbd, eqRow);
    panelEl.append(hdr);

    // Presets
    const presetsDiv = h("div", { id: "presets" });
    const presetBtns = {};
    function clearPresets() {
      Object.values(presetBtns).forEach((b) => b.classList.remove("on"));
    }
    Object.entries(PRESETS).forEach(([name, vals]) => {
      const btn = h("button", { className: "pb", textContent: name });
      presetBtns[name] = btn;
      btn.onclick = () => {
        vals.forEach((v, i) => {
          gains[i] = v;
          sliders[i].value = v;
          updateValLabel(i, v);
        });
        syncAll();
        clearPresets();
        btn.classList.add("on");
      };
      presetsDiv.append(btn);
    });
    panelEl.append(presetsDiv);

    // Band sliders and inputs
    const bandsDiv = h("div", { id: "bands" });
    const stopProp = (e) => e.stopPropagation(); // Helper to stop keys

    BANDS.forEach((band, i) => {
      const col = h("div", { className: "band" });

      const val = h("input", { className: "val", value: "0.0", type: "text" });
      val.addEventListener("keydown", stopProp);
      val.addEventListener("keyup", stopProp);
      val.addEventListener("keypress", stopProp);

      val.addEventListener("change", () => {
        let parsed = parseFloat(val.value);
        if (isNaN(parsed)) parsed = 0;
        parsed = Math.max(-15, Math.min(15, parsed)); // Clamp

        gains[i] = parsed;
        sliders[i].value = parsed;
        updateValLabel(i, parsed);
        syncAll();
        clearPresets();
      });

      const wrap = h("div", { className: "sl-wrap" });
      const sl = h("input", {
        type: "range",
        min: -15,
        max: 15,
        step: 0.5,
        value: 0,
      });
      sl.setAttribute("orient", "vertical");
      sl.oninput = () => {
        gains[i] = parseFloat(sl.value);
        updateValLabel(i, gains[i]);
        syncAll();
        clearPresets();
      };
      const lbl = h("span", { className: "blbl", textContent: band.label });
      wrap.append(sl);
      col.append(val, wrap, lbl);
      bandsDiv.append(col);
      sliders.push(sl);
      valLabels.push(val);
    });
    panelEl.append(bandsDiv);

    // Bottom (Master Volume)
    const bot = h("div", { id: "bot" });
    const mLbl = h("span", { className: "muted", textContent: "Master" });
    const mSl = h("input", {
      type: "range",
      id: "mvol",
      min: 0,
      max: 2,
      step: 0.01,
      value: 1,
    });

    // Converted master volume text to input field
    const mVal = h("input", { id: "mval", value: "100%", type: "text" });

    mVal.addEventListener("keydown", stopProp);
    mVal.addEventListener("keyup", stopProp);
    mVal.addEventListener("keypress", stopProp);

    mVal.addEventListener("change", () => {
      let parsed = parseFloat(mVal.value.replace("%", ""));
      if (isNaN(parsed)) parsed = masterGain * 100; // fallback

      parsed = Math.max(0, Math.min(200, parsed));

      masterGain = parsed / 100;
      mSl.value = masterGain;
      mVal.value = Math.round(parsed) + "%";
      syncAll();
    });

    const rst = h("button", { id: "rst", textContent: "Reset all" });
    mSl.oninput = () => {
      masterGain = parseFloat(mSl.value);
      mVal.value = Math.round(masterGain * 100) + "%";
      syncAll();
    };
    rst.onclick = () => {
      gains.fill(0);
      sliders.forEach((s, i) => {
        s.value = 0;
        updateValLabel(i, 0);
      });
      masterGain = 1;
      mSl.value = 1;
      mVal.value = "100%";
      syncAll();
      clearPresets();
    };
    bot.append(mLbl, mSl, mVal, rst);
    panelEl.append(bot);
    shadow.appendChild(panelEl);

    // Drag
    let dx = 0,
      dy = 0,
      drag = false;
    hdr.addEventListener("mousedown", (e) => {
      if (panelLocked) return;
      drag = true;
      const r = panelEl.getBoundingClientRect();
      dx = e.clientX - r.left;
      dy = e.clientY - r.top;
    });
    document.addEventListener("mousemove", (e) => {
      if (!drag) return;
      panelEl.style.left = e.clientX - dx + "px";
      panelEl.style.top = e.clientY - dy + "px";
      panelEl.style.right = "auto";
      panelEl.style.bottom = "auto";
    });
    document.addEventListener("mouseup", () => {
      drag = false;
    });
  }

  function togglePanel() {
    if (!shadowHost) buildUI();
    panelOpen = !panelOpen;
    panelEl.classList.toggle("open", panelOpen);
  }

  /* Alt+X shortcut */
  document.addEventListener(
    "keydown",
    (e) => {
      if (e.altKey && e.key.toLowerCase() === "x") {
        e.preventDefault();
        e.stopPropagation();
        togglePanel();
      }
    },
    false,
  );

  /* Build UI as soon as <body> exists */
  function tryBuild() {
    if (document.body) buildUI();
  }
  if (document.body) buildUI();
  else {
    new MutationObserver((_, obs) => {
      if (document.body) {
        obs.disconnect();
        buildUI();
      }
    }).observe(document.documentElement, { childList: true });
  }
})();