Audio Worklet is nu standaard beschikbaar

Chrome 64 wordt geleverd met een langverwachte nieuwe functie in de Web Audio API: AudioWorklet . Hier leert u de concepten en het gebruik ervan om een ​​aangepaste audioprocessor met JavaScript-code te maken. Bekijk de live demo's . Het volgende artikel in de serie, Audio Worklet Design Pattern , kan interessant zijn om te lezen voor het bouwen van een geavanceerde audio-app.

Achtergrond: ScriptProcessorNode

Audioverwerking in Web Audio API wordt uitgevoerd in een aparte thread van de hoofdthread van de UI, zodat deze soepel verloopt. Om aangepaste audioverwerking in JavaScript mogelijk te maken, stelde de Web Audio API een ScriptProcessorNode voor die gebeurtenishandlers gebruikte om gebruikersscript op te roepen in de hoofdthread van de UI.

Er zijn twee problemen bij dit ontwerp: de afhandeling van gebeurtenissen is asynchroon van opzet en de uitvoering van de code vindt plaats op de hoofdthread. De eerste veroorzaakt de latentie, en de laatste zet de rode draad onder druk die gewoonlijk vol zit met verschillende UI- en DOM-gerelateerde taken, waardoor de gebruikersinterface "jankt" of de audio "glitcht". Vanwege deze fundamentele ontwerpfout wordt ScriptProcessorNode uit de specificatie gehaald en vervangen door AudioWorklet.

Concepten

Audio Worklet houdt de door de gebruiker aangeleverde JavaScript-code binnen de audioverwerkingsthread. Dat betekent dat het niet naar de hoofdthread hoeft te springen om audio te verwerken. Dit betekent dat de door de gebruiker aangeleverde scriptcode kan worden uitgevoerd op de audioweergavethread ( AudioWorkletGlobalScope ) samen met andere ingebouwde AudioNodes , wat zorgt voor nul extra latentie en synchrone weergave.

Belangrijkste globale scope en audioworklet-scopediagram
Afb.1

Registratie en instantiatie

Het gebruik van Audio Worklet bestaat uit twee delen: AudioWorkletProcessor en AudioWorkletNode . Dit is meer betrokken dan het gebruik van ScriptProcessorNode, maar het is nodig om ontwikkelaars de mogelijkheid te bieden op laag niveau voor aangepaste audioverwerking. AudioWorkletProcessor vertegenwoordigt de daadwerkelijke audioprocessor geschreven in JavaScript-code, en bevindt zich in de AudioWorkletGlobalScope . AudioWorkletNode is de tegenhanger van AudioWorkletProcessor en verzorgt de verbinding van en naar andere AudioNodes in de hoofdthread. Het wordt weergegeven in de belangrijkste globale reikwijdte en functioneert als een gewone AudioNode .

Hier zijn een paar codefragmenten die de registratie en de instantiatie demonstreren.

// The code in the main global scope.
class MyWorkletNode extends AudioWorkletNode {
  constructor(context) {
    super(context, 'my-worklet-processor');
  }
}

let context = new AudioContext();

context.audioWorklet.addModule('processors.js').then(() => {
  let node = new MyWorkletNode(context);
});

Om een AudioWorkletNode te maken, moet u een AudioContext-object en de processornaam als tekenreeks toevoegen. Een processordefinitie kan worden geladen en geregistreerd door de addModule() -aanroep van het nieuwe Audio Worklet-object. Worklet-API's, waaronder Audio Worklet, zijn alleen beschikbaar in een beveiligde context . Een pagina die deze gebruikt, moet dus via HTTPS worden aangeboden, hoewel http://localhost als veilig wordt beschouwd voor lokaal testen.

U kunt AudioWorkletNode subklassen om een ​​aangepast knooppunt te definiëren dat wordt ondersteund door de processor die op de werklet draait.

