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:
@@ -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")}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user