[HTML5] 音声ファイルをJSで再生/停止する - howler.js編

以前取り上げた通り、音声ファイルを再生したり止めるだけであればタグとJavaScriptの組み合わせで簡単に実現できるのですが、一歩踏み込んだことをしようと思うと様々な罠が襲いかかりもだえ苦しむことになりますw WebAudioAPIを使えばあらゆることが自由自在に出来ますがこいつは学習コストが半端なく使い始めるにはそれ相応の覚悟がいります。

というわけで、若干凝ったことをしたいがWebAudioAPIは避けて通りたい場合には迷うことなく便利ライブラリを使うのがオススメ。今回はこの手の用途ではお約束な「howler.js」を利用します。

音声ファイルを再生する

実行例

以下から実際のサンプルをお試しいただけます。ボタンを押す度に再生と一時停止を繰り返します。 miku3.net

  • 音声ファイルの読み込みが完了するまでボタンは無効状態です。
  • ボタンを押す度に再生と一時停止を繰り返します。

音声ファイルは「魔王魂」さん、アイコンは「FontAwesome」からお借りしました。

ソース

HTML

具体的な処理は外部のJavaScriptファイルapp.jsへ逃がしています。またHTML内にaudioタグは一切存在せずすべてJSで処理しています。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Howler.js Sample1</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <style>
    #btn-play{ width:100px; height:40px; padding:10px; font-size:18px; }
  </style>
</head>
<body>

<h1>Howler.js Sample1</h1>

<!-- 再生ボタン (最初は利用不可) -->
<button id="btn-play" type="button" disabled><i class="fas fa-play"></i></button>

<!-- 必要なJSファイルを読み込む -->
<script src="howler.min.js"></script>
<script src="app.js"></script>

</body>
</html>

JavaScript

HTMLから呼び出しているapp.jsです。audioタグで書いた場合とそれほど変わらないことがわかりますね。

const btn = document.querySelector("#btn-play");   // <button>

//--------------------------
// インスタンスを生成
//--------------------------
const bgm1 = new Howl({
    // 読み込む音声ファイル
    src: [
      'op.ogg',
      'op.mp3'  // Ogg非対応の場合にMP3を採用する
    ],

    // 設定 (以下はデフォルト値です)
    preload: true,   // 事前ロード
    volume: 1.0,     // 音量(0.0〜1.0の範囲で指定)
    loop: false,     // ループ再生するか
    autoplay: false, // 自動再生するか

    // 読み込み完了時に実行する処理
    onload: ()=>{
      btn.removeAttribute("disabled");  // ボタンを使用可能にする
    }
});

/**
 * [event] ボタンクリック時に実行
 */
btn.addEventListener("click", ()=>{
  // true=>再生中, false=>停止
  if( bgm1.playing() ){
    btn.innerHTML = '<i class="fas fa-play"></i>';  // 「再生ボタン」に変更
    bgm1.pause();
  }
  else{
    btn.innerHTML = '<i class="fas fa-pause"></i>';  // 「停止ボタン」に変更
    bgm1.play();
  }
});

/**
 * [event] 再生終了時に実行
 */
bgm1.on("end", ()=>{
  bgm1.seek(0);  // 再生位置を先頭に移動
  btn.innerHTML = '<i class="fas fa-play"></i>';  // 「再生ボタン」に変更
});

基本的な利用方法

導入

CDN

CDNから配信されている物をそのまま読み込んであげるのがお手軽ですね。

cdnjs

jsDelivr

ダウンロード

もしくはGitHubからminifyされたソースがゲットできますので、これを保存してscriptタグで読み込んであげます。

$ wget https://raw.githubusercontent.com/goldfire/howler.js/master/dist/howler.min.js

落としたファイルをそのままHTMLから呼び出します。

<script src="howler.min.js"></script>

パッケージマネージャ

npmやyarnでも入ります。

$ npm install howler
$ yarn add howler

