Files
ytplayer/frontend/vocal-reducer.js
2026-10-03 14:59:44 +08:00

22 lines
3.1 KiB
JavaScript

/* Mid/side centre reduction shared by live EQ and offline iPhone renders. */
(function(root) {
'use strict';
const amount = value => Math.max(0, Math.min(100, Number.isFinite(Number(value)) ? Number(value) : 100));
function coefficients(value) { const v = amount(value) / 100; return { same: (1 + v) / 2, cross: (v - 1) / 2 }; }
function process(left, right, value) { const c = coefficients(value); return [left * c.same + right * c.cross, right * c.same + left * c.cross]; }
function create(context, source, value = 100) {
const input = context.createGain(); input.channelCount = 2; input.channelCountMode = 'explicit'; input.channelInterpretation = 'speakers'; source.connect(input);
const splitter = context.createChannelSplitter(2), output = context.createChannelMerger(2); input.connect(splitter);
const matrix = [];
for (let from = 0; from < 2; from++) for (let to = 0; to < 2; to++) { const gain = context.createGain(); splitter.connect(gain, from); gain.connect(output, 0, to); matrix.push({ gain, same: from === to }); }
function set(next) { const c = coefficients(next); for (const entry of matrix) entry.gain.gain.setTargetAtTime(entry.same ? c.same : c.cross, context.currentTime, .03); }
// Initial values must be exact for OfflineAudioContext, without a ramp.
const c = coefficients(value); for (const entry of matrix) entry.gain.gain.value = entry.same ? c.same : c.cross;
return { output, set };
}
function register(adapter) { root.SettingsSections.register({ id: 'vocal-reducer', title: 'Vocal reducer', cluster: 'Listening', summary: () => `${amount(adapter.settings().vocalLevel ?? 100)}% · centre-panned vocals`, render(container) { const doc = container.ownerDocument, label = doc.createElement('label'); label.className = 'set-row'; const text = doc.createElement('span'); text.textContent = 'Vocal — reduces centre-panned vocals'; const input = doc.createElement('input'); input.type = 'range'; input.min = '0'; input.max = '100'; input.step = '1'; input.value = String(amount(adapter.settings().vocalLevel ?? 100)); input.setAttribute('aria-label', 'Vocal level: reduces centre-panned vocals'); const output = doc.createElement('output'); output.textContent = `${input.value}%`; input.oninput = () => { adapter.settings().vocalLevel = Number(input.value); output.textContent = `${input.value}%`; adapter.persist(); adapter.apply(); }; label.append(text, input, output); const note = doc.createElement('p'); note.textContent = '100% keeps the original mix; 0% removes its stereo centre. This can also reduce bass, drums and other centred instruments. Mono recordings may become silent. On iPhone, saved songs use processed copies; streamed songs require Live sound processing.'; container.append(label, note); } }); }
// TODO: model-based Demucs separation needs an optional server worker. See docs/vocal-stems.md.
function stems() { return { available: false, reason: 'Model-based stem separation is not implemented.' }; }
const api = { amount, coefficients, process, create, register, stems }; if (typeof module !== 'undefined') module.exports = api; else root.VocalReducer = api;
})(typeof window !== 'undefined' ? window : globalThis);