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"
/>
</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 -->
<div class="setting-item">

View file

@ -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();
}
}

View file

@ -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
// ========================================

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 = {
STORAGE_KEY: 'monochrome-sidebar-collapsed',