初期化

まず最初にインスタンスを作成します。基本的にはファイル名を指定するだけで動作します。

const sound = new Howl({
  src: ['sound.mp3']
});

同時に次のようなオプションも指定できます。カッコ内はデフォルト値です。

autoplay (false)
再生可能な状態になった時点で自動的に再生が始まります。audioタグでは現状できませんでしたがこちらは実現可能です。
loop (false)
最後まで再生すると一番最初に自動的に戻ります。ループ再生ですね。
mute (false)
ミュート(無音)になります。
preload (true)
再生されるかどうかに関わらずファイル全体をダウンロードします。
volume (1.0)
音量を0.0から1.0の範囲で指定します。
html5 (false)
通常はWebAudioAPIで動作しますが、これを強制的にaudioタグに切り替えることができます。例えば巨大なファイルが対象の場合にaudioタグでストリーミングによる再生を行う場合などに活躍します。

再生と停止

非常にシンプルでplay()メソッドを実行すれば問答無用で再生されます。

const sound = new Howl({
  src: ['sound.mp3']
});

sound.play();

一時的に再生を止めたい時はpause()メソッドを呼び出します。この後にplay()メソッドを実行すると停止したその地点から引き続き再生されます。

sound.pause();

完全に停止させる場合はstop()メソッドです。この後にplay()メソッドを実行すると自動的に曲の1番最初(0秒地点)から再生されます。

sound.stop();

複数のファイル形式

インスタンスを作成する際に複数のファイルを指定することが可能です。配列の順番通りに評価が行われ、最初にブラウザが対応するファイル形式に出会った場合にその楽曲が採用されます。

例えば以下のような記述をした場合、

const sound = new Howl({
  src: ['sound.ogg', 'sound.mp3']
});

以下のHTMLと等価と言えます。

<audio preload>
  <source src="sound.ogg" type="audio/ogg">
  <source src="sound.mp3" type="audio/mp3">
</audio>

状態を知りたい

ロード状態

state()メソッドで現在ファイルが再生可能な状態にあるかどうかを確認できます。ここで言う「ロード」とはダウンロードしつつメモリ上で再生可能な状態にする一連の動作を指します。

// ロード状態
switch( sound.state() ){
  case "unloaded":
    console.log("ローディング前");
    break;
  case "loading":
    console.log("ローディング中");
    break;
  case "loaded":
    console.log("ローディング完了");
    break;
}

再生状態

playing()メソッドからtrueが返されれば再生中、falseなら停止中また一時停止中となります。

// 再生状態
if( sound.playing() ){
  console.log("再生中");
}

ボリューム

ボリュームの調整はインスタンスを作成する時と、その後任意のタイミングでvolume()メソッドを実行することで行えます。音量はいずれも0.0〜1.0の範囲で指定する必要があります。volume()メソッドは引数を渡すとその音量に即時に設定され、何も引数を渡さないと現在の音量を返してくれます 。

const sound = new Howl({
  src: ['sound.mp3'],
  volume: 0.1  // 最初は小さめの音量にする
});

// 再生開始
sound.play();

// 音量を上げる(0.1→0.5)
sound.volume(0.5);

// 現在の音量に+0.5する
const vol = sound.volume();  //現在の音量を取得
sound.volume(vol + 0.5);

また一時的に無音状態にしたい場合にはmute()メソッドを利用します。trueを渡すとミュート開始、falseだとミュートが解除されます。注意点としてはミュート中も再生は継続されている点です。mute()に引数を渡さないと現在ミュート中かをBooleanで返してくれます。

// ミュート開始
sound.mute(true);

// ミュート中ならミュートを解除 (元の音量に戻る)
if( sound.mute() ){
  sound.mute(false);
}

フェードイン/フェードアウト

audioタグにフェードイン/フェードアウトの機能は用意されていなため自分でがんばる必要がありましたが、Howler.jsでは最初から用意されています。

