JavaScriptでゲーム作り「19:BGMと効果音を再生する」

いよいよ当ゲームのメインとなるBGM、効果音再生の機能を実装していきます。

どのように音を鳴らせばいい?

(2019.6.24執筆)

次は会話の選択肢にとりかかるのと平行して、サウンドの再生にも着手しようとしてる段階です。たぶん私のゲームでは此処がメインです。

必要な機能は、
> ・メインとなるBGMを再生する。
> ・同時に効果音を複数鳴らせるようにする。
> ・音の出る方向(左右のPAN)、音の大きさを、それぞれの音声に対して制御できるようにする


ですがサウンドについて、どのように取り扱ってよいかが未だよく判ってない感じです。なので先生に質問を交えつつ、まずは音声が普通に再生されるまでを目標にやっていきたいと思います。
実装にあたり、以下のアドバイスを頂きました...

アドバイス、実装までの流れ


オーディオを鳴らすにはWeb Audio APIというのを使えば簡単です。
その名の通りオーディオを扱う機能で、多数のフィルタを接続してパンやエフェクトを実現できます。

まずはオーディオを読み込む必要があるので、AseetLoaderにloadAudioメソッドを追加します。

loadAudio(name, url) { // URLからファイルを読み込みarrayBuffer形式で取得する
    const promise = fetch(url).then((response) => response.arrayBuffer())
        .then((buffer) => {
            this._assets.set(name, buffer);
        });
    this._promises.push(promise);
}

これでオーディオの読み込み・取得ができるはずです。
動作確認はしていないのでエラーは適宜直してください。

あとはWebAudioでいろいろやるだけです。
参考: WebAudioAPIで遊べるようになった - Qiita

注意点としては、WebAudioは一度
ユーザアクション(クリック、キー入力)があったあとでないと音がミュートされる点です。
これは突然大音量を流す悪質な広告などを防ぐための仕組みです。

あとはオーディオを鳴らす管理をする場所を考えないといけないですね。
WebAudioを使う際に必要なaudioContextが6個までの制限があるので、
どこかで統一的に管理してないとすぐに溢れるかもです。
グローバルにAudioManagerとか作ってそっちに任せるといいかもです。


なるほど。。。
AssetLoaderクラスに記述を加える事で、画像だけでなく音声ファイルの読み込みもOKになるようです。
ただ音声の場合は読み込み形式が違うのか...「arrayBuffer」??なんじゃそりゃ(' '*);;


それとfetchについて調べてみましたが、 ファイルを読み込んでもらうリクエストのことで従来のXMLHttpRequest(XHR)に替わるものだとか。読み込み完了後に色々記載しやすい、従来よりもシンプルな2019年最新の描き方のようです。

JavaScriptのFetch APIを利用してリクエストを送信する
Promiseとasync/awaitでJavaScriptの非同期処理をシンプルに記述する


XMLHttpRequest(XHR)と両方試しましたが、私の環境でもfetchを使うほうがいい感じでした。
また容量の大きな音声ファイルは読み込みが長くなりがちので、再生のタイミングを測る非同期処理の確認をとっておきたい所です。

音声ファイルを用意してAseetLoaderに読み込ませてみる

とりあえず音声を読み込んでアセットに格納したものが、一体どういうものかコンソールに表示することにします。
用意したBGMは、当サイトお馴染みの「♪Chapter4-とある少女との遭遇」です。

AssetLoaderクラスに記述


class AssetLoader {//画像などを読み込むための箱、このクラスで画像ファイルを管理する
    constructor() {
        this._promises = [];
        this._assets = new Map();
    }
    addImage(name, url) {
        const img = new Image();
        img.src = url;

        const promise = new Promise((resolve, reject) =>//画像を完全に読み込んだ後に動作させるための記述
            img.addEventListener('load', (e) => {
                this._assets.set(name, img);
                resolve(img);
            }));
        this._promises.push(promise);
    }
    loadAudio(name, url) {// URLからファイルを読み込みarrayBuffer形式で取得する
        const promise = fetch(url)
            .then((response) => response.arrayBuffer())
            .then((buffer) => { 
                this._assets.set(name, buffer);
                console.log(buffer); //ここでファイルの状態を確認
            }
        );
        this._promises.push(promise);
    }

