FIX: try to fix application volume control on linux

This commit is contained in:
Julien Maille 2026-02-13 21:01:57 +01:00
parent bb93edefde
commit 02cf092904
4 changed files with 46 additions and 19 deletions

View file

@ -81,12 +81,14 @@ class AudioContextManager {
this.analyser = null; this.analyser = null;
this.filters = []; this.filters = [];
this.outputNode = null; this.outputNode = null;
this.volumeNode = null;
this.isInitialized = false; this.isInitialized = false;
this.isEQEnabled = false; this.isEQEnabled = false;
this.isMonoAudioEnabled = false; this.isMonoAudioEnabled = false;
this.monoMergerNode = null; this.monoMergerNode = null;
this.currentGains = new Array(16).fill(0); this.currentGains = new Array(16).fill(0);
this.audio = null; this.audio = null;
this.currentVolume = 1.0;
// Callbacks for audio graph changes (for visualizers like Butterchurn) // Callbacks for audio graph changes (for visualizers like Butterchurn)
this._graphChangeCallbacks = []; this._graphChangeCallbacks = [];
@ -170,6 +172,10 @@ class AudioContextManager {
this.outputNode = this.audioContext.createGain(); this.outputNode = this.audioContext.createGain();
this.outputNode.gain.value = 1; this.outputNode.gain.value = 1;
// Create volume node
this.volumeNode = this.audioContext.createGain();
this.volumeNode.gain.value = this.currentVolume;
// Create mono audio merger node // Create mono audio merger node
this.monoMergerNode = this.audioContext.createChannelMerger(2); this.monoMergerNode = this.audioContext.createChannelMerger(2);
@ -199,6 +205,11 @@ class AudioContextManager {
// Disconnect everything first // Disconnect everything first
this.source.disconnect(); this.source.disconnect();
this.outputNode.disconnect(); this.outputNode.disconnect();
if (this.volumeNode) {
this.volumeNode.disconnect();
}
this.analyser.disconnect();
if (this.monoMergerNode) { if (this.monoMergerNode) {
try { try {
this.monoMergerNode.disconnect(); this.monoMergerNode.disconnect();
@ -207,13 +218,6 @@ class AudioContextManager {
} }
} }
// Only disconnect destination from analyser to preserve other taps (like Butterchurn)
try {
this.analyser.disconnect(this.audioContext.destination);
} catch {
// Ignore if not connected
}
let lastNode = this.source; let lastNode = this.source;
// Apply mono audio if enabled // Apply mono audio if enabled
@ -234,15 +238,17 @@ class AudioContextManager {
} }
if (this.isEQEnabled && this.filters.length > 0) { if (this.isEQEnabled && this.filters.length > 0) {
// EQ enabled: lastNode -> EQ filters -> output -> analyser -> destination // EQ enabled: lastNode -> EQ filters -> output -> analyser -> volume -> destination
lastNode.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.volumeNode);
this.volumeNode.connect(this.audioContext.destination);
console.log('[AudioContext] EQ connected'); console.log('[AudioContext] EQ connected');
} else { } else {
// EQ disabled: lastNode -> analyser -> destination // EQ disabled: lastNode -> analyser -> volume -> destination
lastNode.connect(this.analyser); lastNode.connect(this.analyser);
this.analyser.connect(this.audioContext.destination); this.analyser.connect(this.volumeNode);
this.volumeNode.connect(this.audioContext.destination);
console.log('[AudioContext] EQ bypassed'); console.log('[AudioContext] EQ bypassed');
} }
@ -313,6 +319,18 @@ class AudioContextManager {
return this.isInitialized; return this.isInitialized;
} }
/**
* Set the volume level (0.0 to 1.0)
* @param {number} value - Volume level
*/
setVolume(value) {
this.currentVolume = Math.max(0, Math.min(1, value));
if (this.volumeNode && this.audioContext) {
const now = this.audioContext.currentTime;
this.volumeNode.gain.setTargetAtTime(this.currentVolume, now, 0.01);
}
}
/** /**
* Toggle EQ on/off * Toggle EQ on/off
*/ */

View file

@ -66,11 +66,6 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
scrobbler.updateNowPlaying(player.currentTrack); scrobbler.updateNowPlaying(player.currentTrack);
} }
// Resume AudioContext for waveform on mobile (iOS)
if (waveformGenerator.audioContext.state === 'suspended') {
waveformGenerator.audioContext.resume();
}
updateWaveform(); updateWaveform();
} }

View file

@ -111,9 +111,18 @@ export class Player {
// Calculate effective volume // Calculate effective volume
const effectiveVolume = curvedVolume * scale; const effectiveVolume = curvedVolume * scale;
// Apply to audio element // Apply to audio element and/or Web Audio graph
if (audioContextManager.isReady()) {
// If Web Audio is active, we apply volume there for better compatibility
// Especially on Linux where audio.volume might not affect the Web Audio graph
// We set audio.volume to 1.0 to avoid double-reduction, or keep it synced?
// Some browsers require audio.volume to be set for system media controls to show volume
this.audio.volume = 1.0;
audioContextManager.setVolume(effectiveVolume);
} else {
this.audio.volume = Math.max(0, Math.min(1, effectiveVolume)); this.audio.volume = Math.max(0, Math.min(1, effectiveVolume));
} }
}
applyAudioEffects() { applyAudioEffects() {
const speed = audioEffectsSettings.getSpeed(); const speed = audioEffectsSettings.getSpeed();
@ -213,6 +222,7 @@ export class Player {
// Must happen before audio.play() or audio won't route through Web Audio // Must happen before audio.play() or audio won't route through Web Audio
if (!audioContextManager.isReady()) { if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio); audioContextManager.init(this.audio);
this.applyReplayGain();
} }
await audioContextManager.resume(); await audioContextManager.resume();
@ -233,6 +243,7 @@ export class Player {
// Ensure audio context is active for iOS lock screen controls // Ensure audio context is active for iOS lock screen controls
if (!audioContextManager.isReady()) { if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio); audioContextManager.init(this.audio);
this.applyReplayGain();
} }
await audioContextManager.resume(); await audioContextManager.resume();
this.playPrev(); this.playPrev();
@ -242,6 +253,7 @@ export class Player {
// Ensure audio context is active for iOS lock screen controls // Ensure audio context is active for iOS lock screen controls
if (!audioContextManager.isReady()) { if (!audioContextManager.isReady()) {
audioContextManager.init(this.audio); audioContextManager.init(this.audio);
this.applyReplayGain();
} }
await audioContextManager.resume(); await audioContextManager.resume();
this.playNext(); this.playNext();

View file

@ -2,7 +2,9 @@
export class WaveformGenerator { export class WaveformGenerator {
constructor() { constructor() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Use OfflineAudioContext to prevent creating unnecessary OS audio streams
// decodeAudioData doesn't require a real-time AudioContext
this.audioContext = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100);
this.cache = new Map(); this.cache = new Map();
} }