WebAudio
webmusic

この記事は WebAudio/WebMIDI API Advent Calendar 2017 Advent Calendar 2017の25日目です。

TL;DR

この記事はChromiumのWeb AudioのコミッタであるSoftware EngineerのHongchan Choiが書いたEnter AudioWorkletの記事の邦訳です。
(This is the Japanese version of 'Enter AudioWorklet' of Google Developers.)

前書き

Chrome 64(実験的フラグ付きではあるものの)で待ち望まれていたWeb Audioの新しい機能AudioWorkletが利用可能になります。この記事ではJavaScriptでカスタムのAudio処理を作つことを首を長くして待っていた開発者へAPIのコンセプトと使い方の紹介を書いたものです。GitHubで公開しているライブデモ、またChrome64でこの実験的APIを使うための手順をご覧ください。

バックグラウンドの軽い説明

AudioWorkletの位置づけはScriptProcessorNodeの後継です。では、なぜScriptProcessorNodeは変更されるのでしょうか?
Web Audio APIはAudio processing(音声処理)を出来る限りスムーズに行う為にブラウザのメインのスレッドであるUIのスレッドとは別のスレッド(Audioスレッド)で動作しています。JavaScriptでカスタムの音声処理をする為に、Web Audio APIでは「ユーザが記述したメインスレッドで動作するスクリプトの実行をAudioスレッドからイベントハンドラを使って行う」ScriptProcessorNodeが定義され、実装されています。

このScriptProcessorNodeには2つの問題があります。
イベントのハンドリングは非同期に行われるような仕様になっていて、そしてイベントで動作するコードはメインスレッドで動作します。よって前者はレイテンシ(遅延)を、後者はUIやDOMに関連したタスクをより過密化させてることでメインスレッドに圧力を与え、UIに対してカクカクした動作等を代表されるようなUIの非スムースな動作「ジャンク」、また不自然な音切れに代表されるような音声再生中の問題「グリッチ」を引き起こします。
この基本的な欠陥がある為にScriptProcessorNodeはWeb Audio APIの仕様から削除され、AudioWorkletに置き換えられるのです。

Concepts

AudioWorkletは開発者が記述したJavaScriptのコードをAudioスレッドで動作させることができます。音声処理をメインスレッドにジャンプさせて行う必要がなくなりました。開発者が記述したJavaScriptのコードは他のAudioNodeが実装されているオーディオスレッド(AudioWorkletGlobalScope)で動作するようになるので、処理をする為にレイテンシを増加させることなく、また同期して音声を発せさせることを確実なものにしています。

Screen Shot 2017-12-23 at 1.37.06 PM.png

登録とインストール

AudioWorkletで音声処理を記述するには2つの要素から構成します。それらは、AudioWorkletProcessorAudioWorketNodeです。これはScriptProcessorNodeを使う場合に比べて複雑になりますが、開発者にカスタムの音声処理の為にLow Levelの機能を提供する必要があるからです。
AudioWorketProcessorはJavaScriptで記述された実際の音声処理のことを示し、AudioWorketGlobalScopeにスコープされます。
AudioWorkletNodeは他のAudioNodeとのメッセージのやり取りをAudioWorketProcessorに呼応して動作することで面倒をみます。そして、他のAudioNodeと同じくGlobal Scopeで動作します。

以下がAudioWorkletの登録とインストールを行うコードスニペットです。

// 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);
});

AudioWorkletNodeを作るには少なくとも2つのことが要求されます。その2つとはAudioContectのオブジェクトとその処理の名前がstringで定義されていることです。処理の定義は新しいAudioWorkletオブジェクトのAddModule()で読み込まれ、登録されます。AudioWorkletを含むWorkletに関連するAPIはSecure Contentでのみ動作します。つまりWorkletを使うページ、アプリはHTTPSが利用可能なサーバから配信される必要がるということです。例外的にhttp://localhostでも利用可能です。

Worklet上で動作している処理によって裏付けられたカスタムNodeを定義する為にAudioWorkletNodeをサブクラス化できることにも注目してください。

// この'processor.js'ファイルはGlobalScopeの`AudioWorklet.addModule()`
// のCallによってAudioWorkletGlobalScopeとして評価されます
class MyWorkletProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
  }

  process(inputs, outputs, parameters) {
    // ここに音声処理を書く
  }
}

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

registerProcessor()stringの名前でAudioWorkletGlobalScopeに登録、またClassとして定義されます。GlobalScopeでのコードの評価が完了すると、AudioWorklet.addModukle()からGlobalScopeでClassの定義が完了し、利用ができるというresolve(または何らかのエラー)がPromiseで返されます。

カスタム(独自の)のAudio Param

AudioNodeでのAudioParamのオートメションは便利な機能の1つです。AudioWorkletNodeでも利用することが可能で、自動的に制御できる公開されたパラメータを取得することが可能です。