// This is the "processors.js" file, evaluated in AudioWorkletGlobalScope
// upon audioWorklet.addModule() call in the main global scope.
class MyWorkletProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
  }

  process(inputs, outputs, parameters) {
    // audio processing code here.
  }
}

registerProcessor('my-worklet-processor', MyWorkletProcessor);

De methode registerProcessor() in AudioWorkletGlobalScope gebruikt een tekenreeks voor de naam van de te registreren processor en de klassedefinitie. Nadat de evaluatie van de scriptcode in het globale bereik is voltooid, wordt de belofte van AudioWorklet.addModule() opgelost door gebruikers op de hoogte te stellen dat de klassendefinitie klaar is om te worden gebruikt in het algemene globale bereik.

Aangepaste audioparameters

Een van de nuttige dingen van AudioNodes is planbare parameterautomatisering met AudioParam . AudioWorkletNodes kan deze gebruiken om blootgestelde parameters te verkrijgen die automatisch op de audiosnelheid kunnen worden geregeld.

Audioworkletknooppunt en processordiagram
Afb.2

Door de gebruiker gedefinieerde audioparameters kunnen worden gedeclareerd in een AudioWorkletProcessor -klassedefinitie door een set AudioParamDescriptor in te stellen. De onderliggende WebAudio-engine pikt deze informatie op tijdens de constructie van een AudioWorkletNode en maakt vervolgens AudioParam objecten aan en koppelt deze aan het knooppunt.

/* A separate script file, like "my-worklet-processor.js" */
class MyWorkletProcessor extends AudioWorkletProcessor {

  // Static getter to define AudioParam objects in this custom processor.
  static get parameterDescriptors() {
    return [{
      name: 'myParam',
      defaultValue: 0.707
    }];
  }

  constructor() { super(); }

  process(inputs, outputs, parameters) {
    // |myParamValues| is a Float32Array of either 1 or 128 audio samples
    // calculated by WebAudio engine from regular AudioParam operations.
    // (automation methods, setter) Without any AudioParam change, this array
    // would be a single value of 0.707.
    const myParamValues = parameters.myParam;

    if (myParamValues.length === 1) {
      // |myParam| has been a constant value for the current render quantum,
      // which can be accessed by |myParamValues[0]|.
    } else {
      // |myParam| has been changed and |myParamValues| has 128 values.
    }
  }
}

AudioWorkletProcessor.process() methode

De daadwerkelijke audioverwerking vindt plaats in de callback-methode process() in de AudioWorkletProcessor . Het moet door een gebruiker in de klassendefinitie worden geïmplementeerd. De WebAudio-engine roept deze functie op isochrone wijze aan om invoer en parameters in te voeren en uitvoer op te halen.

/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
  // The processor may have multiple inputs and outputs. Get the first input and
  // output.
  const input = inputs[0];
  const output = outputs[0];

  // Each input or output may have multiple channels. Get the first channel.
  const inputChannel0 = input[0];
  const outputChannel0 = output[0];

  // Get the parameter value array.
  const myParamValues = parameters.myParam;

  // if |myParam| has been a constant value during this render quantum, the
  // length of the array would be 1.
  if (myParamValues.length === 1) {
    // Simple gain (multiplication) processing over a render quantum
    // (128 samples). This processor only supports the mono channel.
    for (let i = 0; i < inputChannel0.length; ++i) {
      outputChannel0[i] = inputChannel0[i] * myParamValues[0];
    }
  } else {
    for (let i = 0; i < inputChannel0.length; ++i) {
      outputChannel0[i] = inputChannel0[i] * myParamValues[i];
    }
  }

  // To keep this processor alive.
  return true;
}

Bovendien kan de retourwaarde van de methode process() worden gebruikt om de levensduur van AudioWorkletNode te controleren, zodat ontwikkelaars de geheugenvoetafdruk kunnen beheren. Het retourneren van false van process() methode markeert de processor inactief en de WebAudio engine roept de methode niet langer aan. Om de processor in leven te houden, moet de methode true retourneren. Anders wordt het knooppunt en het processorpaar uiteindelijk door het systeem verzameld.