let vol = sound.volume();  // 現在の音量を取得
sound.play();  // 再生を開始

// フェードアウト/インが完了した際に実行
sound.on("fade", ()=>{
  sound.stop();  // 再生を停止
});

// フェードアウトする
sound.fade(
  vol,  // 開始時のボリューム
  0.0,  // 終了時のボリューム
  3000  // 指定秒数の時間をかけて開始値から終了値に変化(ミリ秒)
);

fade()メソッドは1つでフェードインもフェードアウトにも対応しています。第1引数に開始時の音量、第2引数に終了時の音量、第3引数に音量の変化に必要な時間をミリ秒で指定します。なおフェードアウトした際にも再生は継続されていますので止めたい場合には上記のサンプルにあるようなイベント処理を予め定義する必要があります。

曲の長さを知りたい

曲の長さはduration()メソッドを実行するだけですが、ファイルの読み込みが完了するまでは常に0が返却される点に注意が必要です。

if( sound.state() === "loaded" ){
  sound.duration();
}

実際に実行すると小数点以下まで返却されますね。

51.645532879818596

現在の再生位置を知りたい

seek()メソッドに引数を何も指定せず実行すると、現在の再生位置を秒数で返してくれます。

sound.play();
console.log( sound.seek() );

実際に実行するとこちらも小数点以下まで取得できます。

21.495873015873045

再生位置を変更したい

seek()メソッドに移動先の秒数を指定します。ここでいう秒数は現在位置からではなく先頭からの秒数です。

sound.play();
sound.seek(30);  //30秒の位置に移動

停止状態でseek()をした後にplay()することも可能です。

再生スピードを変更したい

rate()メソッドで再生スピードを変更できます。0.5から4.0までの値を指定します。1が等速で値が大きくなればなるほど早くなります。デフォルトはもちろん1です。

sound.play();
sound.rate(2);  //2倍速で再生

引数に何も渡さないと現在のスピードを返却してくれます。

console.log( sound.rate() );

同じ音源を複数同時に再生したい

