feat: mono audio

This commit is contained in:
Eduard Prigoana 2026-02-09 12:06:44 +00:00
parent 19baee21aa
commit 3974ec7551
4 changed files with 98 additions and 7 deletions

View file

@ -2896,6 +2896,16 @@
style="width: 80px" style="width: 80px"
/> />
</div> </div>
<div class="setting-item">
<div class="info">
<span class="label">Mono Audio</span>
<span class="description">Combine left and right channels into mono</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="mono-audio-toggle" />
<span class="slider"></span>
</label>
</div>
<!-- 16-Band Equalizer --> <!-- 16-Band Equalizer -->
<div class="setting-item"> <div class="setting-item">

View file

@ -1,7 +1,7 @@
// js/audio-context.js // js/audio-context.js
// Shared Audio Context Manager - handles EQ and provides context for visualizer // Shared Audio Context Manager - handles EQ and provides context for visualizer
import { equalizerSettings } from './storage.js'; import { equalizerSettings, monoAudioSettings } from './storage.js';
// Standard 16-band ISO center frequencies (Hz) // Standard 16-band ISO center frequencies (Hz)
const EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000]; const EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
@ -83,6 +83,8 @@ class AudioContextManager {
this.outputNode = null; this.outputNode = null;
this.isInitialized = false; this.isInitialized = false;
this.isEQEnabled = false; this.isEQEnabled = false;
this.isMonoAudioEnabled = false;
this.monoMergerNode = null;
this.currentGains = new Array(16).fill(0); this.currentGains = new Array(16).fill(0);
this.audio = null; this.audio = null;
@ -168,13 +170,16 @@ class AudioContextManager {
this.outputNode = this.audioContext.createGain(); this.outputNode = this.audioContext.createGain();
this.outputNode.gain.value = 1; this.outputNode.gain.value = 1;
// Create mono audio merger node
this.monoMergerNode = this.audioContext.createChannelMerger(2);
// Connect filter chain: filter[0] -> filter[1] -> ... -> filter[15] -> outputNode // Connect filter chain: filter[0] -> filter[1] -> ... -> filter[15] -> outputNode
for (let i = 0; i < this.filters.length - 1; i++) { for (let i = 0; i < this.filters.length - 1; i++) {
this.filters[i].connect(this.filters[i + 1]); this.filters[i].connect(this.filters[i + 1]);
} }
this.filters[this.filters.length - 1].connect(this.outputNode); this.filters[this.filters.length - 1].connect(this.outputNode);
// Connect the audio graph based on EQ state // Connect the audio graph based on EQ and mono state
this._connectGraph(); this._connectGraph();
this.isInitialized = true; this.isInitialized = true;
@ -185,7 +190,7 @@ class AudioContextManager {
} }
/** /**
* Connect the audio graph based on EQ enabled state * Connect the audio graph based on EQ and mono audio state
*/ */
_connectGraph() { _connectGraph() {
if (!this.source || !this.audioContext) return; if (!this.source || !this.audioContext) return;
@ -194,6 +199,13 @@ class AudioContextManager {
// Disconnect everything first // Disconnect everything first
this.source.disconnect(); this.source.disconnect();
this.outputNode.disconnect(); this.outputNode.disconnect();
if (this.monoMergerNode) {
try {
this.monoMergerNode.disconnect();
} catch (e) {
// Ignore if not connected
}
}
// Only disconnect destination from analyser to preserve other taps (like Butterchurn) // Only disconnect destination from analyser to preserve other taps (like Butterchurn)
try { try {
@ -202,15 +214,34 @@ class AudioContextManager {
// Ignore if not connected // Ignore if not connected
} }
let lastNode = this.source;
// Apply mono audio if enabled
if (this.isMonoAudioEnabled && this.monoMergerNode) {
// Create a gain node to mix channels before the merger
const monoGain = this.audioContext.createGain();
monoGain.gain.value = 0.5; // Reduce volume to prevent clipping when mixing
// Connect source to mono gain
this.source.connect(monoGain);
// Connect mono gain to both inputs of the merger
monoGain.connect(this.monoMergerNode, 0, 0);
monoGain.connect(this.monoMergerNode, 0, 1);
lastNode = this.monoMergerNode;
console.log('[AudioContext] Mono audio enabled');
}
if (this.isEQEnabled && this.filters.length > 0) { if (this.isEQEnabled && this.filters.length > 0) {
// EQ enabled: source -> EQ filters -> output -> analyser -> destination // EQ enabled: lastNode -> EQ filters -> output -> analyser -> destination
this.source.connect(this.filters[0]); lastNode.connect(this.filters[0]);
this.outputNode.connect(this.analyser); this.outputNode.connect(this.analyser);
this.analyser.connect(this.audioContext.destination); this.analyser.connect(this.audioContext.destination);
console.log('[AudioContext] EQ connected'); console.log('[AudioContext] EQ connected');
} else { } else {
// EQ disabled: source -> analyser -> destination // EQ disabled: lastNode -> analyser -> destination
this.source.connect(this.analyser); lastNode.connect(this.analyser);
this.analyser.connect(this.audioContext.destination); this.analyser.connect(this.audioContext.destination);
console.log('[AudioContext] EQ bypassed'); console.log('[AudioContext] EQ bypassed');
} }
@ -303,6 +334,27 @@ class AudioContextManager {
return this.isInitialized && this.isEQEnabled; return this.isInitialized && this.isEQEnabled;
} }
/**
* Toggle mono audio on/off
*/
toggleMonoAudio(enabled) {
this.isMonoAudioEnabled = enabled;
monoAudioSettings.setEnabled(enabled);
if (this.isInitialized) {
this._connectGraph();
}
return this.isMonoAudioEnabled;
}
/**
* Check if mono audio is active
*/
isMonoAudioActive() {
return this.isInitialized && this.isMonoAudioEnabled;
}
/** /**
* Set gain for a specific band * Set gain for a specific band
*/ */
@ -372,6 +424,7 @@ class AudioContextManager {
_loadSettings() { _loadSettings() {
this.isEQEnabled = equalizerSettings.isEnabled(); this.isEQEnabled = equalizerSettings.isEnabled();
this.currentGains = equalizerSettings.getGains(); this.currentGains = equalizerSettings.getGains();
this.isMonoAudioEnabled = monoAudioSettings.isEnabled();
} }
} }

View file

@ -24,6 +24,7 @@ import {
homePageSettings, homePageSettings,
sidebarSectionSettings, sidebarSectionSettings,
fontSettings, fontSettings,
monoAudioSettings,
} from './storage.js'; } from './storage.js';
import { audioContextManager, EQ_PRESETS } from './audio-context.js'; import { audioContextManager, EQ_PRESETS } from './audio-context.js';
import { getButterchurnPresets } from './visualizers/butterchurn.js'; import { getButterchurnPresets } from './visualizers/butterchurn.js';
@ -690,6 +691,17 @@ export function initializeSettings(scrobbler, player, api, ui) {
}); });
} }
// Mono Audio Toggle
const monoAudioToggle = document.getElementById('mono-audio-toggle');
if (monoAudioToggle) {
monoAudioToggle.checked = monoAudioSettings.isEnabled();
monoAudioToggle.addEventListener('change', (e) => {
const enabled = e.target.checked;
monoAudioSettings.setEnabled(enabled);
audioContextManager.toggleMonoAudio(enabled);
});
}
// ======================================== // ========================================
// 16-Band Equalizer Settings // 16-Band Equalizer Settings
// ======================================== // ========================================

View file

@ -844,6 +844,22 @@ export const equalizerSettings = {
}, },
}; };
export const monoAudioSettings = {
STORAGE_KEY: 'mono-audio-enabled',
isEnabled() {
try {
return localStorage.getItem(this.STORAGE_KEY) === 'true';
} catch {
return false;
}
},
setEnabled(enabled) {
localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
},
};
export const sidebarSettings = { export const sidebarSettings = {
STORAGE_KEY: 'monochrome-sidebar-collapsed', STORAGE_KEY: 'monochrome-sidebar-collapsed',