refactor(audio): Phase 2 — simplify AudioService to thin WaveSurfer wrapper

audioService.ts rewritten from ~850 lines to ~130:
- Remove custom logging system with throttle that suppressed ERROR logs
- Remove AudioContext management entirely (initializeAudioContext,
  handleAudioContextResume, setupAudioContext, shareAudioContextWithWaveSurfer,
  ensureAudioContext). WaveSurfer v7 owns its AudioContext; fighting it caused
  prod/dev divergence and silent failures.
- Replace 5-state InitializationState machine + split promise with a single
  isReady boolean set in the 'ready' event handler
- Remove retry/debounce logic from play() — these are UI concerns
- Remove dead methods: canPlayAudio (always returned true), getWaveSurferVersion,
  updatePlayerState, getAudioContextState, setLogLevel
- Extract destroyWaveSurfer() helper so cleanup is one place
- MiniPlayer now passes songId/bandId to play() (was calling with no args)
- SongPage spacebar handler simplified: just checks isReady from hook
- SongPage no longer imports audioService directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mistral Vibe
2026-04-08 20:47:10 +02:00
parent 1a0d926e1a
commit d4c0e9d776
4 changed files with 159 additions and 899 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { audioService, InitializationState } from "../services/audioService";
import { audioService } from "../services/audioService";
import { usePlayerStore } from "../stores/playerStore";
export interface UseWaveformOptions {
@@ -27,7 +27,6 @@ export function useWaveform(
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [error, setError] = useState<string | null>(null);
const [initializationState, setInitializationState] = useState<InitializationState>(InitializationState.NotStarted);
const markersRef = useRef<CommentMarker[]>([]);
useEffect(() => {
@@ -62,22 +61,19 @@ export function useWaveform(
options.bandId &&
currentPlayingSongId === options.songId &&
currentPlayingBandId === options.bandId &&
wasPlaying
wasPlaying &&
audioService.isWaveformReady()
) {
try {
if (audioService.canAttemptPlayback()) {
audioService.play(options.songId, options.bandId);
if (savedTime > 0) {
audioService.seekTo(savedTime);
}
}
await audioService.play(options.songId, options.bandId);
if (savedTime > 0) audioService.seekTo(savedTime);
} catch (err) {
console.warn('Auto-play prevented during initialization:', err);
}
}
// Sync local state from the store at ~15fps via RAF.
// The loop is started after initialization so we only poll when something is loaded.
// The loop is started after initialization so we only poll when loaded.
let lastUpdateTime = 0;
const updateInterval = 1000 / 15;
@@ -96,7 +92,6 @@ export function useWaveform(
animationFrameId = requestAnimationFrame(handleStateUpdate);
setIsReady(true);
setInitializationState(audioService.getInitializationState());
options.onReady?.(audioService.getDuration());
} catch (err) {
console.error('useWaveform: initialization failed', err);
@@ -115,92 +110,60 @@ export function useWaveform(
}, [options.url, options.songId, options.bandId]);
const play = () => {
// Use the unified readiness check
if (audioService.isReadyForPlayback()) {
try {
audioService.play(options.songId || null, options.bandId || null);
} catch (error) {
console.error('useWaveform.play failed:', error);
}
} else {
// If we can attempt playback (even during initialization), try it
if (audioService.canAttemptPlayback()) {
try {
audioService.play(options.songId || null, options.bandId || null);
} catch (error) {
console.error('useWaveform.play failed during initialization attempt:', error);
}
} else {
console.warn('Cannot play: not ready for playback', {
initializationState: audioService.getInitializationState(),
error: audioService.getInitializationError(),
duration: audioService.getDuration(),
url: options.url
});
}
if (!audioService.isWaveformReady()) {
console.warn('[useWaveform] play() called but not ready', { url: options.url });
return;
}
audioService.play(options.songId ?? null, options.bandId ?? null)
.catch(err => console.error('useWaveform.play failed:', err));
};
const pause = () => {
try {
audioService.pause();
} catch (error) {
console.error('useWaveform.pause failed:', error);
}
audioService.pause();
};
const seekTo = (time: number) => {
try {
if (isReady && isFinite(time)) {
audioService.seekTo(time);
}
} catch (error) {
console.error('useWaveform.seekTo failed:', error);
}
audioService.seekTo(time);
};
const addMarker = (marker: CommentMarker) => {
if (isReady) {
try {
// This would need proper implementation with the actual wavesurfer instance
const markerElement = document.createElement("div");
markerElement.style.position = "absolute";
markerElement.style.width = "24px";
markerElement.style.height = "24px";
markerElement.style.borderRadius = "50%";
markerElement.style.backgroundColor = "var(--accent)";
markerElement.style.cursor = "pointer";
markerElement.style.zIndex = "9999";
markerElement.style.left = `${(marker.time / audioService.getDuration()) * 100}%`;
markerElement.style.transform = "translateX(-50%) translateY(-50%)";
markerElement.style.top = "50%";
markerElement.style.border = "2px solid white";
markerElement.style.boxShadow = "0 0 4px rgba(0, 0, 0, 0.3)";
markerElement.title = `Comment at ${formatTime(marker.time)}`;
markerElement.onclick = marker.onClick;
if (!isReady) return;
try {
const markerElement = document.createElement("div");
markerElement.style.position = "absolute";
markerElement.style.width = "24px";
markerElement.style.height = "24px";
markerElement.style.borderRadius = "50%";
markerElement.style.backgroundColor = "var(--accent)";
markerElement.style.cursor = "pointer";
markerElement.style.zIndex = "9999";
markerElement.style.left = `${(marker.time / audioService.getDuration()) * 100}%`;
markerElement.style.transform = "translateX(-50%) translateY(-50%)";
markerElement.style.top = "50%";
markerElement.style.border = "2px solid white";
markerElement.style.boxShadow = "0 0 4px rgba(0, 0, 0, 0.3)";
markerElement.title = `Comment at ${formatTime(marker.time)}`;
markerElement.onclick = marker.onClick;
if (marker.icon) {
const iconElement = document.createElement("img");
iconElement.src = marker.icon;
iconElement.style.width = "100%";
iconElement.style.height = "100%";
iconElement.style.borderRadius = "50%";
iconElement.style.objectFit = "cover";
markerElement.appendChild(iconElement);
}
const waveformContainer = containerRef.current;
if (waveformContainer) {
waveformContainer.style.position = "relative";
waveformContainer.appendChild(markerElement);
}
markersRef.current.push(marker);
} catch (error) {
console.error('useWaveform.addMarker failed:', error);
if (marker.icon) {
const iconElement = document.createElement("img");
iconElement.src = marker.icon;
iconElement.style.width = "100%";
iconElement.style.height = "100%";
iconElement.style.borderRadius = "50%";
iconElement.style.objectFit = "cover";
markerElement.appendChild(iconElement);
}
const waveformContainer = containerRef.current;
if (waveformContainer) {
waveformContainer.style.position = "relative";
waveformContainer.appendChild(markerElement);
}
markersRef.current.push(marker);
} catch (err) {
console.error('useWaveform.addMarker failed:', err);
}
};
@@ -215,23 +178,11 @@ export function useWaveform(
markersRef.current = [];
};
return {
isPlaying,
isReady,
currentTime,
duration,
play,
pause,
seekTo,
addMarker,
clearMarkers,
error,
initializationState
};
return { isPlaying, isReady, currentTime, duration, play, pause, seekTo, addMarker, clearMarkers, error };
}
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${String(s).padStart(2, "0")}`;
}
}