2 août 2024
Créer un indicateur d'activité audio dans le navigateur
6 minutes de lecture
Lors d'un récent projet d'application de visioconférence, j'ai été confronté au défi de mettre en place une détection d'activité audio sur les vignettes des participants. Une fonctionnalité plutôt courante. Mais au cours de mes recherches, j'ai constaté que les ressources traitant de ce sujet étaient assez rares. C'est pourquoi j'ai décidé de partager mon retour d'expérience avec vous.
Dans cet article nous explorerons comment détecter une activité audio d'un participant en utilisant un flux MediaStream et l'API Web Audio du navigateur.
L'audio dans le navigateur
Pour développer cette fonctionnalité nous aurons besoin d'un MediaStream (qui représente le flux du participant) et de l'API Web Audio du navigateur (afin d'extraire des informations de ce flux). Pour ceux qui ne sont pas familiers avec ces concepts je vous fais une rapide description ci-dessous.
MediaStream
Un MediaStream est comme un flux de données multimédias en direct. Imaginez-le comme un tuyau par lequel passent le son et/ou la vidéo de votre microphone ou de votre caméra. Si vous vous demandez comment en obtenir un je vous conseille cet article.
Nous partons dans cet article du postulat que vous avez réussi à obtenir un flux MediaStream à analyser. Ce qui nous intéresse dans ce flux, ce sont les pistes audio.
Web Audio API & AnalyserNode
L'API Web Audio est un ensemble d'outils puissants fournis par le navigateur pour manipuler et analyser un ou des sons. Cette API est globalement plutôt bien supportée par les navigateurs. Parmi ces outils, nous allons nous concentrer sur l'analyseur (AnalyserNode). C'est un peu comme un appareil sophistiqué qui nous permet en temps réel de nous donner les caractéristiques et les informations détaillées d'un son. Cet analyseur, une fois connecté à un MediaStream, pourra nous donner les informations que nous recherchons à un instant T.
Analyser quoi ?
Pour détecter l'activité audio d'un participant nous avons besoin d'une fonction qui à un intervalle régulier nous permettrait de savoir si un flux audio est actif ou non.
Pour ce faire nous allons utiliser la notion de seuil (threshold).
Nous allons définir une valeur seuil qui représentera une intensité à partir de laquelle nous allons considérer que notre signal audio est actif. En dessous de ce seuil nous le considérons comme inactif.
Ce seuil est un paramètre qui peut être ajusté à l'usage par la suite (en fonction des bruits de fond et de la qualité du microphone, etc..).
Via un AnalyzerNode nous pouvons copier l'intensité de plusieurs plages de fréquence d'un signal audio à un instant T. Grâce à un petit calcul de moyenne, il nous est possible d'en déduire une intensité globale et de la comparer à notre valeur seuil.
Cela se traduit par cette fonction :
/**
* Check if the audio level is exceeding the threshold
*/
export function isAudioLevelExceedingThreshold({
threshold,
analyser,
}: {
threshold: number;
analyser: AnalyserNode;
}) {
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
const average =
dataArray.reduce((sum, value) => sum + value, 0) / dataArray.length;
const normalizedValue = average / 255;
return normalizedValue > threshold;
}
L'API Web Audio utilise des valeurs de fréquence sur 8 bits. Cela signifie que chaque valeur dans le dataArray
est un entier compris entre 0 et 255. Notre valeur average
est donc un entier compris entre 0 et 255. On normalise donc cette valeur pour la ramener vers une valeur comprise entre 0 et 1 (0 représentant le silence absolu et 1 l'intensité maximum possible).
À un intervalle régulier nous allons donc devoir appeler la fonction isAudioLevelExceedingThreshold
puis en fonction de son retour, true
ou false
, effectuer une action.
/**
* Check if the audio level is exceeding the threshold,
* and call the appropriate callback
*/
export function checkAudioLevel({
threshold,
analyser,
onExceedThreshold,
onBelowThreshold,
}: {
threshold: number;
analyser: AnalyserNode;
onExceedThreshold: () => void;
onBelowThreshold: () => void;
}) {
if (isAudioLevelExceedingThreshold({ threshold, analyser })) {
onExceedThreshold();
} else {
onBelowThreshold();
}
}
Voilà pour la partie logique de notre indicateur d'activité audio. Il ne nous reste plus qu'à l'intégrer dans notre application.
Intégration dans React
Ici, je vais vous montrer comment intégrer cette logique dans un composant React.
Pour ce faire je vais isoler la logique dans un hook custom useAudioActivityIndicator
qui prendra en paramètre un mediaStream
, une valeur threshold
et une valeur interval
(qui représente l'intervalle de temps entre chaque vérification).
Ce hook va gérer la création de l'AudioContext
, de l'AnalyserNode
et lancer la vérification de l'activité audio à intervalles réguliers.
Il nous retournera un état isExceedingThreshold
qui sera true
si l'activité audio est détectée et false
sinon.
import { useEffect, useRef, useState } from "react";
import { checkAudioLevel } from "./utils";
interface AudioLevelDetectorOptions {
threshold?: number; // a number between 0 and 1, where 1 represents the maximum audio level and 0 represents the minimum audio level
interval?: number;
}
const useAudioLevelDetector = (
mediaStream: MediaStream | null,
options: AudioLevelDetectorOptions = {}
): boolean => {
const [isExceedingThreshold, setIsExceedingThreshold] = useState(false);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const timeoutIdRef = useRef<number | null>(null);
const threshold = options.threshold || 0.1;
const interval = options.interval || 100;
function onInterval() {
if (!analyserRef.current) return;
checkAudioLevel({
threshold,
analyser: analyserRef.current!,
onExceedThreshold: () => {
setIsExceedingThreshold(true);
},
onBelowThreshold: () => {
setIsExceedingThreshold(false);
},
});
}
function onCleanUp() {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
if (audioContextRef.current && audioContextRef.current.state !== "closed") {
// Close the audio context if it is not closed
audioContextRef.current.close();
}
audioContextRef.current = null;
analyserRef.current = null;
}
useEffect(() => {
if (!mediaStream || !mediaStream.getAudioTracks().length) {
setIsExceedingThreshold(false);
return;
}
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext ||
(window as any).webkitAudioContext)(); // Create an AudioContext, or a webkitAudioContext for Safari
}
if (!analyserRef.current) {
analyserRef.current = audioContextRef.current.createAnalyser(); // Create an AnalyserNode
}
const source = audioContextRef.current.createMediaStreamSource(mediaStream); // Add the media stream to the audio context
source.connect(analyserRef.current); // Connect the source to the analyser
timeoutIdRef.current = window.setInterval(onInterval, interval);
onInterval();
return () => {
onCleanUp();
};
}, [mediaStream, threshold, interval]);
return isExceedingThreshold;
};
export default useAudioLevelDetector;
Il ne nous reste plus qu'à utiliser ce hook dans notre composant React, puis à appliquer le style de votre choix lorsque le seuil est dépassé.
Dans l'exemple suivant, j'ai choisi d'afficher des bordures blanches sur la vignette concernée.
C'est le composant <AudioActivityIndicator>
qui se charge de cela.
Repository Github de l'exemple
Vous pouvez bien sûr adapter le style à votre convenance et même modifier le comportement global de l'indicateur d'activité audio pour qu'il corresponde à vos besoins. Par exemple, au lieu de traiter l'activité audio de façon binaire (actif ou inactif), vous pourriez envisager de faire varier l'intensité de l'indicateur en fonction de l'intensité du son. Dans ce cas-là, ce n'est plus une notion de seuil qu'il faudra utiliser mais plutôt une notion de plage d'intensité.
Il est plus agréable de définir une animation réactive et instantanée quand l'état passe de non-actif → actif et au contraire d'animer avec un petit délai et plus progressivement dans l'animation inverse.
L'API Web Audio est un outil puissant qui permet de manipuler et d'analyser des sons dans le navigateur. Dans cet article nous avons effleuré le sujet en nous concentrant sur la détection d'activité audio. Mais si vous souhaitez aller plus loin, il est possible de faire bien plus de choses avec cette API. Je vous recommande cet article qui vous apprend comment jouer de la musique dans votre navigateur, ou bien de consulter la documentation officielle pour en apprendre plus sur le sujet.