Press Alt+X to open/close. (Note: Does not work on DRM-protected sites like Netflix/Spotify).
// ==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 });
}
})();