diff --git a/web/src/hooks/useWaveform.ts b/web/src/hooks/useWaveform.ts index 5539b41..842e123 100755 --- a/web/src/hooks/useWaveform.ts +++ b/web/src/hooks/useWaveform.ts @@ -35,12 +35,16 @@ export function useWaveform( currentTime: globalCurrentTime, currentSongId, currentBandId: globalBandId, + currentPlayingSongId, + currentPlayingBandId, setCurrentSong } = usePlayerStore(state => ({ isPlaying: state.isPlaying, currentTime: state.currentTime, currentSongId: state.currentSongId, currentBandId: state.currentBandId, + currentPlayingSongId: state.currentPlayingSongId, + currentPlayingBandId: state.currentPlayingBandId, setCurrentSong: state.setCurrentSong })); diff --git a/web/src/services/audioService.ts b/web/src/services/audioService.ts index 064dbf4..12ed749 100755 --- a/web/src/services/audioService.ts +++ b/web/src/services/audioService.ts @@ -327,16 +327,22 @@ private readonly PLAY_DEBOUNCE_MS: number = 100; this.log(LogLevel.DEBUG, 'AudioService.play called', { songId, bandId }); try { + // Always stop current playback first to ensure only one audio plays at a time + if (this.isPlaying()) { + this.log(LogLevel.INFO, 'Stopping current playback before starting new one'); + this.pause(); + // Small delay to ensure cleanup + await new Promise(resolve => setTimeout(resolve, 50)); + } + // Check if we need to switch songs const isDifferentSong = songId && bandId && (this.currentPlayingSongId !== songId || this.currentPlayingBandId !== bandId); - // If switching to a different song, stop current playback first - if (isDifferentSong && this.isPlaying()) { - this.log(LogLevel.INFO, 'Switching songs - stopping current playback first'); - this.pause(); - // Small delay to ensure cleanup - await new Promise(resolve => setTimeout(resolve, 50)); + // If switching to a different song, perform cleanup + if (isDifferentSong) { + this.log(LogLevel.INFO, 'Switching to different song - performing cleanup'); + this.cleanup(); } // Ensure we have a valid audio context @@ -444,6 +450,10 @@ private readonly PLAY_DEBOUNCE_MS: number = 100; if (this.wavesurfer) { try { + // Always stop playback first + if (this.wavesurfer.isPlaying()) { + this.wavesurfer.pause(); + } // Disconnect audio nodes but keep audio context alive this.wavesurfer.unAll(); this.wavesurfer.destroy(); @@ -458,9 +468,10 @@ private readonly PLAY_DEBOUNCE_MS: number = 100; this.currentPlayingSongId = null; this.currentPlayingBandId = null; - // Reset player store + // Reset player store completely const playerStore = usePlayerStore.getState(); playerStore.setCurrentPlayingSong(null, null); + playerStore.batchUpdate({ isPlaying: false, currentTime: 0 }); // Note: We intentionally don't nullify audioContext to keep it alive } @@ -504,18 +515,35 @@ private readonly PLAY_DEBOUNCE_MS: number = 100; private setupAudioContext(ws: WaveSurferWithBackend) { // Simplified audio context setup - we now manage audio context centrally try { - // If we already have an audio context, use it for WaveSurfer + // If we already have an audio context, ensure WaveSurfer uses it if (this.audioContext) { - // Try to set the audio context for WaveSurfer if possible - if (ws.backend && typeof ws.backend.getAudioContext === 'function') { - // Some WaveSurfer versions allow setting the audio context - try { + // Try multiple ways to share the audio context with WaveSurfer + try { + // Method 1: Try to set via backend if available + if (ws.backend) { // @ts-expect-error - WaveSurfer typing doesn't expose this ws.backend.audioContext = this.audioContext; this.log(LogLevel.DEBUG, 'Shared audio context with WaveSurfer backend'); - } catch (error) { - this.log(LogLevel.DEBUG, 'Could not share audio context with WaveSurfer, but continuing'); } + + // Method 2: Try to access and replace the audio context + if (ws.backend?.getAudioContext) { + const originalGetAudioContext = ws.backend.getAudioContext; + // @ts-expect-error - Replace the method + ws.backend.getAudioContext = () => this.audioContext; + this.log(LogLevel.DEBUG, 'Overrode backend.getAudioContext with shared context'); + } + + // Method 3: Try top-level getAudioContext + if (typeof ws.getAudioContext === 'function') { + const originalGetAudioContext = ws.getAudioContext; + // @ts-expect-error - Replace the method + ws.getAudioContext = () => this.audioContext; + this.log(LogLevel.DEBUG, 'Overrode ws.getAudioContext with shared context'); + } + + } catch (error) { + this.log(LogLevel.WARN, 'Could not share audio context with WaveSurfer, but continuing:', error); } return; } diff --git a/web/tests/audioService.test.ts b/web/tests/audioService.test.ts index 1c5afa6..102036e 100644 --- a/web/tests/audioService.test.ts +++ b/web/tests/audioService.test.ts @@ -223,4 +223,37 @@ describe('initializeAudioContext', () => { }); }); +describe('cleanup', () => { + it('should stop playback and clean up properly', () => { + // Mock a playing wavesurfer instance + const mockWavesurfer = { + isPlaying: vi.fn(() => true), + pause: vi.fn(), + unAll: vi.fn(), + destroy: vi.fn() + }; + audioService['wavesurfer'] = mockWavesurfer; + + audioService['currentPlayingSongId'] = 'song-123'; + audioService['currentPlayingBandId'] = 'band-456'; + + audioService.cleanup(); + + expect(mockWavesurfer.pause).toHaveBeenCalled(); + expect(mockWavesurfer.unAll).toHaveBeenCalled(); + expect(mockWavesurfer.destroy).toHaveBeenCalled(); + expect(audioService['wavesurfer']).toBeNull(); + expect(audioService['currentPlayingSongId']).toBeNull(); + expect(audioService['currentPlayingBandId']).toBeNull(); + }); + + it('should handle cleanup when no wavesurfer instance exists', () => { + audioService['wavesurfer'] = null; + audioService['currentPlayingSongId'] = 'song-123'; + + expect(() => audioService.cleanup()).not.toThrow(); + expect(audioService['currentPlayingSongId']).toBeNull(); + }); +}); + }); \ No newline at end of file