Ga naar Audiowerklet

Chrome 64 wordt geleverd met een langverwachte nieuwe functie in de Web Audio API: AudioWorklet . Dit artikel introduceert het concept en het gebruik ervan voor degenen die graag een aangepaste audioprocessor met JavaScript-code willen maken. Bekijk de live demo's op GitHub. Ook 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 mooi binnen de audioverwerkingsthread - dat wil zeggen 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
Figuur 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);
});

Voor het maken van een AudioWorkletNode zijn minimaal twee dingen nodig: een AudioContext-object en de processornaam als string. 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.

Het is ook vermeldenswaard dat u AudioWorkletNode in een subklasse kunt plaatsen om een ​​aangepast knooppunt te definiëren dat wordt ondersteund door de processor die op de werklet draait.

// This is "processor.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 AudioParam

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

Audioworkletknooppunt en processordiagram
Fig. 2

Door de gebruiker gedefinieerde AudioParams kunnen worden gedeclareerd in een AudioWorkletProcessor-klassedefinitie door een set AudioParamDescriptors in te stellen. De onderliggende WebAudio-engine pikt deze informatie op bij de constructie van een AudioWorkletNode en zal vervolgens dienovereenkomstig AudioParam-objecten maken en aan het knooppunt koppelen.

/* 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 en moet door de gebruiker worden geïmplementeerd in de klassendefinitie. De WebAudio-engine zal deze functie op isochrone wijze aanroepen 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 regelen, zodat ontwikkelaars de geheugenvoetafdruk kunnen beheren. Als u false retourneert van process() methode, wordt de processor als inactief gemarkeerd en zal de WebAudio-engine de methode niet meer aanroepen. Om de processor in leven te houden, moet de methode true retourneren. Anders zal het knooppunt/processorpaar uiteindelijk door het systeem worden verzameld.

Bidirectionele communicatie met MessagePort

Soms willen aangepaste AudioWorkletNodes besturingselementen weergeven die niet zijn toegewezen aan AudioParam. Een tekenreeksgebaseerd type kan bijvoorbeeld worden gebruikt om een ​​aangepast filter te beheren. 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.

Fig. 2
Fig. 2

MessagePort is toegankelijk via .port attribuut op zowel het knooppunt als de processor. De methode port.postMessage() van het knooppunt verzendt een bericht naar de handler port.onmessage 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!');
});
/* "processor.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);

Houd er ook rekening mee dat MessagePort Transferable ondersteunt, 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.

Walkthrough: een GainNode bouwen

Alles bij elkaar genomen, is hier een compleet voorbeeld van GainNode gebouwd bovenop AudioWorkletNode en AudioWorkletProcessor.

Index.html

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

  // Loads module script via 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>

gain-processor.js

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.