diff --git a/index.html b/index.html index 9fe1391..b31800f 100644 --- a/index.html +++ b/index.html @@ -2896,6 +2896,16 @@ style="width: 80px" /> +
+
+ Mono Audio + Combine left and right channels into mono +
+ +
diff --git a/js/audio-context.js b/js/audio-context.js index dd23c6c..c30c067 100644 --- a/js/audio-context.js +++ b/js/audio-context.js @@ -1,7 +1,7 @@ // js/audio-context.js // 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) 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.isInitialized = false; this.isEQEnabled = false; + this.isMonoAudioEnabled = false; + this.monoMergerNode = null; this.currentGains = new Array(16).fill(0); this.audio = null; @@ -168,13 +170,16 @@ class AudioContextManager { this.outputNode = this.audioContext.createGain(); 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 for (let i = 0; i < this.filters.length - 1; i++) { this.filters[i].connect(this.filters[i + 1]); } 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.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() { if (!this.source || !this.audioContext) return; @@ -194,6 +199,13 @@ class AudioContextManager { // Disconnect everything first this.source.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) try { @@ -202,15 +214,34 @@ class AudioContextManager { // 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) { - // EQ enabled: source -> EQ filters -> output -> analyser -> destination - this.source.connect(this.filters[0]); + // EQ enabled: lastNode -> EQ filters -> output -> analyser -> destination + lastNode.connect(this.filters[0]); this.outputNode.connect(this.analyser); this.analyser.connect(this.audioContext.destination); console.log('[AudioContext] EQ connected'); } else { - // EQ disabled: source -> analyser -> destination - this.source.connect(this.analyser); + // EQ disabled: lastNode -> analyser -> destination + lastNode.connect(this.analyser); this.analyser.connect(this.audioContext.destination); console.log('[AudioContext] EQ bypassed'); } @@ -303,6 +334,27 @@ class AudioContextManager { 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 */ @@ -372,6 +424,7 @@ class AudioContextManager { _loadSettings() { this.isEQEnabled = equalizerSettings.isEnabled(); this.currentGains = equalizerSettings.getGains(); + this.isMonoAudioEnabled = monoAudioSettings.isEnabled(); } } diff --git a/js/settings.js b/js/settings.js index a4ae68c..02d246e 100644 --- a/js/settings.js +++ b/js/settings.js @@ -24,6 +24,7 @@ import { homePageSettings, sidebarSectionSettings, fontSettings, + monoAudioSettings, } from './storage.js'; import { audioContextManager, EQ_PRESETS } from './audio-context.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 // ======================================== diff --git a/js/storage.js b/js/storage.js index 03d84e0..08f651a 100644 --- a/js/storage.js +++ b/js/storage.js @@ -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 = { STORAGE_KEY: 'monochrome-sidebar-collapsed',