    loadAll() { return Promise.all(this._promises).then((p) => this._assets); }
    get(name) { return this._assets.get(name); }
}
教わったコード(urlを読み込んでarrayBufferに変換)のbufferの内容をconsole.log()を使って見てみます。

すると...
arrayBuffer

なにやらlengthという要素をもったArrayBufferオブジェクトが顔を出しました。
このlengthの値は、そのまま音声ファイルの容量2.2MBの数値と一致します。

...つまりこれは、2.2MB分の音声形式のファイルを、データ配列に変換したものと読み替えることができるでしょうか。。。
とんでもねえーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー長さの配列です。
2.2メガByteですからね。2,200,000Byteですからね(' '*)

読み込んだArrayBufferを再生してみよう

ココからは教わったURLを見ながら実践していきます。

参考: WebAudioAPIで遊べるようになった - Qiita


リンク先にて音声再生に必要なものが一覧で見れました。
手順を要約して載せてみます。

読み込んだ音声ファイルを再生するまでの手順

  1. const context = new window.AudioContext();...オーディオコンテキストを用意する
  2. buffer ... これはArrayBufferのこと? AudioContextでデコード?
  3. source ... AudioContextでデコードしたbufferを入力?
  4. source.connect(context.destination);... 音声を出力先に繋ぐ?
  5. source.start(0);...音声スタートメソッド。これで音声再生



URLを見た段階では、どうも暗号が多いようですね(' '*)...何言ってるか分からん
とりあえず全体の流れとしては、

  • 音声機能を使うためのAudioContextを宣言する!
  • AudioContext.sourceに、音声ファイルを入力する
  • AudioContext.sourceから、スピーカー?みたいな(context.destination);に繋ぐ
  • source.start(0);...playボタンを押す!


この4段階といったところでしょうか? bufferとデコードがどういうものか気になるが、とりあえずやってみます。

音声マネージャーをクラスを新規で作る

アドバイスどおり、まずAudioManagerクラスを用意。音声周りのメソッドは一括で管理するようにしますか。

//音声再生のためのスクリプト
//オーディオマネージャークラスに基本機能を設定
//クラス設定後、グローバルにaudioインスタンスを作成する

class AudioManager { //複数のオーディオコンテキストをまとめるクラス
    constructor(){
        this.context = []; //一度に使えるオーディオコンテキストは6個まで。
        for(let i=0; i<6; i++) { //用意した配列に6個のオーディオコンテキストを格納
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();
            this.context[i] = audioContext;
        }
    }

    playBGM(i, buffer){
        const source = this.context[i].createBufferSource(); //コンテキストにソース領域を作成
        console.log(buffer); // このバッファは何かを確認
        source.buffer = buffer; // ソース領域にバッファデータを入力
        source.connect(this.context[i].destination); // スピーカーへ出力!
        source.loop = true; // ソース要素で色々設定できそう。とりあえずループ再生をONに。
        source.start(0); // いざ再生!
    }
}

const audio = new AudioManager(); //グローバル領域に展開

主な機能を見ていきます。

オーディオコンテキストを宣言する箇所

    constructor(){
        this.context = []; //一度に使えるオーディオコンテキストは6個まで。
        for(let i=0; i<6; i++) { //用意した配列に6個のオーディオコンテキストを格納
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();
            this.context[i] = audioContext;
        }
    }
まずはオーディオコンテキストの宣言です。6個までという制約があるようなので、ここで6つ全部作っちゃいます。 配列を用意して[0]〜[5]までの番号を割り振って、audio.context[0]〜audio.context[5]という感じにそれぞれアクセスできる形にしました。

コンテキスト宣言時に、new (window.AudioContext || window.webkitAudioContext)();としてるのは、ブラウザによって対応するコンテキストが異なるためだとか。この辺は、リンク先のリンク先にある⇒Web Audio APIの基礎 - HTML5 Rocks で、詳細を確認することができます。

