Safari 14.1 の AudioWorklet で音が出ない話


これは 2021/5/4 時点の Safari 14.1 (16611.1.21.161.3) で調査した結果に基づく記事です。Safari の AudioWorklet は公開されたばかりで、すぐにパッチが当たる可能性もあります。この記事の内容は風化が激しいかもしれませんのでご注意ください。

Safari 14.1 でついに AudioWorklet が利用可能になりました。早速使ってみたのですが、まったく音が出せずに1日、音が出てからも音が出る条件がよく分からず、合計3日ほどハマりました。せっかく人柱になったので、ハマりどころを書いてみようと思います。

なお、筆者は macOS のデスクトップ版 Safari 14.1 のほか、 iOS 14.5 の モバイル Safari でも本記事の内容を確認しています。

Chrome / Firefox では音が出るが、 Safari では音が出ないコード

はじめに Chrome 90, Firefox 88.0 では音が出るのに、Safari 14.1 では出ないコードを掲載します。画面に出た PLAY ボタンを押すと、440Hzのサイン波が鳴る、という単純なものです。

お手元で動作確認される場合は、以下の3ファイル (index.html index.js processor.js) を localhost:8000 あたりで配信して index.html を開いてみてください(localhost 以外に置く場合は https 通信ができるセキュアな場所に置かないと AudioWorklet は利用できません)。

index.html
<!DOCTYPE html>
<html>
<head>
  <script src="./index.js"></script>
  <style>
    button#play:after { content: 'PLAY'; }
    .playing button#play:after { content: 'NOW PLAYING (RELOAD BROWSER TO STOP)'; }
  </style>
</head>
<body><button id="play"></button></body>
</html>
index.js
let audioContext;
let node;
let isPlaying;

window.addEventListener('DOMContentLoaded', async () => {
  document.getElementById('play').addEventListener('click', async () => {
    if (isPlaying) return;
    isPlaying = true;
    audioContext = new (window.AudioContext || window.webkitAudioContext)();
    await audioContext.audioWorklet.addModule('./processor.js');
    node = new (window.AudioWorkletNode || window.webkitAudioWorkletNode)(audioContext, 'my-processor');
    node.connect(audioContext.destination);
    document.body.classList.add('playing');
  });
});
processor.js
export class MyAudioWorkletProcessor extends AudioWorkletProcessor {
  static get parameterDescriptors() { return [] };
  process(inputs, outputs, parameters) {
    const output = outputs[0];
    output.forEach((channel) => {
      for (let i = 0; i < channel.length; i++) {
        channel[i] = 0.5 * Math.sin(440 * 2.0 * Math.PI * (currentFrame + i) / sampleRate);
      }
    });
    return true;
  }
}
registerProcessor('my-processor', MyAudioWorkletProcessor);

Safari で音がでない原因

AudioWorkletProcessor を継承したクラスに export 修飾子があると Safari だけエラーになる

class 宣言にうっかり export を書いても、Chrome と FireFox は問題ありませんが、 Safari では動作しません。 audioWorklet.addModule 時点では何もエラーが出ませんが、AudioWorkletNode の生成時に No ScriptProcessor was registered with this name というエラーが出てしまいます。

export class MyAudioWorkletProcessor extends AudioWorkletProcessor {
^^^^^^

ちなみにtypescriptを使っていてモジュール形式を es系 (es2015とかesnext) にすると、コンパイル後のクラス宣言にexport指定がついてしまいますので、typescriptの場合は モジュール形式にes系以外を選ばざるを得ないということになります。

おそらく内部的には JavaScript のパース時にエラーが出ていると思うのですが、恐ろしいことに Safari 14.1 では AudioWorkletProcessor の js ファイル側で生じたエラーは、構文エラーであろうとランタイムエラーであろうと、一切 console.log に出力されないため、確認の術がありません。Worklet の動作状態も観察するすべがないので、現時点では Safari を使った AudioWorkletProcessor のデバッグは不可能と思って良いと思います。

なお、AudioWorkletProcessor のログが見えないという問題は、WebKit の Bugzilla に Unable to log to console from Audio Worklet processor が2020年の12月ごろに登録済みで、既知の問題ではあるようです。

クリックイベント内で、 AudioContext を生成すると、Safari だけ音がでない

Safari の WebAudio は FireFox や Chrome よりも制限が厳しく、ユーザー操作イベント(ボタンクリック等)の処理中に AudioContext と ScriptProcessorNode を生成しないと音が出ません。これはユーザー操作と関係なく、音を鳴らしてしまうサイトを作れないようにするための制限と思われますが、発音の開始だけではなく、AudioContext の生成までイベント期間中に実行しなければならなかったのは Safari / Webkit の特徴と思います。

なので、普通に考えると ScriptProcessorNode と同様に、AudioWorklet でも、ボタンクリックのイベント中に AudioContext の生成や audioWorklet.addModule を行うのが妥当と思われます。しかし、そのようにすると、AudioWorkletNode から音が出ません。

対処としては AudioContext の生成と addModule をクリックイベントの前に行っておく、になります。

window.addEventListener('DOMContentLoaded', async () => {
  // AudioContext の生成と、addModule はクリックイベントの外側で行っておく
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
  await audioContext.audioWorklet.addModule('./processor.js');
  document.getElementById('play').addEventListener('click', async () => {
    if (isPlaying) return;
    isPlaying = true;
    // ここで初期化すると、直後に生成した AudioWorkletNode から音がでない
    // audioContext = new (window.AudioContext || window.webkitAudioContext)();
    // await audioContext.audioWorklet.addModule('./processor.js');
    node = new (window.AudioWorkletNode || window.webkitAudioWorkletNode)(audioContext, 'my-processor');
    node.connect(audioContext.destination);
    document.body.classList.add('playing');
  });
});

ただこの対処では、元々 ScriptProcessorNode のように音が出ないノードもあったわけで、AudioWorkletNode 以外のノードと組み合わせたときに別の問題が発生するかもしれません(今回は、深く調査していません)。

AudioWorklet を使おうとすると ScriptProcessorNode が動かなくなる

これは AudioWorklet の問題ではありませんが、ScriptProcessorNode が動かなくなるという問題です。
AudioContext.audioWorklet.addModule で AudioWorkletProcessor をロードすると、それ以降に生成した ScriptProcessorNode の onaudioprocess が呼ばれなくなり、音が出ません。確認した範囲では、Chrome や FireFox では問題がなく、これも Safari のみ発生するようです。

対処としては、ScriptProcessorNode を使う場合は AudioWorklet を addModule しないことが簡単ですが、以下の方法も発見しました。

  • addModule 後に AudioWorkletNode を使って何か一度ダミーの音を再生してやる。

上記を行うと、それ以降、同じ AudioContext で生成した ScriptProcessorNode では onaudioprocess が正常に呼ばれるようになるようです。

おわりに

どうにか音は鳴ったものの、現時点の Safari で AudioWorklet を使うのはかなり大変だなという感じです。特に AudioWorkletProcessor の動作ログは一切見えず、デバッグは困難を極めます。 Safari の WebAudio 関係の更新は非常にゆっくりすすむので、この状態がいつまで続くかは分かりませんが、少なくともログが見えるようになるまでは、本番サイトで Safari の AudioWorklet を使うのは時期尚早かなという印象です。