AudioWorklet – Web Audio Api
La « Web Audio API » propose un système puissant et flexible pour contrôler les données audio sur une page web. Elle permet notamment de sélectionner des sources audio (microphone, flux media), de les traiter avec du filtrage ou autre, avant de les restituer en local ou les envoyer par internet.
Le traitement peut être décomposé en traitements élémentaires que l’on appelle « Audio Node », interconnectés entre eux. La « Web Audio API » dispose d’une bibliothèque d’Audio Nodes , comme du filtrage passe-haut (high pass filter), filtrage passe-bas (low pass filter) etc. Mais ici on va s’intéresser au cas où l’on souhaite créer son propre traitement sur les données Audio. Ce type de Node s’appelle un AudioWorklet.
Autorisation microphone
Avant tout essai, il faut s’affranchir du fait que les navigateurs web ne donnent pas accès au microphone si le site n’est pas à accès sécurisé en https ou sur localhost. Sur son réseau local à la maison, en général on travaille en http simplement. Pour contourner cette difficulté, la solution est de mettre en place une dérogation au niveau du navigateur web en accédant aux paramètres « flags ». il faut taper dans la barre d’adresse:
avec Chrome: chrome://flags
avec Edge(2020): edge://flags
Cherchez la rubrique:
Insecure origins treated as secure
Remplir le champ comme ci-dessous avec l’adresse IP de votre serveur qui fournit les pages.
Audio Worklet
L’Audio Worklet remplace le ScripProcessor Node devenu obsolète. Il se décompose en 2:
- l’audio worklet node qui s’interconnecte avec les autres nodes du traitement comme tout autre node de la « Web Audio API »
- l’audio worklet processor qui traite les échantillons audios avec son propre programme en Javascript. Il est décrit dans un fichier à part sous forme de module que l’on charge. Il sera executé dans un ‘thread’ en parallèle du ‘thread’ principal, ce qui apporte de l’intérêt pour le temps d’exécution et la latence.
Dans l’exemple ci-dessous, nous allons prendre le signal du microphone, l’envoyer uniquement sur le haut-parleur de gauche. Sur celui de droite nous allons envoyer une sinusoïde dont on fera varier le niveau.
Sélection de la source microphonique.
Pour sélectionner le micro, les navigateurs web, imposent 2 contraintes.
– Être sur un site sécurisé https ou avoir une dérogation comme décrit plus haut.
– Faire un click sur un bouton une fois la page web affichée
var MyAudio={Ctx:null,mic_stream:null,RightGain:0,node:null};
// Microphone Selection
function Start(){
MyAudio.Ctx = new AudioContext({sampleRate:10000}); //Force 10kHz as sampling rate for the microphone
if (!navigator.getUserMedia)
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia || navigator.msGetUserMedia;
if (navigator.getUserMedia){
//By default, connection to the microphone
navigator.getUserMedia({audio:true},
function(stream) {
start_microphone(stream);
},
function(e) {
alert('Error capturing audio.May be no microphone connected');
}
);
} else {
alert('getUserMedia not supported in this browser or access to a non secure server(not https)');
}
}
En premier il faut créer un contexte Audio qui rassemble toutes les caractéristiques du canal. On en profite pour réduire à 10kHz l’échantillonnage. Cela est suffisant pour un micro qui traite de la voix jusqu’à 3kHz environ.
MyAudio.Ctx = new AudioContext({sampleRate:10000})
Si tout ce passe bien en cliquant sur le bouton qui lance la fonction Start(), l’entrée microphone est choisie et on crée un flux appelé stream. que l’on passe à la fonction asynchrone start_microphone(stream) .
AudioWorklet Node
Le node d’entrée du microphone, MyAudio.mic_stream ,est crée.
async function start_microphone(stream){
//Microphone stream
MyAudio.mic_stream = MyAudio.Ctx.createMediaStreamSource(stream);
await MyAudio.Ctx.audioWorklet.addModule('MyWorklet.js?t=22') //Separate file containing the code of the AudioWorkletProcessor
MyAudio.node = new AudioWorkletNode(MyAudio.Ctx, 'MyWorkletProcessor'); //Link to MyWorkletProcessor defined in file MyWorklet.js
MyAudio.node.port.onmessage = event => {
if (event.data.MaxMicLevel) { //Message received from the AudioWorkletProcessor called 'MyWorkletProcessor'
let MaxMicLevel = event.data.MaxMicLevel;
let NbSample = event.data.NbSample
let SampleRate = event.data.SampleRate
console.log("Microphone",MaxMicLevel,NbSample,SampleRate);
}
}
Vol_Level( 0.5)
MyAudio.mic_stream.connect(MyAudio.node).connect(MyAudio.Ctx.destination) // Stream connected to the node then to the speakers
}
on indique, où se trouve le module de traitement.
await MyAudio.Ctx.audioWorklet.addModule(‘MyWorklet.js?t=22’)
C’est un fichier javascript se trouvant dans le même dossier que la page web principal. Le complément ?t=22, ne sert qu’à forcer les navigateurs à recharger la page à chaque fois. Ce qui est pratique en phase de développement pour que chaque modification passée dans le fichier soit prise en compte et non pas la page en cache. On peut y mettre n’importe quoi qui ressemble à un paramètre. Pensez à rajouter le chemin du dossier si l’ensemble n’est pas dans le même.
Ensuite on crée le node :
MyAudio.node = new AudioWorkletNode(MyAudio.Ctx, ‘MyWorkletProcessor’);
On lui indique le nom de la class qui contient le traitement à executer. Ici ‘ MyWorkletProcessor ‘.
Notre traitement du signal audio peut retourner des messages vers la page principale. Cela crée un évènement :
MyAudio.node.port.onmessage = event => { …}
Il est possible d’envoyer des messages pour positionner des paramètres. Ici ce sera le niveau audio de la sinusoïde sur l’haut-parleur de droite.
function Vol_Level(V){
MyAudio.RightGain=V;
MyAudio.node.port.postMessage({volumeRight :MyAudio.RightGain } ) //Message sent to the AudioWorkletProcessor called 'MyWorkletProcessor'
}
On interconnecte tous les nodes entre eux.
MyAudio.mic_stream.connect(MyAudio.node).connect(MyAudio.Ctx.destination)
Le node d’entrée du microphone, se connecte au node MyAudio.node que l’on a crée, lequel se connecte au node de sortie par défaut appelé MyAudio.Ctx. destination.
Audio Worklet Processor
On construit la class ‘MyWorkletProcessor’
class MyWorkletProcessor extends AudioWorkletProcessor {
constructor () {
super();
this.volumeRight =0;
this.port.onmessage = (e) => {
this.volumeRight = e.data.volumeRight
console.log("Volume Right",this.volumeRight);
}
}
process (inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
var MaxMicLevel=0;
for (var channel = 0; channel < input.length; ++channel) {
var inputChannel = input[channel]
var outputChannel = output[channel]
if(channel ==0){
for (var i = 0; i < inputChannel.length; ++i) {
outputChannel[i] = inputChannel[i];
MaxMicLevel=Math.max(MaxMicLevel,Math.abs(outputChannel[i]));
}
}
if(channel ==1){
for (var i = 0; i < inputChannel.length; ++i) {
outputChannel[i] =this.volumeRight*Math.sin(Phase++);
}
}
}
this.port.postMessage({MaxMicLevel: MaxMicLevel,NbSample:inputChannel.length, SampleRate: sampleRate });
return true;
}
};
registerProcessor('MyWorkletProcessor',MyWorkletProcessor);
On définit les paramètres qui nous serviront. Ici le niveau audio de la sinusoide que l’on recevra du worklet node.
this.volumeRight …
On décrit le processus de traitement qui se caractérise par des entrées et des sorties:
process (inputs, outputs, parameters) {..}. Il peut y avoir plusieurs entrée ‘inputs’. Ici, avec des cartes audio classique, on s’intéresse à la première uniquement inputs[0]. Cette entrée à 2 voies ‘channel’ pour pouvoir traiter de la stéréo si besoin.
Sur le channel 0, la voie de gauche, on copie en sortie outputChannel[i], tous les échantillons reçus (128) sur inputChannel[i]. Au passage, on mesure l’amplitude max que l’on enverra, par message,au node du thread principal.
Sur le channel 1, on n’utilise pas l’entrée, mais on envoie un sinus ‘Math.sin(Phase++)’ en faisant tourner la phase à chaque échantillon. Le niveau est modulé par le curseur défini en page principal.
En fin, on déclare et enregistre ce traitement :
registerProcessor(‘MyWorkletProcessor’,MyWorkletProcessor);
En faisant, tourner ce projet, et en regardant la sortie sur la console du navigateur, on constatera 2 points importants :
– les échantillons audio sont traités par paquet de 128
_ la dynamique des signaux est entre +1 et -1. Ne jamais aller au delà avec la ‘Web Audio API’.
Code Source de la page web
Ci-dessous le code html/javascript de la page principale à mettre dans un fichier, par exemple, ‘Worklet_demo.html’. Il contient la création du worklet node.
<html>
<head>
<title>AudioWorklet Example - F1ATB 2021</title>
</head>
<body>
<h1>AudioWorklet Example</h1>
<div> Speaker left channel : microphone</div>
<div> Speaker right channel : sine wave</div>
<div>
<label for="vol">Volume right channel(between 0 and 1):</label>
<input type="range" id="vol" name="vol" min="0" max="1" step="0.01" onmousemove=" Vol_Level( this.value);">
</div>
<br>
<button onclick="Start();">Click to start audio process</button>
<script type="text/javascript" charset="utf-8">
var MyAudio={Ctx:null,mic_stream:null,RightGain:0,node:null};
// Microphone Selection
function Start(){
MyAudio.Ctx = new AudioContext({sampleRate:10000}); //Force 10kHz as sampling rate for the microphone
if (!navigator.getUserMedia)
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia || navigator.msGetUserMedia;
if (navigator.getUserMedia){
//By default, connection to the microphone
navigator.getUserMedia({audio:true},
function(stream) {
start_microphone(stream);
},
function(e) {
alert('Error capturing audio.May be no microphone connected');
}
);
} else {
alert('getUserMedia not supported in this browser or access to a non secure server(not https)');
}
}
async function start_microphone(stream){
//Microphone stream
MyAudio.mic_stream = MyAudio.Ctx.createMediaStreamSource(stream);
await MyAudio.Ctx.audioWorklet.addModule('MyWorklet.js?t=22') //Separate file containing the code of the AudioWorkletProcessor
MyAudio.node = new AudioWorkletNode(MyAudio.Ctx, 'MyWorkletProcessor'); //Link to MyWorkletProcessor defined in file MyWorklet.js
MyAudio.node.port.onmessage = event => {
if (event.data.MaxMicLevel) { //Message received from the AudioWorkletProcessor called 'MyWorkletProcessor'
let MaxMicLevel = event.data.MaxMicLevel;
let NbSample = event.data.NbSample
let SampleRate = event.data.SampleRate
console.log("Microphone",MaxMicLevel,NbSample,SampleRate);
}
}
Vol_Level( 0.5)
MyAudio.mic_stream.connect(MyAudio.node).connect(MyAudio.Ctx.destination) // Stream connected to the node then to the speakers
}
function Vol_Level(V){
MyAudio.RightGain=V;
MyAudio.node.port.postMessage({volumeRight :MyAudio.RightGain } ) //Message sent to the AudioWorkletProcessor called 'MyWorkletProcessor'
}
</script>
</body>
</html>
Code source du Worklet Processor
Ci dessous, le module en javascript à mettre dans le fichier ‘MyWorklet.js’ et dans le même dossier que la page principale.
var Phase=0;
//"MyWorkletProcessor" is the name of the AudioWorkletProcessor defined below
class MyWorkletProcessor extends AudioWorkletProcessor {
constructor () {
super();
this.volumeRight =0;
this.port.onmessage = (e) => {
this.volumeRight = e.data.volumeRight
console.log("Volume Right",this.volumeRight);
}
}
process (inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
var MaxMicLevel=0;
for (var channel = 0; channel < input.length; ++channel) {
var inputChannel = input[channel]
var outputChannel = output[channel]
if(channel ==0){
for (var i = 0; i < inputChannel.length; ++i) {
outputChannel[i] = inputChannel[i];
MaxMicLevel=Math.max(MaxMicLevel,Math.abs(outputChannel[i]));
}
}
if(channel ==1){
for (var i = 0; i < inputChannel.length; ++i) {
outputChannel[i] =this.volumeRight*Math.sin(Phase++);
}
}
}
this.port.postMessage({MaxMicLevel: MaxMicLevel,NbSample:inputChannel.length, SampleRate: sampleRate });
return true;
}
};
registerProcessor('MyWorkletProcessor',MyWorkletProcessor);
Execution
Mettre l’ensemble sur un serveur web et lancer dans un navigateur moderne comme Chrome ou Edge, le fichier principal .html.
Démo en ligne
Il est possible, sur le serveur/
https://f1atb.fr/mes_pages/Worklet/Worklet_demo.html
de tester le fonctionnement.
Commentaires récents