音声再生のメソッドをまとめた箇所

    playBGM(i, buffer){
        const source = this.context[i].createBufferSource(); //コンテキストにソース領域を作成
        console.log(buffer); // このバッファは何かを確認
        source.buffer = buffer; // ソース領域にバッファデータを入力
        source.connect(this.context[i].destination); // スピーカーへ出力!
        source.loop = true; // ソース要素で色々設定できそう。とりあえずループ再生をONに。
        source.start(0); // いざ再生!
    }

動くかどうか判らんけど、とりあえずAudioの仕様書を見ながら描いてみました。流れとしてはこんなもんだろ〜・・・試してみましょう(o _ o。)

確認、再生する?

audioBufferが見つからない

ああ、オーディオバッファが見つからないと出た。
やっぱりarrayBufferのままではなく、AudioContextを介してデコード必須なのですね。

ArrayBufferをデコードする手順を踏むと上手く行った


    playBGM(i, buffer){
        const source = this.context[i].createBufferSource(); //音声コンテキストのソース領域を宣言
        this.context[i].decodeAudioData(buffer , (buf) => { //ArrayBufferをAudioBufferに変換
            console.log(buf); // このバッファは何かを確認
            source.buffer = buf; // ソース領域にバッファデータを入力
            source.connect(this.context[i].destination); // スピーカーへ出力!
            source.loop = true; // ソース要素で色々設定できそう。とりあえずループ再生をONに。
            source.start(0); // いざ再生!

            console.log(assets.get('shoujoa')); //元のarrayBufferはどうなってるか?
        });
    }
