feat: VU-mètre audio entrant fonctionnel + simplification UI
Modifications : - Ajout analyseur audio pour pistes distantes (remoteAnalyserRef) - setupRemoteAudioAnalyser() appelé sur TrackSubscribed - analyseAudioLevel() alterne automatiquement entre micro local et audio entrant - useEffect redémarre analyse quand isTalking change - Cleanup complet des 2 contextes audio (local + remote) UI VU-mètre : - Suppression jauge redondante (gardé uniquement barres) - Barres uniformes (même hauteur) au lieu d'effet égaliseur - Couleurs distinctes : vert (audio entrant) vs bleu/rouge (micro) - Jaune > 75%, rouge clignotant > 90% Tests validés : ✅ VU-mètre micro local : fonctionne ✅ VU-mètre audio entrant : fonctionne (fix principal) ✅ Alternance automatique talking/listening 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -56,18 +56,20 @@
|
|||||||
|
|
||||||
.audio-bar {
|
.audio-bar {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: var(--color-bg);
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.1s ease;
|
transition: all 0.1s ease;
|
||||||
min-height: 4px;
|
|
||||||
height: 20%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-bar.active {
|
.audio-bar.active {
|
||||||
height: 100%;
|
|
||||||
background: var(--color-success);
|
background: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-bar.active.talking {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.audio-bar.active.warning {
|
.audio-bar.active.warning {
|
||||||
background: var(--color-warning);
|
background: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,9 @@ export default function AudioIndicator({ level, isTalking }) {
|
|||||||
<div className="audio-indicator-container">
|
<div className="audio-indicator-container">
|
||||||
<div className="audio-indicator-label">
|
<div className="audio-indicator-label">
|
||||||
<span>{isTalking ? 'Votre micro' : 'Audio entrant'}</span>
|
<span>{isTalking ? 'Votre micro' : 'Audio entrant'}</span>
|
||||||
<span className="audio-level-value">{Math.round(normalizedLevel)}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="audio-indicator-bar">
|
{/* VU-mètre barres */}
|
||||||
<div
|
|
||||||
className={`audio-indicator-fill ${isTalking ? 'talking' : ''}`}
|
|
||||||
style={{ width: `${normalizedLevel}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bars VU-mètre style */}
|
|
||||||
<div className="audio-bars">
|
<div className="audio-bars">
|
||||||
{[...Array(20)].map((_, i) => {
|
{[...Array(20)].map((_, i) => {
|
||||||
const threshold = (i + 1) * 5;
|
const threshold = (i + 1) * 5;
|
||||||
@@ -34,7 +26,7 @@ export default function AudioIndicator({ level, isTalking }) {
|
|||||||
key={i}
|
key={i}
|
||||||
className={`audio-bar ${isActive ? 'active' : ''} ${
|
className={`audio-bar ${isActive ? 'active' : ''} ${
|
||||||
isActive && isDanger ? 'danger' : isActive && isWarning ? 'warning' : ''
|
isActive && isDanger ? 'danger' : isActive && isWarning ? 'warning' : ''
|
||||||
}`}
|
} ${isTalking ? 'talking' : ''}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export default function useLiveKit() {
|
|||||||
const animationFrameRef = useRef(null);
|
const animationFrameRef = useRef(null);
|
||||||
const isAudioUnlockedRef = useRef(false);
|
const isAudioUnlockedRef = useRef(false);
|
||||||
|
|
||||||
|
// Analyseur audio pour pistes distantes (audio entrant)
|
||||||
|
const remoteAudioContextRef = useRef(null);
|
||||||
|
const remoteAnalyserRef = useRef(null);
|
||||||
|
const remoteAnimationFrameRef = useRef(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connexion à la room LiveKit
|
* Connexion à la room LiveKit
|
||||||
*/
|
*/
|
||||||
@@ -68,6 +73,9 @@ export default function useLiveKit() {
|
|||||||
if (track.kind === Track.Kind.Audio) {
|
if (track.kind === Track.Kind.Audio) {
|
||||||
const audioElement = track.attach();
|
const audioElement = track.attach();
|
||||||
document.body.appendChild(audioElement);
|
document.body.appendChild(audioElement);
|
||||||
|
|
||||||
|
// Setup analyseur pour audio entrant
|
||||||
|
setupRemoteAudioAnalyser(track);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +100,8 @@ export default function useLiveKit() {
|
|||||||
// Mute par défaut (PTT)
|
// Mute par défaut (PTT)
|
||||||
track.mute();
|
track.mute();
|
||||||
setupAudioAnalyser(track);
|
setupAudioAnalyser(track);
|
||||||
|
// Démarrer l'analyse audio
|
||||||
|
analyseAudioLevel();
|
||||||
console.log('✓ Track audio configuré et muted pour PTT');
|
console.log('✓ Track audio configuré et muted pour PTT');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -249,7 +259,7 @@ export default function useLiveKit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup analyseur audio pour VU-mètre
|
* Setup analyseur audio pour VU-mètre (micro local)
|
||||||
*/
|
*/
|
||||||
const setupAudioAnalyser = (track) => {
|
const setupAudioAnalyser = (track) => {
|
||||||
try {
|
try {
|
||||||
@@ -266,23 +276,47 @@ export default function useLiveKit() {
|
|||||||
audioContextRef.current = audioContext;
|
audioContextRef.current = audioContext;
|
||||||
analyserRef.current = analyser;
|
analyserRef.current = analyser;
|
||||||
|
|
||||||
// Démarrer analyse
|
console.log('✓ Analyseur audio local configuré');
|
||||||
analyseAudioLevel();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur setup analyser:', error);
|
console.error('Erreur setup analyser local:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup analyseur audio pour pistes distantes (audio entrant)
|
||||||
|
*/
|
||||||
|
const setupRemoteAudioAnalyser = (track) => {
|
||||||
|
try {
|
||||||
|
const mediaStream = track.mediaStream;
|
||||||
|
if (!mediaStream) return;
|
||||||
|
|
||||||
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
analyser.fftSize = 256;
|
||||||
|
source.connect(analyser);
|
||||||
|
|
||||||
|
remoteAudioContextRef.current = audioContext;
|
||||||
|
remoteAnalyserRef.current = analyser;
|
||||||
|
|
||||||
|
console.log('✓ Analyseur audio distant configuré');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur setup analyser distant:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyser niveau audio (pour VU-mètre)
|
* Analyser niveau audio (pour VU-mètre)
|
||||||
|
* Alterne entre micro local (si talking) et audio entrant (si listening)
|
||||||
*/
|
*/
|
||||||
const analyseAudioLevel = () => {
|
const analyseAudioLevel = useCallback(() => {
|
||||||
if (!analyserRef.current) return;
|
|
||||||
|
|
||||||
const analyser = analyserRef.current;
|
|
||||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
||||||
|
|
||||||
const analyse = () => {
|
const analyse = () => {
|
||||||
|
// Choisir l'analyseur selon l'état
|
||||||
|
const analyser = isTalking ? analyserRef.current : remoteAnalyserRef.current;
|
||||||
|
|
||||||
|
if (analyser) {
|
||||||
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||||
analyser.getByteFrequencyData(dataArray);
|
analyser.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
// Calculer moyenne
|
// Calculer moyenne
|
||||||
@@ -290,12 +324,15 @@ export default function useLiveKit() {
|
|||||||
const normalized = Math.min(100, (average / 255) * 100);
|
const normalized = Math.min(100, (average / 255) * 100);
|
||||||
|
|
||||||
setAudioLevel(normalized);
|
setAudioLevel(normalized);
|
||||||
|
} else {
|
||||||
|
setAudioLevel(0);
|
||||||
|
}
|
||||||
|
|
||||||
animationFrameRef.current = requestAnimationFrame(analyse);
|
animationFrameRef.current = requestAnimationFrame(analyse);
|
||||||
};
|
};
|
||||||
|
|
||||||
analyse();
|
analyse();
|
||||||
};
|
}, [isTalking]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup
|
* Cleanup
|
||||||
@@ -306,15 +343,39 @@ export default function useLiveKit() {
|
|||||||
animationFrameRef.current = null;
|
animationFrameRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remoteAnimationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(remoteAnimationFrameRef.current);
|
||||||
|
remoteAnimationFrameRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (audioContextRef.current) {
|
if (audioContextRef.current) {
|
||||||
audioContextRef.current.close();
|
audioContextRef.current.close();
|
||||||
audioContextRef.current = null;
|
audioContextRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remoteAudioContextRef.current) {
|
||||||
|
remoteAudioContextRef.current.close();
|
||||||
|
remoteAudioContextRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
analyserRef.current = null;
|
analyserRef.current = null;
|
||||||
|
remoteAnalyserRef.current = null;
|
||||||
localTrackRef.current = null;
|
localTrackRef.current = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Redémarrer l'analyse audio quand isTalking change
|
||||||
|
useEffect(() => {
|
||||||
|
if (animationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
animationFrameRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redémarrer l'analyse si on a au moins un analyseur
|
||||||
|
if (analyserRef.current || remoteAnalyserRef.current) {
|
||||||
|
analyseAudioLevel();
|
||||||
|
}
|
||||||
|
}, [isTalking, analyseAudioLevel]);
|
||||||
|
|
||||||
// Cleanup au démontage
|
// Cleanup au démontage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user