diff --git a/frontend/src/components/AudioPlayer.tsx b/frontend/src/components/AudioPlayer.tsx index 9fc48ef..30ebdd3 100644 --- a/frontend/src/components/AudioPlayer.tsx +++ b/frontend/src/components/AudioPlayer.tsx @@ -14,6 +14,7 @@ import { RotateCw, } from 'lucide-react'; import AlertModal from './AlertModal'; +import AudioVisualizer from './AudioVisualizer'; import { useTheme } from '../context/ThemeContext'; interface AudioPlayerProps { @@ -127,21 +128,6 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating :
🎙️
} - {/* Barres égaliseur */} - {isPlaying && ( - - )} {isPlaying && (
En cours @@ -192,6 +178,11 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating
+ {/* Visualiseur audio */} +
+ +
+ {/* Controls */}
diff --git a/frontend/src/components/AudioVisualizer.tsx b/frontend/src/components/AudioVisualizer.tsx new file mode 100644 index 0000000..fc2e4d9 --- /dev/null +++ b/frontend/src/components/AudioVisualizer.tsx @@ -0,0 +1,75 @@ +import { useEffect, useRef, useState } from 'react'; +import { useAudio } from '../context/AudioContext'; + +interface AudioVisualizerProps { + barCount?: number; + height?: number; + gap?: number; +} + +export default function AudioVisualizer({ + barCount = 32, + height = 64, + gap = 2, +}: AudioVisualizerProps) { + const { isPlaying, getFrequencyData } = useAudio(); + const [heights, setHeights] = useState(() => Array(barCount).fill(4)); + const rafRef = useRef(0); + const playingRef = useRef(isPlaying); + const getDataRef = useRef(getFrequencyData); + + // Keep refs in sync without triggering re-runs + useEffect(() => { playingRef.current = isPlaying; }, [isPlaying]); + useEffect(() => { getDataRef.current = getFrequencyData; }, [getFrequencyData]); + + useEffect(() => { + const animate = () => { + const data = getDataRef.current?.(); + if (data && playingRef.current) { + const newHeights = Array.from({ length: barCount }, (_, i) => { + const idx = Math.floor((i / barCount) * data.length); + const value = data[idx] ?? 0; + // Boost mid-range frequencies for better podcast speech visuals + const boost = i > barCount * 0.2 && i < barCount * 0.8 ? 1.15 : 1; + return Math.max(4, Math.min(100, (value / 255) * 100 * boost)); + }); + setHeights(newHeights); + } else { + // Smooth decay when paused or no data + setHeights(prev => + prev.map(h => { + const next = h * 0.85; + return next < 4.5 ? 4 : next; + }) + ); + } + rafRef.current = requestAnimationFrame(animate); + }; + + rafRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(rafRef.current); + }, [barCount]); + + const barWidth = Math.max(2, Math.floor((100 - (barCount - 1) * gap) / barCount)); + + return ( +