From f250d015b850898d2fb89c2ad81cab43b51da8ad Mon Sep 17 00:00:00 2001 From: dilgenfritz Date: Tue, 28 Oct 2025 08:12:16 -0500 Subject: [PATCH] fix: Prevent multiple audio streams and overlapping audio CRITICAL AUDIO FIXES: 1. DUPLICATE AUDIO PREVENTION: Fixed skipTask() triggering both 'mocking' + 'denial' punishment audio Added skipAudioPlayed flag to prevent consequence audio when skip audio is playing Modified displayCurrentTask() to check flag before playing consequence audio 2. AUDIO OVERLAP ELIMINATION: Made audio stopping in playAudio() synchronous instead of calling stopCategory() Forcibly stop existing audio immediately: pause() + currentTime=0 + src='' Enhanced stopAllImmediate() to use same aggressive stopping method Removed fadeOut delays in completeTask() and skipTask() (now immediate stop) 3. TIMING IMPROVEMENTS: completeTask() now stops audio immediately (0ms) before playing reward audio skipTask() stops task audio immediately (0ms) before punishment audio Added 100ms delay before reward audio to ensure previous audio fully stopped 4. TECHNICAL DETAILS: - Audio elements now get src='' to fully release resources - Synchronous audio stopping prevents race conditions - Clear fade timeouts to prevent delayed audio operations - Aggressive stopping in both individual playAudio() and global stopAllImmediate() EXPECTED RESULT: - Only ONE audio stream plays at a time - No overlapping punishment/task/reward audio - Clean audio transitions without interruption errors - Proper audio stopping when games end --- src/core/game.js | 29 ++++++++++++++++++++--------- src/features/audio/audioManager.js | 26 +++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/core/game.js b/src/core/game.js index 2016061..6eafd6b 100644 --- a/src/core/game.js +++ b/src/core/game.js @@ -4455,8 +4455,14 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag // Play task audio based on type if (this.gameState.isConsequenceTask) { - // Play punishment audio for consequence tasks - this.audioManager.playPunishmentAudio('denial', { fadeIn: 1000 }); + // Play punishment audio for consequence tasks, but not if we just played skip audio + if (!this.gameState.skipAudioPlayed) { + this.audioManager.playPunishmentAudio('denial', { fadeIn: 1000 }); + } else { + // Clear the flag for next time + this.gameState.skipAudioPlayed = false; + console.log('Skipping consequence audio - skip punishment audio already playing'); + } } else { // Determine audio intensity based on task difficulty const difficulty = this.gameState.currentTask.difficulty || 'Medium'; @@ -4623,12 +4629,15 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag completeTask() { if (!this.gameState.isRunning || this.gameState.isPaused) return; - // Stop current task audio - this.audioManager.stopCategory('tasks', 500); - this.audioManager.stopCategory('punishments', 500); + // Stop current task audio immediately (no fade to prevent overlap) + console.log('Task completed - stopping all audio immediately'); + this.audioManager.stopCategory('tasks', 0); + this.audioManager.stopCategory('punishments', 0); - // Play reward audio - this.audioManager.playRewardAudio({ fadeIn: 300 }); + // Brief delay before playing reward audio to ensure previous audio has stopped + setTimeout(() => { + this.audioManager.playRewardAudio({ fadeIn: 300 }); + }, 100); // Mark task as used and award points if (this.gameState.isConsequenceTask) { @@ -4717,8 +4726,9 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag return; } - // Stop current task audio - this.audioManager.stopCategory('tasks', 500); + // Stop current task audio immediately (no fade to prevent overlap) + console.log('Task skipped - stopping task audio immediately'); + this.audioManager.stopCategory('tasks', 0); // Play immediate punishment audio this.audioManager.playPunishmentAudio('mocking', { fadeIn: 200 }); @@ -4742,6 +4752,7 @@ ${usagePercent > 85 ? '⚠️ Storage getting full - consider deleting some imag // Load a consequence task this.gameState.isConsequenceTask = true; + this.gameState.skipAudioPlayed = true; // Flag to prevent duplicate audio this.loadNextTask(); this.updateStats(); diff --git a/src/features/audio/audioManager.js b/src/features/audio/audioManager.js index 2cd661c..042d02d 100644 --- a/src/features/audio/audioManager.js +++ b/src/features/audio/audioManager.js @@ -275,8 +275,15 @@ class AudioManager { console.log(`Selected audio file: ${audioFile.path}`); - // Stop any currently playing audio in this category - this.stopCategory(category); + // Stop any currently playing audio in this category immediately and synchronously + if (this.categories[category].currentAudio) { + const existingAudio = this.categories[category].currentAudio; + existingAudio.pause(); + existingAudio.currentTime = 0; + existingAudio.src = ''; // Clear source to fully stop + this.categories[category].currentAudio = null; + console.log(`Forcibly stopped existing audio in category: ${category}`); + } // Create and configure audio element const audio = new Audio(audioFile.path); @@ -368,7 +375,20 @@ class AudioManager { stopAllImmediate() { console.log('Stopping all audio immediately...'); for (const category in this.categories) { - this.stopCategory(category, 0); // No fade, immediate stop + if (this.categories[category].currentAudio) { + const audio = this.categories[category].currentAudio; + audio.pause(); + audio.currentTime = 0; + audio.src = ''; // Clear source to fully stop + this.categories[category].currentAudio = null; + console.log(`Forcibly stopped audio in category: ${category}`); + } + + // Clear any pending fade timeouts + if (this.categories[category].fadeTimeout) { + clearTimeout(this.categories[category].fadeTimeout); + this.categories[category].fadeTimeout = null; + } } }