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é

Radio Amateur - Domotique - Photovoltaïque

Vous aimerez aussi...

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *