はじめに
以下の 3つの記事の続編になるような内容です。
- Babylon.js の「Playing Sounds and Music」のサンプルを 2つほど見てみる - Qiita
- 続・Babylon.js の「Playing Sounds and Music」のサンプルを見ていく - Qiita
- Babylon.js の「Playing Sounds and Music」のサンプルを見ていく: その3(Stereo pan・Spatial audio) - Qiita
以下のページの内容に関するものになります。
●Playing Sounds and Music | Babylon.js Documentation
https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic/
今回の内容
Babylon.js公式の「Playing Sounds and Music」のドキュメントを見てきた上記 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」を適用可能なものを組み込むことができるようです。
サンプルコード
ドキュメント本文中のサンプルコードとして、以下が書かれていました。
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 で音量を変える操作部分が提示されます。
GUI操作で音量を変えられる対象は、コードでは以下に該当するものになるようです。
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");
それらの個々の要素については、コードで以下の部分が関係しています。
(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 は音を周波数変換したものについて、周波数毎のレベルをリアルタイムに取得できる機能のようです。サウンドビジュアライザーを作るのに利用できたりしそうです。
サンプルコード
それについて、ドキュメントの文章内では以下のサンプルが書かれていました。
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;
}
});
以下の部分が
// 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
音楽が鳴り、それに連動して以下のような素敵なビジュアルが表示されるようになっていました。
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 で公開されていたもののようです
/*
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重に取得するのを避けることができるようでした。
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() で読みこんでおいた音のデータを、別途、利用するという流れになっているようです。
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 という部分は、以下のサンプルなどとともに、異なるコーデックでエンコードされた音データを使い分けるという内容が書かれていました。
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 という部分は、以下のフィードバックの送り先についての記載です。
おわりに
一部、駆け足になった部分もありますが、全4記事で Babylon.js の「Playing Sounds and Music」の公式ドキュメント・サンプルを見ていきました。
個々の内容について、細かな内容まで踏み込めていないところが多々あるので、別途、それらを見てみて活用していければとおもいm