日誌
YT Music sidechain
Youtube 看教學但又不想音樂直接蓋過人聲,所以用 Gemini 寫了個音樂 sidechain 的 script。
現在學 C++ 可以有背景音樂,爽。
Script -> 點我(失效)
更:
// ==UserScript==
// @name Universal Audio Sidechain with UI (v6.3 Multi-Tab Stabilized)
// @namespace http://tampermonkey.net/
// @version 6.3
// @description Hijacks native audio pipelines with sidechain ducking. Multi-tab registry prevents volume pumping.
// @author You
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// Helper to safely parse localStorage values
function getStoredFloat(key, defaultValue) {
const val = localStorage.getItem(key);
if (val === null) return defaultValue;
const parsed = parseFloat(val);
return isNaN(parsed) ? defaultValue : parsed;
}
// Generate unique ID for this browser tab instance
const TAB_ID = 'tab_' + Math.random().toString(36).substring(2, 9);
// =========================================================================
// USER CONFIGURATION (DEFAULT SETTINGS)
// =========================================================================
const CONFIG = {
ENABLED: true,
DUCK_VOLUME: getStoredFloat('sc_duck_volume', 0.10),
ATTACK_TIME: getStoredFloat('sc_attack_time', 0.05),
RELEASE_TIME: getStoredFloat('sc_release_time', 0.60),
CARRIER_DOMAINS: [
'www.youtube.com',
'twitch.tv',
'bilibili.com'
],
MODULATED_DOMAINS: [
'music.youtube.com',
'app.chillhop.com',
'open.spotify.com',
'soundcloud.com',
'music.apple.com'
]
};
// =========================================================================
const currentHost = window.location.hostname;
const isCarrier = CONFIG.CARRIER_DOMAINS.some(domain => currentHost.includes(domain));
const isModulated = CONFIG.MODULATED_DOMAINS.some(domain => currentHost.includes(domain));
if (!isCarrier && !isModulated) return;
// ==========================================
// 1. CARRIER BEHAVIOR (Sender - Tab Registry System)
// ==========================================
if (isCarrier) {
window.addEventListener('DOMContentLoaded', () => {
let lastTriggerState = false;
let lastWriteTime = 0;
let triggerHoldTimer = 0;
// Clean up tab registration on exit
window.addEventListener('beforeunload', () => {
const carriers = GM_getValue('sc_active_carriers') || {};
delete carriers[TAB_ID];
GM_setValue('sc_active_carriers', carriers);
});
setInterval(() => {
const mediaElems = document.querySelectorAll('video, audio');
let isPlaying = false;
for (const media of mediaElems) {
// Filter out tiny thumbnail preview elements (e.g. YouTube hover videos)
const isMiniPreview = media.offsetWidth > 0 && media.offsetWidth < 180 && media.offsetHeight < 180;
if (!isMiniPreview && !media.paused && !media.ended && media.readyState > 2) {
isPlaying = true;
break;
}
}
const now = Date.now();
if (isPlaying) {
triggerHoldTimer = now;
}
// 800ms hold time prevents volume pumping during video buffering or seek transitions
const isTriggered = isPlaying || (now - triggerHoldTimer < 800);
if (isTriggered !== lastTriggerState || now - lastWriteTime > 400) {
const carriers = GM_getValue('sc_active_carriers') || {};
if (isTriggered) {
carriers[TAB_ID] = now;
} else {
delete carriers[TAB_ID];
}
// Prune stale tabs (>3000ms inactive)
for (const id in carriers) {
if (now - carriers[id] > 3000) {
delete carriers[id];
}
}
GM_setValue('sc_active_carriers', carriers);
lastTriggerState = isTriggered;
lastWriteTime = now;
}
}, 50);
});
}
// ==========================================
// 2. MODULATED BEHAVIOR (Target Receiver + UI)
// ==========================================
if (isModulated) {
const injectCode = `
(function(DUCK_VOL, ATTACK, RELEASE, INITIAL_ENABLE) {
let duckTarget = 1.0;
let duckCurrent = 1.0;
let configDuckVol = DUCK_VOL;
let configAttack = ATTACK;
let configRelease = RELEASE;
let isEnabled = INITIAL_ENABLE;
const htmlMedia = new Set();
const webAudioGains = new Set();
let activeAnalyser = null;
const historyScope = new Array(200).fill(1.0);
// [Engine 1]: HTMLMediaElement Hijack
const origVolDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume');
if (origVolDesc) {
Object.defineProperty(HTMLMediaElement.prototype, 'volume', {
get() {
return this._userBaseVol !== undefined ? this._userBaseVol : origVolDesc.get.call(this);
},
set(v) {
this._userBaseVol = v;
origVolDesc.set.call(this, v * (isEnabled ? duckCurrent : 1.0));
}
});
const origPlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
htmlMedia.add(this);
if (this._userBaseVol === undefined) this._userBaseVol = origVolDesc.get.call(this);
origVolDesc.set.call(this, this._userBaseVol * (isEnabled ? duckCurrent : 1.0));
return origPlay.apply(this, arguments);
};
}
// [Engine 2]: Web Audio API Hijack
const OriginalAudioContext = window.AudioContext || window.webkitAudioContext;
if (OriginalAudioContext) {
window.AudioContext = new Proxy(OriginalAudioContext, {
construct(target, args) {
const ctx = new target(...args);
const masterGain = ctx.createGain();
const analyser = ctx.createAnalyser();
analyser.fftSize = 512;
activeAnalyser = analyser;
masterGain.gain.value = isEnabled ? duckCurrent : 1.0;
masterGain.connect(analyser);
analyser.connect(ctx.destination);
webAudioGains.add(masterGain);
Object.defineProperty(ctx, 'destination', { get: () => masterGain });
return ctx;
}
});
window.webkitAudioContext = window.AudioContext;
}
// [DSP & Animation Loop]
let lastTime = performance.now();
function loop() {
const time = performance.now();
const dt = Math.min((time - lastTime) / 1000, 0.1);
lastTime = time;
const activeTarget = isEnabled ? duckTarget : 1.0;
// Math precision snapping prevents floating-point volume jitter
if (Math.abs(duckCurrent - activeTarget) > 0.001) {
const step = activeTarget < duckCurrent
? (1 / Math.max(0.01, configAttack)) * dt
: (1 / Math.max(0.01, configRelease)) * dt;
if (activeTarget < duckCurrent) {
duckCurrent = Math.max(activeTarget, duckCurrent - step);
} else {
duckCurrent = Math.min(activeTarget, duckCurrent + step);
}
applyGainToAll(duckCurrent);
} else if (duckCurrent !== activeTarget) {
duckCurrent = activeTarget;
applyGainToAll(duckCurrent);
}
historyScope.push(duckCurrent);
if (historyScope.length > 200) historyScope.shift();
renderVisualizers();
}
function applyGainToAll(gainVal) {
htmlMedia.forEach(m => {
if(m._userBaseVol !== undefined) {
origVolDesc.set.call(m, m._userBaseVol * gainVal);
}
});
webAudioGains.forEach(g => {
if(g.context.state !== 'closed') g.gain.value = gainVal;
});
}
function renderVisualizers() {
const cAdsr = document.getElementById('sc-canvas-adsr');
if (cAdsr) {
const ctx = cAdsr.getContext('2d');
const w = cAdsr.width;
const h = cAdsr.height;
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, h * 0.5); ctx.lineTo(w, h * 0.5);
ctx.stroke();
const pad = 8;
const totalDur = configAttack + 0.3 + configRelease;
const xAttack = pad + (configAttack / totalDur) * (w - pad * 2);
const xSustain = xAttack + (0.3 / totalDur) * (w - pad * 2);
const xRelease = w - pad;
const yMax = pad;
const yDuck = pad + (1 - configDuckVol) * (h - pad * 2);
ctx.strokeStyle = '#00d2ff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pad, yMax);
ctx.lineTo(xAttack, yDuck);
ctx.lineTo(xSustain, yDuck);
ctx.lineTo(xRelease, yMax);
ctx.stroke();
let dotX = pad;
let dotY = pad + (1 - duckCurrent) * (h - pad * 2);
if (duckCurrent < 0.99) {
if (duckTarget < 1.0) {
dotX = pad + (1 - (duckCurrent - configDuckVol) / Math.max(0.001, 1 - configDuckVol)) * (xAttack - pad);
} else {
dotX = xSustain + ((duckCurrent - configDuckVol) / Math.max(0.001, 1 - configDuckVol)) * (xRelease - xSustain);
}
}
ctx.fillStyle = '#ff0055';
ctx.beginPath();
ctx.arc(Math.min(w - pad, Math.max(pad, dotX)), dotY, 4, 0, Math.PI * 2);
ctx.fill();
}
const cScope = document.getElementById('sc-canvas-scope');
if (cScope) {
const ctx = cScope.getContext('2d');
const w = cScope.width;
const h = cScope.height;
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, w, h);
ctx.lineWidth = 1.5;
ctx.strokeStyle = '#00ff66';
ctx.shadowColor = '#00ff66';
ctx.shadowBlur = 4;
ctx.beginPath();
if (activeAnalyser && activeAnalyser.context.state === 'running') {
const bufferLength = activeAnalyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
activeAnalyser.getByteTimeDomainData(dataArray);
const sliceWidth = w / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = (v * h) / 2;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
x += sliceWidth;
}
} else {
const sliceWidth = w / historyScope.length;
let x = 0;
for (let i = 0; i < historyScope.length; i++) {
const y = h - (historyScope[i] * (h - 10) + 5);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
x += sliceWidth;
}
}
ctx.stroke();
ctx.shadowBlur = 0;
}
}
setInterval(loop, 16);
window.addEventListener('message', e => {
if (e.source !== window || !e.data) return;
if (e.data.type === 'SC_DUCK') duckTarget = configDuckVol;
if (e.data.type === 'SC_RESTORE') duckTarget = 1.0;
if (e.data.type === 'SC_CONFIG_UPDATE') {
configDuckVol = e.data.duckVol;
configAttack = e.data.attack;
configRelease = e.data.release;
isEnabled = e.data.enabled;
if (duckTarget < 1.0) duckTarget = configDuckVol;
}
});
})(${CONFIG.DUCK_VOLUME}, ${CONFIG.ATTACK_TIME}, ${CONFIG.RELEASE_TIME}, ${CONFIG.ENABLED});
`;
const script = document.createElement('script');
script.textContent = injectCode;
document.documentElement.appendChild(script);
script.remove();
// UI Control Panel
function renderControlUI() {
if (document.getElementById('sc-ui-panel')) return;
const panel = document.createElement('div');
panel.id = 'sc-ui-panel';
panel.style.cssText = `
position: fixed; bottom: 20px; left: 20px; z-index: 999999;
background: rgba(18, 18, 18, 0.94); color: #fff;
padding: 12px 16px; border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.15);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 12px; width: 240px; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
backdrop-filter: blur(8px); pointer-events: auto; user-select: none;
`;
panel.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:6px;">
<span style="font-weight:700; color:#00d2ff; display:flex; align-items:center; gap:5px;">🎛️ Sidechain DSP</span>
<span id="sc-toggle-btn" style="cursor:pointer; color:#888; font-size:11px;">[Collapse]</span>
</div>
<div id="sc-body">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<label style="color:#ccc;">Enable Sidechain</label>
<input type="checkbox" id="sc-chk-enable" ${CONFIG.ENABLED ? 'checked' : ''} style="cursor:pointer;">
</div>
<div style="margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; color:#bbb; margin-bottom:3px;">
<span>Duck Volume:</span>
<span id="sc-val-duck">${Math.round(CONFIG.DUCK_VOLUME * 100)}%</span>
</div>
<input type="range" id="sc-rng-duck" min="0" max="1" step="0.01" value="${CONFIG.DUCK_VOLUME}" style="width:100%; cursor:pointer; accent-color:#00d2ff;">
</div>
<div style="margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; color:#bbb; margin-bottom:3px;">
<span>Attack:</span>
<span id="sc-val-attack">${Math.round(CONFIG.ATTACK_TIME * 1000)}ms</span>
</div>
<input type="range" id="sc-rng-attack" min="0.01" max="1" step="0.01" value="${CONFIG.ATTACK_TIME}" style="width:100%; cursor:pointer; accent-color:#00d2ff;">
</div>
<div style="margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; color:#bbb; margin-bottom:3px;">
<span>Release:</span>
<span id="sc-val-release">${Math.round(CONFIG.RELEASE_TIME * 1000)}ms</span>
</div>
<input type="range" id="sc-rng-release" min="0.05" max="2" step="0.05" value="${CONFIG.RELEASE_TIME}" style="width:100%; cursor:pointer; accent-color:#00d2ff;">
</div>
<div style="margin-top:10px; margin-bottom:6px;">
<div style="color:#aaa; font-size:10px; margin-bottom:2px;">Envelope ADSR Curve:</div>
<canvas id="sc-canvas-adsr" width="240" height="45" style="background:#111; border-radius:4px; border:1px solid #333; display:block;"></canvas>
</div>
<div style="margin-top:6px; margin-bottom:6px;">
<div style="color:#aaa; font-size:10px; margin-bottom:2px;">Oscilloscope:</div>
<canvas id="sc-canvas-scope" width="240" height="45" style="background:#0a0a0a; border-radius:4px; border:1px solid #333; display:block;"></canvas>
</div>
<div id="sc-status" style="font-size:10px; text-align:right; color:#777; margin-top:6px; font-weight:600;">
Status: Idle
</div>
</div>
`;
document.body.appendChild(panel);
const chkEnable = document.getElementById('sc-chk-enable');
const rngDuck = document.getElementById('sc-rng-duck');
const rngAttack = document.getElementById('sc-rng-attack');
const rngRelease = document.getElementById('sc-rng-release');
const toggleBtn = document.getElementById('sc-toggle-btn');
const scBody = document.getElementById('sc-body');
let isCollapsed = false;
toggleBtn.addEventListener('click', () => {
isCollapsed = !isCollapsed;
scBody.style.display = isCollapsed ? 'none' : 'block';
toggleBtn.innerText = isCollapsed ? '[Expand]' : '[Collapse]';
});
const updateConfig = () => {
CONFIG.ENABLED = chkEnable.checked;
CONFIG.DUCK_VOLUME = parseFloat(rngDuck.value);
CONFIG.ATTACK_TIME = parseFloat(rngAttack.value);
CONFIG.RELEASE_TIME = parseFloat(rngRelease.value);
localStorage.setItem('sc_duck_volume', CONFIG.DUCK_VOLUME);
localStorage.setItem('sc_attack_time', CONFIG.ATTACK_TIME);
localStorage.setItem('sc_release_time', CONFIG.RELEASE_TIME);
document.getElementById('sc-val-duck').innerText = Math.round(CONFIG.DUCK_VOLUME * 100) + '%';
document.getElementById('sc-val-attack').innerText = Math.round(CONFIG.ATTACK_TIME * 1000) + 'ms';
document.getElementById('sc-val-release').innerText = Math.round(CONFIG.RELEASE_TIME * 1000) + 'ms';
window.postMessage({
type: 'SC_CONFIG_UPDATE',
enabled: CONFIG.ENABLED,
duckVol: CONFIG.DUCK_VOLUME,
attack: CONFIG.ATTACK_TIME,
release: CONFIG.RELEASE_TIME
}, '*');
};
[chkEnable, rngDuck, rngAttack, rngRelease].forEach(el => el.addEventListener('input', updateConfig));
}
window.addEventListener('DOMContentLoaded', () => {
renderControlUI();
setInterval(() => {
if (!document.getElementById('sc-ui-panel')) renderControlUI();
}, 2000);
});
let localTriggerState = false;
function checkActiveCarriers(carriers) {
if (!carriers || typeof carriers !== 'object') return false;
const now = Date.now();
for (const id in carriers) {
if (now - carriers[id] < 3000) {
return true; // Active playing carrier tab exists
}
}
return false;
}
function updateStatusUI(isTriggered) {
const statusEl = document.getElementById('sc-status');
if (statusEl) {
if (!CONFIG.ENABLED) {
statusEl.innerText = 'Status: Disabled';
statusEl.style.color = '#777';
} else if (isTriggered) {
statusEl.innerText = 'Status: 🔊 Ducking Audio';
statusEl.style.color = '#00d2ff';
} else {
statusEl.innerText = 'Status: 🎵 Normal Output';
statusEl.style.color = '#888';
}
}
}
GM_addValueChangeListener('sc_active_carriers', (name, oldValue, newValue) => {
const isTriggered = checkActiveCarriers(newValue);
if (isTriggered !== localTriggerState) {
window.postMessage({ type: isTriggered ? 'SC_DUCK' : 'SC_RESTORE' }, '*');
localTriggerState = isTriggered;
updateStatusUI(isTriggered);
}
});
// Fallback registry heartbeat scan
setInterval(() => {
const carriers = GM_getValue('sc_active_carriers');
const isTriggered = checkActiveCarriers(carriers);
if (isTriggered !== localTriggerState) {
window.postMessage({ type: isTriggered ? 'SC_DUCK' : 'SC_RESTORE' }, '*');
localTriggerState = isTriggered;
updateStatusUI(isTriggered);
}
}, 500);
}
})();