JavaScript
game
MP3
WebAudioAPI

#作業してくる で実装した、mp3によるゲームBGMのようなループ再生について

More than 1 year has passed since last update.

はじめまして

プログラミングについてちゃんと発信するのが初めてで震えています。
ドキドキしてきました。よろしくおねがいします。
雰囲気でプログラミングをしているので、認識等間違っていたら優しく指摘してください。

・MIDI投稿サイト「Picotune
・開いておくだけで作業が捗るサイト「#作業してくる
という2つの音楽系のウェブアプリを開発・運営しています。
先週公開した #作業してくる では100名を超える作曲家の方に参加していただいて、ゲームBGMのような終わりなくループする曲を配信しています。
そのループの部分で四苦八苦したので、、、書きます。
今回の実装はWeb Audio API周辺でしています。
たぶんゲームのBGMとかで同じ手法が使えるとおもいます。
(MediaElementAudioSourceNodeではなくAudioBufferSourceNode利用での実装になるので負荷によるかも…?)

ちなみに、#作業してくる の企画関連のことはブログに書いているので、よければ覗いてみてください。
#作業してくる 企画メモ 発案・企画編
#作業してくる バズらせるための仕掛け(サイト公開編)

そもそも…

なぜmp3か

一般的にループ系のゲームBGMには、mp3ではなくOgg(Vorbis)やもっと別の方式(MIDI+SoundFontのような形)がとられる。
iOS11の対応状況はあまり追ってないが、2017年11月時点で、OggはSafariとiOSブラウザで再生できないため、一番多くの端末で対応していてファイルサイズが妥当なmp3を扱うことにした。

何が問題か

実際にwav等からmp3に変換して比べると一目瞭然なのだが、
mp3に変換する際に、前後に無音が挿入されたり、末尾が切れてしまったりする。(試した感じではコンバータによって結果はまちまち)
これはmp3の規格によるもので、どうにかしようと思っても基本的になんとかなるものではないらしい。
これにより、楽曲のつなぎ目が不自然になってしまう。

このような形のループ楽曲は、ファイルをイントロ部分とループ部分の2つに分け、
イントロ→ループ→ループ→ループ→…と再生していくが、
・イントロ部分からループ部分
・ループ時
の二つが繋ぎの部分となり、今回も同じループ方法をとっている。
曲としてのイントロの有無にかかわらずイントロ部分の用意は必須とした
(リバーブ等の問題もあるので…後述)

100名以上参加していることもあり、作曲家側になるべく手間をかけず、こちらも説明がすくなくて済むような、繋ぎ部分を自然にする手法を考えた。

実装

動くもの

#作業してくる 楽曲募集時の説明ページ
音楽ファイルの処理や、イントロ・リバーブについての説明も書いている。
先にザッと目を通してもらえると良いかも。

音楽ファイル側の準備

・ループする曲を作る
・イントロ部分とループ部分を切り分け
・それぞれの部分に、前後にある程度(0.5秒とか)の無音を挿入してmp3で書き出し
(無音を挿入する理由は、「そもそも…何が問題か」の部分で、末尾が切れてしまうのをカバーするため)

再生側の処理

ざっくりと

intro.mp3読み込み
→decodeAudioData()によるデコード
→AudioBuffer.getChannelData()で曲の波形データを取得
→波形データから前後の無音を切り取り
→intro.mp3 再生開始
→loop.mp3読み込み
→同様にデコード
→同様に無音切り取り
→BufferSource.loopStart/loopEndを設定
→BufferSource.loopをtrueにして再生
という流れで再生する。
loop.mp3周りの処理前にintro.mp3を再生開始すると、環境によっては曲が途切れる原因になりますが、上手く使えば再生までの時間短縮にもなる。

細かい実装

いまコードをみたら全部ここに載せるには結構アレなので、全体は
#作業してくる 楽曲募集時の説明ページ
を見てください。
以下、公開の実装のアレだった部分の解説
コードも上記のページからの抜粋

・デコード

Safari/iOSだとmp3のdecodeAudioData()が失敗するので、
decodeAudioData returning a null error
を参考に対応する。

・波形データの取得~無音の切り取り

// 波形データの用意
var channelLs = new Float32Array(audioBuffer.length);
channelLs.set(audioBuffer.getChannelData(0));
// 取得した波形データから、どこまでが無音かを探す
channelLs.some(function(val, idx){
    // ここのthresholdはユーザ定義
    // 切り取りの際、threshold以下は無音とみなすという意味合い
    if(Math.abs(val) > threshold){
        // サンプルレートからidxを秒数に直して設定
        source.loopStart = idx / context.sampleRate;
        return true;
    }
});

channelData周りは、
【Web Audio API入門】音楽プレイヤー作成 第3弾 から知った。

・そのほか

その他再生周りは Web Audio API のよくあるサンプルと変わらないはず。
本題ではないが、全体の音量調節用、個々の音量調節用(フェードアウトしたり…)など、gainNodeを複数作り、それを噛ませて音量を制御できると楽。

最後に

書いてみたら滅茶苦茶疲れました。
Web Audio API 的には、MIDI投稿サイト Picotune のほうがゴリゴリ書いているので、よかったら見てみてください(バグ放置中で初めてのページ読み込み時だと動かなかったような気が…)(ガバガバ)(何度かリロードしてみてください)

2017年も残すところ僅かとなりました が、どうぞよろしくおねがいします~