Facebookの「ピコッ」もWeb Audio APIならJSで実装できるぞ!

2016/11/26

James Wright

0

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Facebookやチャットアプリなどでおなじみの通知音。オーディオファイルを用意しなくてもいまならJavaScriptで鳴らせるんですね。パフォーマンスにも優れたWeb Audio APIの使い方をどうぞ。

Web Audio APIを使うと、プラグインに頼ることなくブラウザーでJavaScripで強力な音声処理が活用できます。このAPIはファイルから音声入力を読み込んでリアルタイムに処理するだけでなく、さまざまな波形を合成して音声を作り出せ、低速回線からアクセスされることが多いWebアプリケーションで役立ちます。

この記事では、便利なメソッドを取り上げてWeb Audio APIについて説明したあと、MP3ファイルを読み込んで再生する方法と、ユーザーインターフェイスに通知音を追加する方法を紹介します。

Web Audio APIでできること

Web Audio APIは本番環境では多様な使われ方をするものの、よく見かけるのは次のような場合です。

  • リアルタイムの音声処理(例: ユーザーの声にリバーブをかける)
  • ゲームの効果音生成
  • ユーザーインターフェイスに効果音を追加

記事では3番目の使用例を実装するコードを最終的に作成します。

対応ブラウザー

Web AudioはChrome、Edge、Firefox、Opera、Safariが対応していますが、執筆時点でSafariでは試験運用機能扱いなのでWebKitプレフィックスが必要です。

284-1

APIを使う

Web Audio APIのエントリーポイントはAudioContextと呼ばれるグローバルコンストラクターです。インスタンス化されると、AudioNodeインターフェイスを満たすさまざまなノードを定義するメソッドを使えます。ノードは次の3つのグループに分けられます。

  • Sourceノード:MP3入力、合成入力など
  • Effectノード:パンニングなど
  • DestinationノードAudioContextインスタンスによりdestinationとして取得できるもので、スピーカーやヘッドフォンなどユーザーのデフォルト出力機器を示す

これらのノードはconnectメソッドを使っていろいろな組み合わせで接続できます。Web Audio APIで作成したオーディオグラフの概念図を示します。

1474054295audio-graph

出典:MDN

次に、MP3ファイルをAudioBufferSourceNodeに変換しAudioContextインスタンスのdestinationノードから再生する例を示します。

音声を生成する

Web Audio APIは記録済みの音声をAudioBufferSourceNodeを使って再生できるだけでなく、OscillatorNodeというSourceノードを使えば、特定の波形に対する周波数を生成できます。具体的には次のようなことです。

概念的には、ヘルツという単位で計測される音の高低は周波数によって決まり、周波数が高いほど音も高くなります。OscillatorNodeにはカスタム波形だけでなく定義済みの波形も用意されており、使用する波形はインスタンスのtypeプロパティで指定します。

  • 'sine':口笛のような音
  • 'square':古いビデオゲーム機で音を合成するのによく使われたもの
  • 'triangle':sineとsquareを合わせたような音
  • 'sawtooth':強いブンブンという音

OscillatorNodeでリアルタイムに音を合成する例を次に示します。

WebサイトにおけるOscillatorNodeの活用方法

ファイルから読み込む代わりにコードで音を合成すると負荷を大きく下げられるので、2Gから4Gのすべての帯域でアプリケーションを同じように動作させることができます。特に新興市場において、モバイルデータの通信速度は保証できないので重要なことです。

2Gや3Gでモバイルからインターネットにアクセスしているユーザーの48%が2Gと3Gサービスの違いを認識できません
Ericsson, 変わりゆくモバイル帯域の状況

これを確かめるために、先ほどのOscillatorNodeを使ったコード例で鳴らした音を録音し、同等の音質が得られるビットレートでMP3ファイルにエンコードしたところ、できあがったファイルは10KBでした。ChromeのディベロッパーツールのNetwork Throttle機能で調べたところ、通常の2G接続では読み込みに2.15秒かかることが分かりました。この例では、プログラミングによるアプローチが明らかに勝ります。

