diff --git a/frontend/src/components/AudioVisualizer.tsx b/frontend/src/components/AudioVisualizer.tsx index cc77b85..ef70fbe 100644 --- a/frontend/src/components/AudioVisualizer.tsx +++ b/frontend/src/components/AudioVisualizer.tsx @@ -78,13 +78,17 @@ export default function AudioVisualizer({ height = 80 }: AudioVisualizerProps) { hts[i] += (target - hts[i]) * 0.22; } } else { - // Simulated — symmetric wave emanating from center + // Simulated speech-like: syllabic rhythm ~4/sec at 60fps + const syllable = Math.max(0, Math.sin(frame * 0.038)) ** 2; + const breathGroup = Math.sin(frame * 0.007) * 0.25 + 0.75; + const consonant = Math.random() < 0.025 ? Math.random() * 0.6 : 0; for (let i = 0; i < HALF; i++) { - const slow = Math.sin(frame * 0.025 + i * 0.38) * 0.5 + 0.5; - const fast = Math.sin(frame * 0.08 + i * 1.1) * 0.35 + 0.65; - const pulse = Math.sin(frame * 0.012) * 0.2 + 0.8; - const target = MAX_AMPLITUDE * slow * fast * pulse; - hts[i] += (target - hts[i]) * 0.18; + const t = i / (HALF - 1); // 0=center(bass), 1=edge(treble) + // bass tracks syllables, treble spikes on consonants + const lowEnergy = syllable * breathGroup * (1 - t * 0.5); + const highEnergy = (consonant + syllable * 0.3) * (0.3 + t * 0.7); + const target = MAX_AMPLITUDE * (lowEnergy * 0.7 + highEnergy * 0.3); + hts[i] += (target - hts[i]) * 0.2; } } } else { diff --git a/frontend/src/context/AudioContext.tsx b/frontend/src/context/AudioContext.tsx index 97eb25b..e816ab2 100644 --- a/frontend/src/context/AudioContext.tsx +++ b/frontend/src/context/AudioContext.tsx @@ -36,6 +36,7 @@ export function AudioProvider({ children }: { children: ReactNode }) { const webAudioRef = useRef(null); const analyserRef = useRef(null); const dataArrayRef = useRef(null); + const savedPositionRef = useRef(null); const [currentEpisode, setCurrentEpisode] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); @@ -97,14 +98,21 @@ export function AudioProvider({ children }: { children: ReactNode }) { }; }, [currentEpisode?._id, userId, isPlaying]); - // Resume at previous position when episode loads + // Resume at previous position: store in ref, applied on loadedmetadata + // (handles the case where CORS failure remounts the audio element) useEffect(() => { if (!currentEpisode || !userId) return; + savedPositionRef.current = null; setIsResuming(true); episodeService.getProgress(currentEpisode._id).then(r => { - if (r.progress?.position && audioRef.current) { - audioRef.current.currentTime = r.progress.position; - setCurrentTime(r.progress.position); + const pos = r.progress?.position; + if (pos && pos > 5) { + savedPositionRef.current = pos; + if (audioRef.current && audioRef.current.readyState >= 1) { + audioRef.current.currentTime = pos; + setCurrentTime(pos); + savedPositionRef.current = null; + } } setIsResuming(false); }).catch(() => setIsResuming(false)); @@ -121,8 +129,8 @@ export function AudioProvider({ children }: { children: ReactNode }) { const ctx = new AudioCtx(); const source = ctx.createMediaElementSource(audioRef.current); const analyser = ctx.createAnalyser(); - analyser.fftSize = 128; - analyser.smoothingTimeConstant = 0.8; + analyser.fftSize = 256; + analyser.smoothingTimeConstant = 0.55; source.connect(analyser); analyser.connect(ctx.destination); @@ -226,7 +234,15 @@ export function AudioProvider({ children }: { children: ReactNode }) { src={currentEpisode.audioUrl} crossOrigin={corsMode === 'cors' ? 'anonymous' : undefined} onTimeUpdate={() => audioRef.current && setCurrentTime(audioRef.current.currentTime)} - onLoadedMetadata={() => audioRef.current && setDuration(audioRef.current.duration)} + onLoadedMetadata={() => { + if (!audioRef.current) return; + setDuration(audioRef.current.duration); + if (savedPositionRef.current !== null) { + audioRef.current.currentTime = savedPositionRef.current; + setCurrentTime(savedPositionRef.current); + savedPositionRef.current = null; + } + }} onPlay={() => setIsPlaying(true)} onPause={() => setIsPlaying(false)} onEnded={() => setIsPlaying(false)}