audioContext.decodeAudioData(buffer , (buf) => { //〜バッファ変換後のメソッド }という描き方で、上手く動作しました。このAudioBufferを音源(素材)に用いることで、WebAudioAPIによる様々な音響操作が可能になるようです。

音声ファイルからarrayBufferを読み込んだ後、さらにAudioBuffer形式に変換するの二度手間だと感じるかもしれない。 でもこれは、音声ファイルの形式が.wavだったり.m4aだったり.oggだったり一昔前の.mp3だったりと様々なパターンがあることを想定したら、そこからArrayBufferの中間地点を挟むのも納得できるような気もします。。。

再生テスト

試しにassetsに音声ファイルを登録して、メインシーンのBGMとして再生してみる。

class MainScene extends Scene {
    constructor(actorsCanvas, windowsUICanvas) {
        super('メイン', 800, 600, 'bg0', actorsCanvas, windowsUICanvas);

        this.add(global.player);

        const Curve2 = new BezierCurveActor (30, 30,  30, 340,  300, 240, 40, 360);
        this.add(Curve2);
        const Curve1 = new BezierCurveActor (400, 110,  70, 20, 220, 160, 330, 190);
        this.add(Curve1);

        const npc1 = new NPC_girl1(150, 100,'すみれ秘書');
        this.add(npc1);
        audio.playBGM(0, assets.get('shoujoa')); //ここを追加
    }
}

assets.addImage('player', 'images/chara_player.png');
assets.addImage('npc1', 'images/chara_npc1.png');
assets.addImage('bg0', 'images/backimage0.png');
assets.addImage('up_npc_girl0', 'images/up_npc_girl0.png');
assets.addImage('up_npc_girl1', 'images/up_npc_girl1.png');
assets.loadAudio('shoujoa', 'images/audio/chapter4-shoujoa.m4a'); //ここを追加
assets.loadAll().then((a) => {
    const game = new RolePlayingGame();
    document.body.appendChild(game.actorsCanvas);
    document.body.appendChild(game.windowsUICanvas);
    document.body.appendChild(timeCounter);
    game.start();

    global.player = new Player(150, 200, 90); //globalオブジェクトに、player: new Player();のプロパティを追加。global.playerでアクセス可能に!
});

ネット上でも聴けるかな?


BGM再生
⇒ シーン中のBGM再生デモ



見事に再生されました。第一関門クリアです(。0 _ 0。)ノ

コンソール上で音声ファイルの状態を確認すると、AudioContextでデコードしたBufferがオーディオ形式に変換されていることが分かります。 そして元のアセット上に読み込んだArrayBufferはデータが空になってますね。。。次回assetsからデータを呼び出したとしても空です。2回目以降再生されません。

ということは、デコードしたAudioBufferはどこかに保存しておく必要があり、音声ファイルを使い回すには、ちょとやり方を考えないといけない...ように思えてきます。

最初からAudioBufferに変換した状態でassetsに登録すればよいのでは?

もっともシンプルな解決法。ArrayBufferをそのままAudioBufferに変換しちゃってassetsに登録したら、以降存分に使えるんじゃないかな?と思ってやってみた。

class AudioManager { //複数のオーディオコンテキストをまとめるクラス
    constructor(){
        this.context = []; //一度に使えるオーディオコンテキストは6個まで
        for(let i=0; i<6; i++) { //用意した配列に6個のオーディオコンテキストを格納
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();
            this.context[i] = audioContext;
        }
    }

    playBGM(i, buffer){
        const source = this.context[i].createBufferSource(); //音声コンテキストのソース領域を宣言
      //  this.context[i].decodeAudioData(buffer , (buf) => { //デコードをスキップ
            console.log(buffer); // このバッファは何かを確認
            source.buffer = buffer; // ソース領域にバッファデータを入力
            source.connect(this.context[i].destination); // スピーカーへ出力!
            source.loop = true; // ソース要素で色々設定できそう。とりあえずループ再生をONに。
            source.start(0); // いざ再生!

            console.log(assets.get('shoujoa')); //元のarrayBufferはどうなってるか?
      //  }); //デコードをスキップ
    }
}
const audio = new AudioManager();



class AssetLoader {//画像などを読み込むための箱、このクラスで画像ファイルを管理する
    constructor() {
        this._promises = [];
        this._assets = new Map();
    }
    addImage(name, url) {
        const img = new Image();
        img.src = url;

        const promise = new Promise((resolve, reject) =>//画像を完全に読み込んだ後に動作させるための記述
            img.addEventListener('load', (e) => {
                this._assets.set(name, img);
                resolve(img);
            }));
        this._promises.push(promise);
    }
    loadAudio(name, url, i = 5) {// URLからファイルを読み込みarrayBuffer形式で取得し、audioBuffer形式に変換)
        const promise = fetch(url, {credentials: 'same-origin'})
            .then((response) => response.arrayBuffer())
            .then((buffer) => {console.log(buffer); //ここでファイルの状態を確認
            audio.context[i].decodeAudioData(buffer, (buf) => this._assets.set(name, buf));
        });
        this._promises.push(promise);
    }

    loadAll() { return Promise.all(this._promises).then((p) => this._assets); }
    get(name) { return this._assets.get(name); }
}

const assets = new AssetLoader();//画像ファイル管理棚のaseetsを作成...asetts.get('名前')で取り出せる

assetLoaderクラスのloadAudio()メソッドに、audio.context[i].decodeAudioData(buffer, (buf) => this._assets.set(name, buf));...デコードまでして、audioBuffer形式でassetsに格納するようにしました。

これで問題なく動くだろ〜と思って、インタネット上で確認して見るでしょ。。。

BGM再生
⇒ 修正後のBGM再生デモ



どうでしょう?

タップのタイミングを遅らせれば再生されますが、早く起動させるとAudioBufferの処理途中でpromiseが終了することもあり、するとassetsから呼び出したのがundefined(処理未完だから?)となって再生されない。
そもそもdecodeAudioDataはバッファをデコードしながらaudioを再生できるような仕組みだろうから、前のデモのように組み合わせて使ったほうが良いのだろうか。。。なやましいい問題です。


あとネット上で確認すると、ArrayBufferの初回読み込みもすっごーーーく遅いですね(私の毎秒17kb/sのネット回線速度での結果です)手軽なはずの2曲分でロード時間こんなん。

音声ファイルロード時間

読み込み2.73分...アホや。ゲームにならん。

どうするのが一番いいのかよーわからn?

音声ファイルを、このタイミングで、的確に、再生!するには、どっちにしろ全部読み込み完了して置いたほうが絶対良いんですよね。

しかしネット上で遊んでもらうなら、ファイルの読み込みとレスポンスの速さも考えなければならない。 オープニングの音声をさ、短くして、最初のテロップで読み込んで。。。再生?? って感覚でしょうか。。。

それか割りきってゲームはローカル専用と考える?
ローカルにしても、Chromeはローカルのリクエストをエラーで返すやつだからな。
この時点でFireFoxしか使い物にならんし、するとユーザーの普及率から考えて微妙。


非常に悩ましい。
最初にうまく流すことを考えないと、オーディオで色々やるのはかなり骨が折れそうです。

Web Audio API ストリーミング再生?

それで、この検索キーワードで調べてみたら興味深い記事を発見しました(' '*)。。

WebAudioAPIでmp3をすべてダウンロードするのを待たずに再生を開始したい


この記事(2015年当時の状況)から、AudioBufferについての記事を引用します。

AudioBufferについての引用記事


AudioBufferはせいぜい1分程度の比較的短めの音声のために設計されていて、
たとえばCD一枚74分のデータ全体のような大きなデータを読み込むような目的には向いていません。

このインターフェースはメモリ上に保持されるオーディオのリソース(ワンショットの音やその他の短いオーディオクリップ)を表します。
そのフォーマットは-1 ~ +1の名目上の範囲を持つ非インターリーブのIEEE 32ビット・リニアPCMです。
1つ以上のチャンネルを持つ事ができます
 典型的には、そのPCMデータは適度に(通常1分以内程度に)短いと見込まれます。


音楽の1曲分のような長時間の音はaudio要素とMediaElementAudioSourceNodeによるストリーミングを使うべきです。

http://g200kg.github.io/web-audio-api-ja/#AudioBuffer


これらのオブジェクトは短い、一般的には45秒未満の、断片的な音声を保持するために設計されています。
それよりも長い音声は、MediaElementAudioSourceNodeのオブジェクトが適しています。

https://developer.mozilla.org/ja/docs/Web/API/AudioBuffer


というわけで、MediaElementAudioSourceNodeを使うのがいいと思います。
MediaElementAudioSourceNodeなら簡単にストリーミング再生で部分的に読み込みながら再生することが可能です。
万が一XHRで読み込まなければならない理由があるのなら、MediaElementAudioSourceNodeを使うことはできません。
その場合は何らかの方法でオーディオファイルを分割してAudioBufferに読み込むことになるでしょう。


HTMLMediaElementのcurrentTimeプロパティで現在の再生位置を設定できます。
その後でplayを呼び出せば途中から再生できます。
たとえば、ローカルのオーディオファイルを開いて、ローパスフィルタをかけて途中から再生するコードは次のようになります。


var input = document.querySelector("input");
var audio = document.querySelector("audio");

var context = new AudioContext();
var source = context.createMediaElementSource(audio);
var filter = context.createBiquadFilter();

source.connect(filter);
filter.connect(context.destination);

input.addEventListener("change", function(){
    audio.src = URL.createObjectURL(input.files[0]);
    audio.currentTime = 50; 
    audio.play();
});

<input type="file"></input>
<audio controls />


ふむふむ、要約するとBGMのような長い音声ファイルの再生には、audio要素とMediaElementAudioSourceNodeを使ったほうが良いとあります。自動的にストリーミング再生になってくれるみたいですね。



お馴染みのコレがaudio要素。インターフェースを非表示にもできます。
ゲーム上でも、このaudio要素にBGMとなるファイルをURLで紐付けて再生させればいいということです。

audio要素を使ってBGMを再生させてみよう

audio要素を通すと、音声をロードしながら再生させていくストリーミング形式となる。
オンライン上での反応の早さが特徴です。キャッシュやロード時間などを考えなくていい。BGMを簡単に素早く再生できるようになります。

audio要素の導入例


class AudioManager { //複数のオーディオを管理するクラス
    constructor(){
        this.context = new (window.AudioContext || window.webkitAudioContext)();

        this.BGM1 = document.createElement('audio');
        this.BGM1.loop = true; // ループ再生をONに。
        const source1 = this.context[0].createMediaElementSource(this.BGM1);
        this.volume1 = this.context[0].createGain();
        source1.connect(this.volume1);
        this.volume1.connect(this.context[0].destination);
    }
}
例えば、Audioマネージャーを単純な形にしてみました。

        this.context = new (window.AudioContext || window.webkitAudioContext)();
        this.BGM1 = document.createElement('audio');
オーディオコンテキストを一つ作り、ソースの受け入れ口であるaudio要素も一つ用意します。

        this.BGM1 = document.createElement('audio');
        this.BGM1.loop = true; // ループ再生をONに。
        const source1 = this.context[0].createMediaElementSource(this.BGM1);
        this.volume1 = this.context[0].createGain();
        source1.connect(this.volume1);
        this.volume1.connect(this.context[0].destination);
そのaudio要素から音声ソースを取得し、ボリュームコントローラーに繋いでからスピーカーに接続。
ここまでで、BGM再生の下準備が整っています。


あとはシーンの開幕時にaudioタグへ音声ファイルの参照を送ってあげて、再生ボタンを押してあげるだけです。

class TitleScene extends Scene {
    constructor() {
        super('タイトル', screenCanvasWidth, screenCanvasHeight, null);
        const title = new TextLabel(100, 200, 'RPG製作',"white", 25);
        this.open(title);

        audio.BGM1.src = "images/audio/musicbox2.ogg";
        audio.BGM1.play();
    }

class MainScene extends Scene {
    constructor() {
        super('メイン', 800, 600, 'bg0');
        this.add(global.player);
        const npc1 = new NPC_girl1(150, 100,'すみれ秘書');
        this.add(npc1);

        audio.BGM1.src = "images/audio/chapter4-shoujoa.m4a";
        audio.BGM1.play();
    }
}

⇒ audio要素を使ったBGM再生デモを見る


ロード時間短縮(というか読み込みと再生の同時進行でタイムラグを無くす)のメリットはあるのが良い、反面読み込み状況で再生のタイミングがずれるので、音ゲーとか、音声作品メインのジャンルには不向きです。
しかしたいていのゲームBGMはaudio要素を使えば全然OKだと思います。

BGMのロード時間でうんうん唸るよりも、此処はaudioに任せて、他のマップやストーリーやゲーム性に力を注いだほうが良いでしょう。

効果音SEの再生はAudioBufferに取り込んで使おう

一方で何度も繰り返し鳴らす効果音は、再生の度にsrcを切り替えるのではなく、予めFetchリクエストでAudioBufferに取り込んだほうが望ましい。 効果音はそもそも軽いからコスト気にならないし、足跡とか話声の効果音とか微妙な音程、音量を修正するにも、元データをAudioBufferでキャッシュさせたほうが扱いやすくなります。

効果音ファイルをロードする


class AssetLoader {//画像などを読み込むための箱、このクラスで画像ファイルを管理する
    constructor() {
        this._promises = [];
        this._assets = new Map();
    }
    addImage(name, url) {
        const img = new Image();
        img.src = url;

        const promise = new Promise((resolve, reject) =>//画像を完全に読み込んだ後に動作させるための記述
            img.addEventListener('load', (e) => {
                this._assets.set(name, img);
                resolve(img);
            }));
        this._promises.push(promise);
    }
    loadAudio(name, url, i = 5) {// URLからファイルを読み込みarrayBuffer形式で取得し、audioBuffer形式に変換)
        const promise = fetch(url, {credentials: 'same-origin'})
            .then((response) => response.arrayBuffer())
            .then((buffer) => {
            audio.context[i].decodeAudioData(buffer, (buf) => this._assets.set(name, buf));
        });
        this._promises.push(promise);
    }

    loadAll() { return Promise.all(this._promises).then((p) => this._assets); }
    get(name) { return this._assets.get(name); }
}

const assets = new AssetLoader();//画像ファイル管理棚のaseetsを作成...asetts.get('名前')で取り出せる

assets.loadAudio('se_pon', 'assets/audio/se/pon.m4a');
予めアセットにaudioBufferを読み込むメソッドを用意しておいて、実際にファイルを読み込ませてます。

アセットのフォルダ分け

なおアセット素材の場所は、今後を考えてフォルダ分けしました。
音声と画像に分け、音声はBGMかSEかでまた分ける。
何処に何が在るか、整理がつく。大切。

効果音を再生する

後は、その音を鳴らしたい場所でaudioの効果音再生メソッドを実行するだけ。

class AudioManager { //複数のオーディオコンテキストをまとめるクラス
    constructor(){
        this.context = []; //一度に使えるオーディオコンテキストは6個まで
        for(let i=0; i<6; i++) { //用意した配列に6個のオーディオコンテキストを格納
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();
            this.context[i] = audioContext;
        }

        this.BGM1 = document.createElement('audio');
        const source1 = this.context[0].createMediaElementSource(this.BGM1);
        this.volume1 = this.context[0].createGain();
        source1.connect(this.volume1);
        this.volume1.connect(this.context[0].destination);
    }

    sound(buffer, i=1){
        const source = this.context[i].createBufferSource(); //音声コンテキストのソース領域を宣言
        source.buffer = buffer; // ソース領域にバッファデータを入力
        source.connect(this.context[i].destination); // スピーカーへ出力!
        source.start(0); // いざ再生!
    }
}
const audio = new AudioManager();
オーディオマネージャーを用意しておいて、実行箇所にメソッドを書き込む

class PlayerAction extends Actor {
    constructor(x, y, player) {
        const hitArea = new Circle(0, 0, 1);
        super(x, y, hitArea, ['playerAction', 'event']);
        this.player = player;
        audio.sound(assets.get('se_pon')); //この一行
    }

    render(context, scroller) {//オーバーライドしたrenderメソッドで、アクション範囲を可視化してみる
        context.beginPath();
        context.fillColor("rgba(255,255,255,0.7)"); //半透明の白で塗りつぶす
        context.circle(this.x - scroller.x, this.y - scroller.y, this.hitArea.radius);
        context.fill();
    }
    update(sceneInfo, input, touch) { 
        if(this.hitArea.radius === 1) { this.hitArea.radius = 8;}
        else if(this.hitArea.radius === 8) { this.hitArea.radius = 16;}
        else if(this.hitArea.radius === 16) {this.release();}
    }
}
今回は、アクション発生時に鳴らすようにしてみました。
        audio.sound(assets.get('se_pon')); //この一行
この一行を追加しただけ。簡単です。。
後は、色々音量調整とかピッチとか、Panとか、必要に応じてメソッドに手を加えれば良いんじゃないかと思います。

ひとまず、BGMと効果音を再生できました。


BGMと効果音のデモを見る


【目次】
  1. JavaScriptでゲーム作り「1:基礎編」
  2. JavaScriptでゲーム作り「2:プレイヤーの歩かせ方」
  3. JavaScriptでゲーム作り「3:プレイヤーの向きに応じてアクションを起こす」
  4. JavaScriptでゲーム作り「4:NPCと会話する」
  5. JavaScriptでゲーム作り「5:当たり判定を最適化する」
  6. JavaScriptでゲーム作り「6:線分の当たり判定を実装する」
  7. JavaScriptでゲーム作り「7:フィールドの背景スクロール」
  8. JavaScriptでゲーム作り「8:プログラムの最適化、高速化、コード整理」
  9. JavaScriptでゲーム作り「9:タッチ・マウスイベント入力」
  10. JavaScriptでゲーム作り「10:法線ベクトルと衝突時のバウンス判定」
  11. JavaScriptでゲーム作り「11:多角形を使った魔法陣を実装する」
  12. JavaScriptでゲーム作り「12:キャッシュの扱い方と計算処理の高速化」
  13. JavaScriptでゲーム作り「13:メッセージウィンドウを実装する」
  14. JavaScriptでゲーム作り「14:イベントの仕組みを理解する」
  15. JavaScriptでゲーム作り「15:メッセージテキスト表示の機能追加」
  16. JavaScriptでゲーム作り「16:会話時の立ち絵&名前表示」


古都さんのフレームワークを元にほぼ最初から作ってます。
たぶん順番に見ないとちんぷんかんぷん(' '*)...

すぺしゃるさんくす

https://sbfl.net/
古都さん
JavaScriptで作る弾幕STGの基礎(フレームワーク)を使わせていただいてます。感謝!


プレイヤーキャラ
ぴぽやさんもありがとう、キャラチップをお借りしています。