Bidirectionele communicatie met MessagePort

Soms wil een aangepaste AudioWorkletNode besturingselementen vrijgeven die niet zijn toegewezen aan AudioParam , zoals een op tekenreeksen gebaseerd type dat wordt gebruikt om een ​​aangepast filter te besturen. Voor dit doel en daarbuiten zijn AudioWorkletNode en AudioWorkletProcessor uitgerust met een MessagePort voor bidirectionele communicatie. Via dit kanaal kunnen alle soorten aangepaste gegevens worden uitgewisseld.

Afb.2
Afb.2

MessagePort is toegankelijk met het .port attribuut op zowel het knooppunt als de processor. De port.postMessage() methode van het knooppunt verzendt een bericht naar de port.onmessage handler van de bijbehorende processor en omgekeerd.

/* The code in the main global scope. */
context.audioWorklet.addModule('processors.js').then(() => {
  let node = new AudioWorkletNode(context, 'port-processor');
  node.port.onmessage = (event) => {
    // Handling data from the processor.
    console.log(event.data);
  };

  node.port.postMessage('Hello!');
});
/* "processors.js" file. */
class PortProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.port.onmessage = (event) => {
      // Handling data from the node.
      console.log(event.data);
    };

    this.port.postMessage('Hi!');
  }

  process(inputs, outputs, parameters) {
    // Do nothing, producing silent output.
    return true;
  }
}

registerProcessor('port-processor', PortProcessor);

MessagePort ondersteunt overdraagbaar, waarmee u gegevensopslag of een WASM-module over de threadgrens kunt overbrengen. Dit opent talloze mogelijkheden voor de manier waarop het Audio Worklet-systeem kan worden gebruikt.

Doorloop: Bouw een GainNode

Hier is een compleet voorbeeld van GainNode gebouwd bovenop AudioWorkletNode en AudioWorkletProcessor .

Het index.html bestand:

<!doctype html>
<html>
<script>
  const context = new AudioContext();

  // Loads module script with AudioWorklet.
  context.audioWorklet.addModule('gain-processor.js').then(() => {
    let oscillator = new OscillatorNode(context);

    // After the resolution of module loading, an AudioWorkletNode can be
    // constructed.
    let gainWorkletNode = new AudioWorkletNode(context, 'gain-processor');

    // AudioWorkletNode can be interoperable with other native AudioNodes.
    oscillator.connect(gainWorkletNode).connect(context.destination);
    oscillator.start();
  });
</script>
</html>

Het gain-processor.js bestand:

class GainProcessor extends AudioWorkletProcessor {

  // Custom AudioParams can be defined with this static getter.
  static get parameterDescriptors() {
    return [{ name: 'gain', defaultValue: 1 }];
  }

  constructor() {
    // The super constructor call is required.
    super();
  }

  process(inputs, outputs, parameters) {
    const input = inputs[0];
    const output = outputs[0];
    const gain = parameters.gain;
    for (let channel = 0; channel < input.length; ++channel) {
      const inputChannel = input[channel];
      const outputChannel = output[channel];
      if (gain.length === 1) {
        for (let i = 0; i < inputChannel.length; ++i)
          outputChannel[i] = inputChannel[i] * gain[0];
      } else {
        for (let i = 0; i < inputChannel.length; ++i)
          outputChannel[i] = inputChannel[i] * gain[i];
      }
    }

    return true;
  }
}

registerProcessor('gain-processor', GainProcessor);

Dit behandelt de basis van het Audio Worklet-systeem. Live demo's zijn beschikbaar in de GitHub-repository van het Chrome WebAudio-team .

Functieovergang: experimenteel naar stabiel

Audio Worklet is standaard ingeschakeld voor Chrome 66 of hoger. In Chrome 64 en 65 zat de functie achter de experimentele vlag.