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.

F1ATB André

Ham Radio - Home automation - Photovoltaic