OscillatorNodeで効果音を鳴らす

記事のはじめに効果音をユーザーインターフェイスに追加すると書いた通り、OscillatorNodeを例で使います。こちらのCodePenにアクセスするとメッセージアプリのUIが開きます。Sendボタンをクリックすると、メッセージが送信されたという通知が表示されます。このボイラープレートには注目すべき点が2つ、contextと呼ばれるAudioContextのインスタンスとplaySoundという関数があります。

始める前に、Forkボタンをクリックしてください。ボイラープレートをコピーして、変更を保存できます。

ChromeとFirefoxの両方でテスト済みなので、どちらかのブラウザーを使うことをすすめます。

playSound内で、次のようにoscillatorNodeという名前の変数を宣言し、context.createOscillator()の戻り値を代入してください。

const oscillatorNode = context.createOscillator();

続いて、使用するノードを設定します。次のように、typeプロパティを'sine'に、frequency.valueプロパティを150にします。

oscillatorNode.type = 'sine';
oscillatorNode.frequency.value = 150;

sine波をスピーカーやヘッドフォンから再生するためには、oscillatorNode.connectを呼び出し、context.destinationノードへの参照を渡します。最後に、oscillatorNode.startを呼び出し、続いて、context.currentTime +0.5というパラメーターを引数にscillatorNode.stopを呼び出します。これで、AudioContext'sのハードウェアスケジュールタイムスタンプで500ミリ秒が経過したあとに音が止まります。playSoundメソッドは次のようになります。

function playSound() {
  const oscillatorNode = context.createOscillator();

  oscillatorNode.type = 'sine';
  oscillatorNode.frequency.value = 150;

  oscillatorNode.connect(context.destination);
  oscillatorNode.start();
  oscillatorNode.stop(context.currentTime + 0.5);
}

変更を保存後にSendをクリックすると、設定した通知音が鳴ります。

GainNodeの紹介

言うまでもなく、このままではとてもけばけばしいので、Effectノードを使って心地良い音にします。GainNodeはEffectノードの一例です。Gainは入力シグナルの振幅を調整する手段で、例では音声入力の音量をコントロールするのに使います。

oscillatorNodeの宣言文の下に、次のようにgainNodeという名前で新たな変数を宣言し、context.createGain()の戻り値を代入します。

const gainNode = context.createGain();

oscillatorNodeを設定した下に、次のようにのgainNodegain.valueプロパティを0.3にするコードを挿入します。元の音量の30%で再生されます。

gainNode.gain.value = 0.3;

最後に、gainNodeoscillatorNode.connectに渡してGainNodeをオーディオグラフに追加し、gainNode.connectを呼び出してcontext.destinationを渡します。

function playSound() {
  const oscillatorNode = context.createOscillator();
  const gainNode = context.createGain();

  oscillatorNode.type = 'sine';
  oscillatorNode.frequency.value = 150;

  gainNode.gain.value = 0.3;

  oscillatorNode.connect(gainNode);
  gainNode.connect(context.destination);

  oscillatorNode.start();
  oscillatorNode.stop(context.currentTime + 0.5);
}

変更を保存したあとにSendをクリックすると、先ほどより静かな音で再生されます。

AudioParamでミキシング

OscillatorNodefrequencyGainNodegainを設定するために、valueというプロパティを設定したことに気が付いたでしょう。設定理由はgainfrequencyがともにAudioParamだからです。特定の値だけでなく、スケジュールに従って徐々に変化する値も設定できるインターフェイスです。AudioParamは多くのメソッドやプロパティがありますが、次の3つが重要です。

  • setValueAtTime:指定した時刻に値を即座に変更
  • linearRampToValueAtTime:指定した終了時刻まで値がだんだんと線形に変化するようにスケジューリング
  • exponentialRampToValueAtTime:だんだんと指数関数的に値が変化するようスケジューリング。変化量が一定割合の線形変化に対して、指数関数的な変化はスケジューラーが終了時刻に近づくほど変化量が増加もしくは減少する。こちらのほうが人間の耳には自然に聞こえるので好まれる

