Babylon.js の「Playing Sounds and Music」の公式ドキュメント・サンプルを見ていく： その4

## はじめに

以下の 3つの記事の続編になるような内容です。

- [Babylon.js の「Playing Sounds and Music」のサンプルを 2つほど見てみる - Qiita](https://qiita.com/youtoy/items/4f66e9a5f53d9af322ca)
- [続・Babylon.js の「Playing Sounds and Music」のサンプルを見ていく - Qiita](https://qiita.com/youtoy/items/c1ae0d53217f0d3e4e43)
- [Babylon.js の「Playing Sounds and Music」のサンプルを見ていく： その3（Stereo pan・Spatial audio） - Qiita](https://qiita.com/youtoy/items/808171d799cb2a250571)


以下のページの内容に関するものになります。

●Playing Sounds and Music | Babylon.js Documentation
　https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic/


### 今回の内容

[Babylon.js公式の「Playing Sounds and Music」のドキュメント](https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic/)を見てきた上記 2つの記事で、まだとりあげられてないものとして、以下の内容がありました。


- Audio buses
   - Main audio buses
   - Intermediate audio buses
- Analyzer
- Sound buffers
- Using browser-specific audio codecs
- Browser autoplay considerations
   - Playing sounds after the audio engine is unlocked
   - Unmute button
- Feature requests and bug fixes

今回、その続きの内容をいくつか見ていこうと思います。



## 内容を見ていく

### Audio buses

Audio buses を見ていきます。Audio buses は、複数のサウンドをまとめることができる仕組みになるようです。

また、Main audio bus が最終出力直前の出口になり、これはオーディオエンジンにより自動で生成されるようです。それと、前回の記事で扱った「Stereo pan・Spatial audio」は適用できないようです。
その最終出力前に、Intermediate audio bus として、「Stereo pan・Spatial audio」を適用可能なものを組み込むことができるようです。


#### サンプルコード

ドキュメント本文中のサンプルコードとして、以下が書かれていました。

```javascript
const bus = await BABYLON.CreateAudioBusAsync("bus",
    { spatialEnabled: true }
);

bus.spatial.attach(mesh);

const bounce = await BABYLON.CreateSoundAsync("bounce",
    "sounds/bounce.wav",
);

bounce.outBus = bus;

// Wait until audio engine is ready to play sounds.
await audioEngine.unlockAsync();

bounce.play({ loop: true });
```

シンプルに Intermediate audio bus を使った例になるようです。


そして、プレイグラウンド上のサンプルで以下がありました。

●AudioEngineV2 | Babylon.js Playground
　https://playground.babylonjs.com/#1BZK59#16

サンプルを実行すると、複数の音が鳴り続ける状態になり、画面右上には GUI で音量を変える操作部分が提示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/1a6dda60-adee-4a43-a2aa-094ab424b398.png)

GUI操作で音量を変えられる対象は、コードでは以下に該当するものになるようです。

```javascript
        volumeGui.add(cannonBlast, "volume", 0, 1).name("Cannon");
        volumeGui.add(gunshotsBus, "volume", 0, 1).name("Gunshots");
        volumeGui.add(weaponsBus, "volume", 0, 1).name("All Weapons");
        volumeGui.add(music, "volume", 0, 1).name("Music");
        volumeGui.add(audioEngine, "volume", 0, 1).name("Master");
```

それらの個々の要素については、コードで以下の部分が関係しています。

```javascript
    (async () => {
        // Create the audio engine and sounds
        const audioEngine = await BABYLON.CreateAudioEngineAsync();
        const cannonBlast = await audioEngine.createSoundAsync("cannonBlast", "https://assets.babylonjs.com/sound/cannonBlast.mp3");
        const gunshot1 = await audioEngine.createSoundAsync("gunshot1", "https://playground.babylonjs.com/sounds/gunshot.wav", { stereoPan: -0.333 });
        const gunshot2 = await audioEngine.createSoundAsync("gunshot2", gunshot1.buffer, { stereoPan: 0.333 });
        const music = await audioEngine.createStreamingSoundAsync("music", "https://assets.babylonjs.com/sound/pirateFun.mp3");

        // Create a bus for gunshot sounds
        const gunshotsBus = await audioEngine.createBusAsync("gunshotBus", { volume: 0.5 });
        gunshot1.outBus = gunshotsBus;
        gunshot2.outBus = gunshotsBus;

        // Create a bus for cannon and gunshot sounds
        // Note that the gunshots bus is chained to the weapons bus
        const weaponsBus = await audioEngine.createBusAsync("weaponsBus", { volume: 0.5 });
        cannonBlast.outBus = weaponsBus;
        gunshotsBus.outBus = weaponsBus;

        // Lower the volume of the gunshots bus to make both gunshots quieter but leave the cannon blast as is
        gunshotsBus.volume = 0.75;

        // Lower the volume of the weapons bus to make both cannon and gunshot sounds quieter
        weaponsBus.volume = 0.5;

        // Wait for the audio engine to unlock
        await audioEngine.unlockAsync();

        // Play sounds with some randomness thrown in to make it more interesting
        cannonBlast.play();
        cannonBlast.onEndedObservable.add(() => {
            cannonBlast.stereo.pan = -1 + Math.random() * 2;
            cannonBlast.play({ waitTime: Math.random() * 5 });
        });

        gunshot1.play({ waitTime: 1 });
        gunshot1.onEndedObservable.add(() => {
            gunshot1.play({ playbackRate: 1 + Math.random() * 0.5, waitTime: Math.random() * 0.5 });
        });

        gunshot2.play({ playbackRate: 1.25, waitTime: 1 });
        gunshot2.onEndedObservable.add(() => {
            gunshot2.play({ playbackRate: 1 + Math.random() * 0.5, waitTime: Math.random() * 0.5 });
        });

        music.play({ loop: true });
```

例えば、元の個々の音源を以下のようにまとめているようです。

- gunshotsBus は gunshot1.outBus と gunshot2.outBus の 2つをまとめたもの
- weaponsBus は cannonBlast.outBus と上記の gunshotsBus.outBus をまとめたもの
- music はループ再生

GUI上から音量を変えられる対象は、上記の「cannonBlast」「gunshotsBus」「weaponsBus」「music」と、全ての音をまとめた「Main audio bus（GUI上だと Master）」になっているようです。



### Analyzer

Analyzer は音を周波数変換したものについて、周波数毎のレベルをリアルタイムに取得できる機能のようです。サウンドビジュアライザーを作るのに利用できたりしそうです。

#### サンプルコード

それについて、ドキュメントの文章内では以下のサンプルが書かれていました。

```javascript
const bounce = await BABYLON.CreateSoundAsync("bounce",
    "sounds/bounce.wav",
    { analyzerEnabled: true }
);

// Wait until audio engine is ready to play sounds.
await audioEngine.unlockAsync();

bounce.play({ loop: true });

// Get the audio analyzer frequency data on every frame.
scene.onBeforeRenderObservable.add(() => {
    const frequencies = bounce.analyzer.getByteFrequencyData();
    for (let i = 0; i < 16; i++) {
        columns[i].value = frequencies[i] / 255;
    }
});
```

以下の部分が

```javascript
// Get the audio analyzer frequency data on every frame.
scene.onBeforeRenderObservable.add(() => {
    const frequencies = bounce.analyzer.getByteFrequencyData();
    for (let i = 0; i < 16; i++) {
        columns[i].value = frequencies[i] / 255;
    }
});
```


また、プレイグラウンド上のサンプルでは以下のサウンドビジュアライザーの例がありました。

●AudioEngineV2 | Babylon.js Playground
　https://playground.babylonjs.com/#1BZK59#17

音楽が鳴り、それに連動して以下のような素敵なビジュアルが表示されるようになっていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/e2542798-d31d-4653-ab78-dbcfbee2824c.png)


```javascript
var createScene = function () {
    const scene = new BABYLON.Scene(engine);
    const camera = new BABYLON.Camera("camera", BABYLON.Vector3.Zero(), scene);

    const fftSize = 1024;
    const fftBinCount = fftSize / 2;
    const fftBins = new Float32Array(fftBinCount);

    let time = 0;

    let audioAnalyzer = null;

    (async () => {
        // Create the audio engine
        const audioEngine = await BABYLON.CreateAudioEngineAsync();

        /*
        "title": "Oceanica",
        "artist": "Avaren",
        "artist-url": "https://freemusicarchive.org/music/Avaren/",
        "download-url": "https://freemusicarchive.org/music/Avaren/Avaren/Oceanica/",
        "license": "CC BY 4.0",
        "license-url": "https://creativecommons.org/licenses/by/4.0/",
        */
        const music = await audioEngine.createStreamingSoundAsync("music", "https://amf-ms.github.io/AudioAssets/cc-music/electronic/Avaren--Oceanica.mp3", {
            analyzerFFTSize: fftSize,
        });

        // Wait for the audio engine to unlock
        await audioEngine.unlockAsync();

        music.play({ loop: true });

        audioAnalyzer = music.analyzer;
    })();

    /*
    Shader based on "3D Audio Visualizer" by @kishimisu - 2022 (https://www.shadertoy.com/view/dtl3Dr)
    This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en).
    */
    BABYLON.Effect.ShadersStore["customFragmentShader"] = `
        #ifdef GL_ES
            precision highp float;
        #endif

        // Samplers
        varying vec2 vUV;
        uniform sampler2D textureSampler;

        // Parameters
        uniform vec2 screenSize;
        uniform sampler2D fftBins;
        uniform float time;

        #define st(t1, t2, v1, v2) mix(v1, v2, smoothstep(t1, t2, time))
        #define light(d, att) 1. / (1. + pow(abs(d * att), 1.3))

        /* Audio-related functions */
        #define getLevel(x) texelFetch(fftBins, ivec2(int(${fftBinCount}. * x), 0), 0).r / 205.
        #define logX(x, a, c) (1./(exp(-a * (x - c)) + 1.))

        float logAmp(float amp){
            float c = st(0., 10., .8, 1.), a = 20.;
            return (logX(amp, a, c) - logX(0.0, a, c)) / (logX(1.0, a, c) - logX(0.0, a, c));
        }

        float getPitch(float freq, float octave){
            freq = pow(2., freq)   * 261.;
            freq = pow(2., octave) * freq / 12000.;
            return logAmp(getLevel(freq));
        }

        float getVol(float samples) {
            float avg = 0.;
            for (float i = 0.; i < samples; i++) avg += getLevel(i/samples);
            return avg / samples;
        }

        float sdBox( vec3 p, vec3 b ) {
            vec3 q = abs(p) - b;
            return length(max(q, 0.0)) + min(max(q.x,max(q.y, q.z)), 0.0);
        }

        float hash13(vec3 p3) {
            p3  = fract(p3 * .1031);
            p3 += dot(p3, p3.zyx + 31.32);
            return fract((p3.x + p3.y) * p3.z);
        }

        void main(void)
        {
            vec2 uv = (gl_FragCoord.xy - screenSize) / screenSize.y - vec2(screenSize.x / screenSize.y, 1.);
            vec3 col = vec3(0.);
            float vol = getVol(8.);

            for (float i = 0., t = 0.; i < 30.; i++) {
                vec3 p  = t * normalize(vec3(uv, 1.));

                vec3 id = floor(abs(p));
                vec3 q  = fract(p) - .5;

                float boxRep = sdBox(q, vec3(.3));
                float boxCtn = sdBox(p, vec3(7.5, 6.5, 16.5));

                float dst = max(boxRep, abs(boxCtn) - vol * .2);
                float freq = smoothstep(16., 0., id.z) * 3. + hash13(id) * 1.5;

                col += vec3(.8, .6, 1) * (cos(id * .4 + vec3(0, 1, 2) + time) + 2.)
                        * light(dst, 10. - vol)
                        * getPitch(freq, 1.);

                t += dst;
            }

            gl_FragColor = vec4(col, 1.0);
        }
    `;

    const texture = BABYLON.RawTexture.CreateRTexture(fftBins, fftBinCount, 1, scene, false, false, BABYLON.Constants.TEXTURE_NEAREST_NEAREST, BABYLON.Constants.TEXTURETYPE_FLOAT);

    const postProcess = new BABYLON.PostProcess("Music viz shader", "custom", ["screenSize", "time"], ["fftBins"], 0.25, camera);
    postProcess.onApplyObservable.add((effect) => {
        texture.update(fftBins);
        effect.setFloat2("screenSize", postProcess.width, postProcess.height);
        effect.setTexture("fftBins", texture);
        effect.setFloat("time", time);
    });

    let previewBin = 0;
    scene.onBeforeRenderObservable.add(() => {
        // If the audio analyzer is enabled, drive the frequency-based audio visualization shader with it.
        if (audioAnalyzer) {
            const analyzerData = audioAnalyzer.getByteFrequencyData();
            for (let i = 0, j = 0; i < fftBinCount; i++) {
                fftBins[i] = analyzerData[i];
            }

            time += scene.getEngine().getDeltaTime() / 1000;
        } else {
            fftBins[previewBin] = 0;
            previewBin++;
            previewBin %= 128;
            fftBins[previewBin] = 125;
        }
    });

    return scene;
};
```


詳細な説明は省略しますが、ビジュアルを作っている部分では以下のカスタムシェーダー（フラグメントシェーダー）が使われているようです。
※ 元の実装は、コメント部分にあるとおり [Shadertoy で公開されていたもの](https://www.shadertoy.com/view/dtl3Dr)のようです


```javascript

    /*
    Shader based on "3D Audio Visualizer" by @kishimisu - 2022 (https://www.shadertoy.com/view/dtl3Dr)
    This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en).
    */
    BABYLON.Effect.ShadersStore["customFragmentShader"] = `
        #ifdef GL_ES
            precision highp float;
        #endif

        // Samplers
        varying vec2 vUV;
        uniform sampler2D textureSampler;

        // Parameters
        uniform vec2 screenSize;
        uniform sampler2D fftBins;
        uniform float time;

        #define st(t1, t2, v1, v2) mix(v1, v2, smoothstep(t1, t2, time))
        #define light(d, att) 1. / (1. + pow(abs(d * att), 1.3))

        /* Audio-related functions */
        #define getLevel(x) texelFetch(fftBins, ivec2(int(${fftBinCount}. * x), 0), 0).r / 205.
        #define logX(x, a, c) (1./(exp(-a * (x - c)) + 1.))

        float logAmp(float amp){
            float c = st(0., 10., .8, 1.), a = 20.;
            return (logX(amp, a, c) - logX(0.0, a, c)) / (logX(1.0, a, c) - logX(0.0, a, c));
        }

        float getPitch(float freq, float octave){
            freq = pow(2., freq)   * 261.;
            freq = pow(2., octave) * freq / 12000.;
            return logAmp(getLevel(freq));
        }

        float getVol(float samples) {
            float avg = 0.;
            for (float i = 0.; i < samples; i++) avg += getLevel(i/samples);
            return avg / samples;
        }

        float sdBox( vec3 p, vec3 b ) {
            vec3 q = abs(p) - b;
            return length(max(q, 0.0)) + min(max(q.x,max(q.y, q.z)), 0.0);
        }

        float hash13(vec3 p3) {
            p3  = fract(p3 * .1031);
            p3 += dot(p3, p3.zyx + 31.32);
            return fract((p3.x + p3.y) * p3.z);
        }

        void main(void)
        {
            vec2 uv = (gl_FragCoord.xy - screenSize) / screenSize.y - vec2(screenSize.x / screenSize.y, 1.);
            vec3 col = vec3(0.);
            float vol = getVol(8.);

            for (float i = 0., t = 0.; i < 30.; i++) {
                vec3 p  = t * normalize(vec3(uv, 1.));

                vec3 id = floor(abs(p));
                vec3 q  = fract(p) - .5;

                float boxRep = sdBox(q, vec3(.3));
                float boxCtn = sdBox(p, vec3(7.5, 6.5, 16.5));

                float dst = max(boxRep, abs(boxCtn) - vol * .2);
                float freq = smoothstep(16., 0., id.z) * 3. + hash13(id) * 1.5;

                col += vec3(.8, .6, 1) * (cos(id * .4 + vec3(0, 1, 2) + time) + 2.)
                        * light(dst, 10. - vol)
                        * getPitch(freq, 1.);

                t += dst;
            }

            gl_FragColor = vec4(col, 1.0);
        }
    `;
```


### Sound buffers

Sound buffers は、ダウンロード済みの音データを再利用する仕組みのようです。

ドキュメントの文章内で以下のコードが書かれていますが、この中で bounce1 で読みこんだ音を bounce2 で `bounce1.buffer` として再利用し、同じ音のデータを 2重に取得するのを避けることができるようでした。


```javascript
const bounce1 = await BABYLON.CreateSoundAsync("bounce1",
    "sounds/bounce.wav"
);

const bounce2 = await BABYLON.CreateSoundAsync("bounce2",
    bounce1.buffer,
    { playbackRate: 2 }
);

// Wait until audio engine is ready to play sounds.
await audioEngine.unlockAsync();

bounce1.play();
bounce2.play();
```

また、別の読み込み方・使い方の例で、以下のサンプルも書かれています。
CreateSoundBufferAsync() で読みこんでおいた音のデータを、別途、利用するという流れになっているようです。

```javascript
const soundBuffer = await BABYLON.CreateSoundBufferAsync(
    "sounds/bounce.wav"
);

const bounce1 = await BABYLON.CreateSoundAsync("bounce1",
    soundBuffer
);

const bounce2 = await BABYLON.CreateSoundAsync("bounce2",
    soundBuffer
    { playbackRate: 2 }
);

// Wait until audio engine is ready to play sounds.
await audioEngine.unlockAsync();

bounce1.play();
bounce2.play();
```


### その他

ここからは、上記よりざっくりとした感じで見ていきます。


#### Using browser-specific audio codecs

Using browser-specific audio codecs という部分は、以下のサンプルなどとともに、異なるコーデックでエンコードされた音データを使い分けるという内容が書かれていました。

```javascript
const sound = BABYLON.CreateSoundAsync("sound", [
    "https://assets.babylonjs.com/sound/testing/ac3.ac3",
    "https://assets.babylonjs.com/sound/testing/ogg.ogg",
    "https://assets.babylonjs.com/sound/testing/mp3.mp3",
]);
```

ブラウザが上記のいずれかのコーデックに対応していれば、音の再生ができるという状態になります。


#### Browser autoplay considerations

Browser autoplay considerations は、初期状態でブラウザが音の再生をロックするという挙動に関するものです。

ユーザー操作によりブラウザが音の再生のロックを外すという挙動になりますが、そのアンロックの際に audioEngine.unlockAsync() が発火して、それを利用した処理を行えるものです。
また、ループ再生の音をロック時に再生する処理を実行した場合、アンロック時にその再生が行われるというような挙動をするようにもなっているようです。


#### Feature requests and bug fixes

Feature requests and bug fixes という部分は、以下のフィードバックの送り先についての記載です。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/cc8c7bab-4d39-4dce-bcde-5a6bfdd68736.png)


## おわりに

一部、駆け足になった部分もありますが、全4記事で Babylon.js の「Playing Sounds and Music」の公式ドキュメント・サンプルを見ていきました。

個々の内容について、細かな内容まで踏み込めていないところが多々あるので、別途、それらを見てみて活用していければとおもいｍ