Screen Shot 2017-12-23 at 2.28.17 PM.png

開発者が定義したAudioParamはAudioWorkletProcessorクラスでAudioParamDescriptorを設定することでその定義が可能です。

/* 例えば、"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|はWeb Audioエンジンで通常のAudioParamの操作で計算された
    // 128サンプルのFloat32Array。すべてのパラメータは0.707で(automation,
    // methods, setter)がデフォルトで設置されている。
    let myParamValues = parameters.myParam;
  }
}

AudioWorkletProcessor.process()

実際の音声処理はAudioWorkletProcessorprocess()コールバックで行われ、開発者がクラス定義内で実装する必要があります。WebAudioエンジンは、これを inputs および parameters を入力として outputs を取得するための関数として、等時間間隔で呼び出します。

/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
  // 複数のInput、Outputがあった場合、最初のInput、Outputを取得する
  let input = inputs[0];
  let output = outputs[0];

  // 複数のチャンネルがあった場合、最初のチャンネルを取得する.
  let inputChannel0 = input[0];
  let outputChannel0 = output[0];

  // パラメータを配列で取得
  let myParamValues = parameters.myParam;

  // シンプルな多重のGainの128サンプルを超えた処理
  // モノラルのみ対応
  for (let i = 0; i < inputChannel0.length; ++i) {
    outputChannel0[i] = inputChannel0[i] * myParamValues[i];
  }

  // 処理を続ける
  return true;
}

更に、process()の戻り値を使ってAudioWorkletNodeのライフタイムを制御することが可能なので、開発者はメモリの管理もすることが可能です。process()メソッドからfalseが返ってくると、その処理は非アクティブとしてマークされてWebAudioエンジンはそのメソッドをもう呼び出すことはありません。処理を有効な状態に保つにはtrueを返す必要があります。さもなければ、最終的にはAudioWorkletNodeAudioWorkletProcessorのペアはシステムのガベージコレクションによって回収されるでしょう。

MessagePortによる双方向のやりとり

AudioWorkletNodeはAudioParamには定義されていない値をNodeの外からコントロールさせたい場合がある。例えば、stringで定義された属性typeはFilterをコントロールするのに利用される。この目的場合、AudioWorkletNodeAudioWorkletProcessorは双方向でやり取りするためにMessagePortが用意されている。いかなる種類のカスタムな値でもこのChannelを使うことでやり取りすることが可能になっている。

Screen Shot 2017-12-23 at 2.56.55 PM.png

MessagePortAudioWorkletNodeAudioWorkletProcessor.portの属性からアクセスすることが可能。AudioWorkletNode側のport.postMessage()でメッセージを送り、AudioWorkletProcessor側はport.onmessageでハンドラを指定する。またその逆も同様に行うことも可能。

/* 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);

MessagePortはスレッドの境界を超えてデータを保存する、またWASM(Web Assembly)に乗り換えさせることも可能になっている。これによってAudioWorkletで実現できることの可能性を無限に広げています。

実装してみる:GainNode

上記を踏まえて、GaiNodeをAudioWorkletNodeAudioWorkletProcessorを実装してみる。

  • 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) {
    let input = inputs[0];
    let output = outputs[0];
    let gain = parameters.gain;
    for (let channel = 0; channel < input.length; ++channel) {
      let inputChannel = input[channel];
      let outputChannel = output[channel];
      for (let i = 0; i < inputChannel.length; ++i)
        outputChannel[i] = inputChannel[i] * gain[i];
    }

    return true;
  }
}

registerProcessor('gain-processor', GainProcessor);

これでAudioWorkletの基本的な部分はカバーできていると思います。ライブデモはChrome WebAudio team's GitHub repositoryにありますので参考にしてみてください。

実験的利用とOrigin Trials

AudioWorkletは Chrome 64にて実験的フラグ付き(behind the experimental flag)で利用可能です。AudioWorkletを有効にするには2つ方法があります。

Chrome起動時にオプションをつける

--enable-blink-features=Worklet,AudioWorklet

Chrome起動後にGUIから有効にする

アドレスバーに以下を入力し、有効(enable)にすることで利用可能になります。

chrome://flags/#enable-experimental-web-platform-features`

この方法を取った場合、Chromeが実験的に実装している機能すべてが有効になるので注意してください。

Origin Trials(開発者向け)

また、Origin TrialもすべてのPlatformで行っています。Origin Trialとはフィードバックを受けることを目的とし、特定のオリジン(scheme + domain + port)にのみ期限付きでその機能を提供する仕組みです。申込はこちらから行ってください。(申請フォーム)
詳しくはこちらの「Origin Trials」の項目を参照してください。

Thanks for Hongchan🤘

Thanks for Hongchan who have been making effort of implementing and of making big improvement of Web Audio in Chromium!!
Screen Shot 2017-12-23 at 12.51.38 PM.png