それではfrequencyとgainを両方とも指数関数的に変化させます。exponentialRampToValueAtTimeメソッドを使うためには、先行するイベントをスケジューリングする必要があります。oscillatorNode.frequency.valueの代わりにoscillatorNode.frequency.setValueAtTimeを呼び出します。先ほどと同じく周波数150ヘルツを渡して、2番目のパラメーターとしてcontext.currentTimeを渡して、すぐに始まるようにスケジューリングします。

oscillatorNode.frequency.setValueAtTime(150, context.currentTime);

setValueAtTimeを設定した下に、値を500ヘルツとしてoscillatorNode.frequency.exponentialRampToValueAtTimeを呼び出し、設定した開始時刻の0.5秒後にスケジューリングします。

oscillatorNode.frequency.exponentialRampToValueAtTime(500, context.currentTime + 0.5);

変更を保存したあとにSendをクリックすると、プレイバックが進むほど周波数が高くなります。

仕上げに、gainNode.gain.valueを設定する代わりに、OscillatorNodeの周波数と同様にgainNode.gain.setValueAtTimeを呼び出します。

gainNode.gain.setValueAtTime(0.3, context.currentTime);

音をフェードアウトするには、次のように0.5秒かけて指数関数的にgainを0.01に落とします。

function playSound() {
  const oscillatorNode = context.createOscillator();
  const gainNode = context.createGain();

  oscillatorNode.type = 'sine';
  oscillatorNode.frequency.setValueAtTime(150, context.currentTime);
  oscillatorNode.frequency.exponentialRampToValueAtTime(500, context.currentTime + 0.5);

  gainNode.gain.setValueAtTime(0.3, context.currentTime);
  gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 0.5);

  oscillatorNode.connect(gainNode);
  gainNode.connect(context.destination);

  oscillatorNode.start();
  oscillatorNode.stop(context.currentTime + 0.5);
}

変更を保存したあとにSendをクリックすると、通知音が時間とともに静かになります。これで人間らしい音になりました。

完成したデモはこちらです。

ソースノードを再生する

最後に、Web Audio APIになじみ始めたばかりの読者のために、取り違えやすい点について説明しておきます。終了後に音を再び鳴らすには、次のように書けばいいと思うかもしれません。

oscillatorNode.start();
oscillatorNode.stop(context.currentTime + 0.5);
oscillatorNode.start(context.currentTime + 0.5);
oscillatorNode.stop(context.currentTime + 1);

このコードを実行すると、cannot call start more than onceというメッセージとともにInvalidStateErrorが発生します。

AudioNodeは低コストで作成できるため、必要ならAudioNodeを再作成するのがWeb Audio APIのデザインが意図するところです。記事では、playSound関数を再度呼び出しています。

最後に

記事ではWeb Audio APIによる音声合成を紹介しました。多くの使用例のうちの1つを実際に試してみました。WebサイトやWebアプリにおける通知音の増加はUXに関する興味的な問いかけですが、答えが出るまでにまだ時間がかかりそうです。

Web Audio APIについてさらに知りたければ、私がSitePoint Premium向けに作成しているスクリーンキャスト5部シリーズ「An Introduction to the Web Audio API」も参照してください。1本目は試聴できます。

※本記事はMark BrownJosh Wedekindが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Web Audio API: Add Bandwidth-Friendly Sound to Your Web Page

[翻訳:内藤夏樹/編集:Livit

Copyright © 2016, James Wright All Rights Reserved.

James Wright

James Wright

高いスキルを持つように努力をして、ソフトウェア開発者でWebテクノロジーに情熱を傾けています。現在はNode.js、C#、Goに取り組んでいます。SkyやNET-A-PORTERに勤務した経験があります。

Loading...