イメージとしては小学校の音楽の時間にやった「森のくまさん」の輪唱です。……え、分かりづらい?(;´∀`)

コードを見てみましょう。以下の通りplay()メソッドを実行する度に新たに再生が開始されます。第1段と第2段はそれぞれ並行して再生されます。今回のように意図した物なら良いのですが、誤って何重にも再生しないよう注意が必要ですね。

const sound = new Howl({
  src: ['kumasan.mp3']
});

// 第1段を再生開始
sound.play();

// 第2段を2秒後に再生開始 (第1段は継続して再生される)
setTimeout(()=>{
  sound.play();
}, 2000);

play()メソッドを実行すると、戻り値としてidが返却されます。これを利用して片方だけに処理を行うことも可能です。

const id1 = sound.play();
const id2 = sound.play();

sound.fade(1, 0, 1000, id1);  // 第1段はフェードアウト
sound.rate(1.5, id2);  // 第2段は再生スピードアップ

イベント処理

様々なイベントが用意されており、前述の通りインスタンス作成時はもちろんですが好きなタイミングで定義することが可能です。

インスタンス作成時

インスタンスを作成する際のオプションとしてイベント処理を定義する場合は次のように記述します。一度に一つだけでも複数同時にたくさんのイベントを設定することも可能です。

const sound = new Howl({
  src: ['sound.mp3'],

  // この後にイベント処理を定義
  onload: ()=>{
    // ロードが完了した場合の処理
  },
  onloaderror: (sound_id, error_message)=>{
    // ロード時にエラーが発生した場合の処理
  }
});

実際に設定できるイベントは以下の通りです。

イベント 説明 引数
onload ロードが正常に完了した直後 onload: function(){}
onloaderror ロードが正常に行えなかった onloaderror: function(sound_id, error_message){}
onplayerror 再生時にエラーが発生した onplayerror: function(sound_id, error_message){}
onplay 再生が開始された直後 onplay: function(sound_id)=>{}
onend 再生が終了した直後。ループ再生時は曲の終わりに毎回発生。 onend: function(sound_id)=>{}
onpause 再生が一時停止された直後 onpause: function(sound_id)=>{}
onstop 再生が停止された直後 onstop: function(sound_id)=>{}
onmute ミュートが開始または解除された直後 onmute: function(sound_id)=>{}
onvolume ボリュームが変更された直後 onvolume: function(sound_id)=>{}
onrate 再生スピードが変更された直後 onrate: function(sound_id)=>{}
onseek 再生位置が変更された(シークされた)直後 onseek: function(sound_id)=>{}
onfade フェードイン/アウトが完了した直後 onfade: function(sound_id)=>{}
onunlock 自動再生のロックが解除された直後(ボタンをクリックした場合など) onunlock: function(){}

on / once / off

自由なタイミングでイベント処理を定義できます。基本的にはon()で定義しますが、最初の1回だけ実行したい場合はonce()を利用します。また定義済みのイベントを削除するにはoff()メソッドを使います。

const sound = new Howl({
  src: ['sound.mp3']
});

//--------------------------------------
// イベント処理を定義
//--------------------------------------
sound.on("play", ()=>{
  // 再生を開始した際の処理
  // 再生される度に実行される
});

//--------------------------------------
// 1回だけ実行されるイベント処理を定義
//--------------------------------------
sound.once("volume", ()=>{
  // ボリュームを変更した際の処理
  // 実行された直後、自動的に削除される
});

//--------------------------------------
// イベント処理を削除
//--------------------------------------
// playイベントに設定されている処理をすべて削除
sound.off("play");

実際に指定するイベントは以下になります。内容としてはインスタンス作成時のオプションと同じです(単純にイベント名の先頭からonを削除した感じですね)

イベント 説明
load ロードが正常に完了した直後
loaderror ロードが正常に行えなかった
playerror 再生時にエラーが発生した
play 再生が開始された直後
end 再生が終了した直後。ループ再生時は曲の終わりに毎回発生。
pause 再生が一時停止された直後
stop 再生が停止された直後
mute ミュートが開始または解除された直後
volume ボリュームが変更された直後
rate 再生スピードが変更された直後
seek 再生位置が変更された(シークされた)直後
fade フェードイン/アウトが完了した直後
unlock 自動再生のロックが解除された直後(ボタンをクリックした場合など)

その他

なぜ自動再生ができるの?

最初触ったときにautplayの動作確認していて意味がわからなすぎて笑いましたw いやなんでクリックイベント書いてないのに音を鳴らせるの!?と混乱したものですw WebAudioAPI特有の機能でなにかあるのか…と思ったらドキュメントに記載がありました。

By default, audio on mobile browsers and Chrome/Safari is locked until a sound is played within a user interaction, and then it plays normally the rest of the page session (Apple documentation). The default behavior of howler.js is to attempt to silently unlock audio playback by playing an empty buffer on the first touchend event.

公式ドキュメント Mobile/Chrome Playbackの項目より

どうやらHowler.jsが裏で暗躍しているらしく、ページ中で最初にtouchendイベントが発生すると「無音」を自動的に再生することでブラウザのロックを解除しているとのこと。ブラウザでは一度でも音が再生されるとそれ以降はページの遷移や再読み込みされるまで自由に音が再生できることを利用しているようです。

この自動アンロックを無効にしたい場合は、以下のようにフラグを下ろしてやります。

Howler.autoUnlock = false;

timeupdateイベントはない

audioタグに存在していたtimeupdateイベントは残念ながらありません。例えば再生中にシークバー(プログレスバー)を動かしたいという場合にはsetInterval()requestAnimationFrame()などで定期的にチェックするしか無いようです

実際のコードは以下の記事を参照ください。 blog.katsubemakito.net

参考ページ