サウンド
音がプレイヤーに届くまでに Earshot が何を決めているか、その決め方をどう変えるかを説明します。
リスナー
createListener(ctx, options) は任意の BaseAudioContext を受け取ります。音源だけなら OfflineAudioContext でも動きます。
| オプション | 意味 | デフォルト |
|---|---|---|
pos() | ワールド座標でのリスナーの位置。必須で、更新のたびに読み取られます | |
tile | 1 タイルあたりのワールド単位。位置をこの値で割ります | 1 |
output | 音の出力先 | コンテキストの destination |
world | wallsBetween と reverbAt。どちらも省略できます | 壁なし、リバーブなし |
params | パラメータの上書き | DEFAULT_PARAMS |
materials | 素材の追加、または同じ名前の置き換え | DEFAULT_MATERIALS |
reverbs | リバーブの追加、または同じ名前の置き換え | DEFAULT_REVERBS |
ゲームループから ear.update() を呼びます。1 秒に 10〜20 回で十分です。変化は params.smoothing 秒かけてなめらかにつながります。
音源
const radio = ear.source(radioNode, { pos: () => radioPosition });
radio.state; // "audible" | "out-of-range" | "muted"
radio.mix; // distance、gain、cutoff、pan、walls。最初の更新までは null
radio.reverb; // 音源がいるリバーブ、または null
radio.muted = true; // 次の更新で反映されます
radio.remove(); // 切り離します。radioNode は引き続き使えます音源は最初の update() まで無音で、それまでの state は "audible" です。"out-of-range" はゲインがちょうど 0 であることを表します。
0.3 から、新しい音源はフェードインせず、最初からミックスどおりの状態で始まります。そのためドアの閉まる音はアタックを保ちます。0.3 より前の新しい音源は params.smoothing でフェードインしていました。
距離とパン
音量は fullRadius タイルの内側で最大、silentRadius タイルからちょうど 0 になり、その間は smoothstep の曲線に沿って変わります。distanceGain(d, full, silent) でこの曲線を求められます。
パンは水平位置だけを使います。panRange タイルで panWidth に達し、それ以上は変わりません。リスナーの真上や真下の音は中央から聞こえます。
壁
world.wallsBetween(a, b) は、2 点の間にある素材の名前をタイル座標で返します。入れ子の配列には wallsOnGrid を使います。
const ear = createListener(ctx, {
tile: 16,
pos: () => player,
world: { wallsBetween: (a, b) => wallsOnGrid(tiles, a, b) },
});どちらのヘルパーも、線分を 1 タイルあたり約 8 回調べます。同じ素材のタイルが隣り合っていれば、厚さによらず壁 1 枚と数えます。違う素材が接していれば 2 枚です。
wallsAlong
0.3 で追加しました。入れ子の配列ではないマップには wallsAlong を使います。タイルを 1 つずつ問い合わせます。
import { wallsAlong } from "@debuffjp/earshot";
const at = (x: number, y: number) =>
x >= 0 && y >= 0 && x < width && y < height && grid[y * width + x] === 1 ? "solid" : null;
const ear = createListener(ctx, {
tile: 16,
pos: () => player,
world: { wallsBetween: (a, b) => wallsAlong(at, a, b) },
});フラットな配列では、上の例のように範囲を自分で確認してください。確認しないと、端を越えた x が次の行を読んでしまいます。
素材
素材は、ローパスのカットオフ hz と音量の変化 db の組です。
| 間にある壁 | 使われる値 | カットオフ | 音量 |
|---|---|---|---|
| なし | なし | 20000 Hz | 0 dB |
thin が 1 枚 | materials.thin | 2000 Hz | -6 dB |
solid が 1 枚 | materials.solid | 1200 Hz | -12 dB |
| 種類を問わず 2 枚以上 | params.multiWall | 700 Hz | -18 dB |
名前をつけて追加できます。
ear.materials.glass = { hz: 5000, db: -3 };materials にない名前の壁が 1 枚だけあると、update() の中で earshot: unknown material "name" を投げます。2 枚以上の壁は multiWall を使うので、名前を調べません。
これらのデフォルト値は、1 つのデモで耳を頼りに調整したものです。信号への影響を壁は音に何をするのかで測定しています。
リバーブ
world.reverbAt(p) は音源がいるリバーブの名前を返します。乾いた音にするなら何も返しません。座標はタイル単位で、基準になるのは音源の位置です。
const ear = createListener(ctx, {
pos: () => player,
reverbs: { hall: { seconds: 2.5, wetDb: -10 } },
world: { reverbAt: (p) => (p.y > 20 ? "cave" : "hall") },
});| リバーブ | 長さ | ウェット |
|---|---|---|
room | 0.4 秒 | -18 dB |
cave | 1.8 秒 | -12 dB |
コンボルバーは負荷が高いので、同じリバーブの音源は 1 つのコンボルバーを共有します。最初にそのリバーブが使われたときにコンボルバーを作るので、以後 seconds は変わりません。wetDb は更新のたびに反映されます。未知の名前は earshot: unknown reverb "name" を投げます。
パラメータ
| パラメータ | 意味 | デフォルト |
|---|---|---|
fullRadius | このタイル数の内側で最大音量 | 2 |
silentRadius | このタイル数からちょうど無音 | 12 |
panWidth | パンの最大値(0〜1) | 0.6 |
panRange | panWidth に達する水平距離(タイル) | 8 |
smoothing | 変化の時定数(秒) | 0.08 |
multiWall | 壁が 2 枚以上のときの素材 | { hz: 700, db: -18 } |
調整
ear.params、ear.materials、ear.reverbs はライブなコピーです。変更は次の更新で反映され、DEFAULT_ の定数には影響しません。
ear.params.silentRadius = 20;
ear.params.multiWall.hz = 500;
ear.materials.thin = { hz: 3000, db: -4 };
ear.reverbs.cave.wetDb = -8;Web Audio なしで使う
computeMix は判断の全体を数値で返します。オーディオコンテキストなしで、表示やテストに使えます。座標はどちらもタイル単位です。
import { computeMix, DEFAULT_MATERIALS, DEFAULT_PARAMS } from "@debuffjp/earshot";
const mix = computeMix({ x: 0, y: 0 }, { x: 8, y: 0 }, ["solid"], DEFAULT_PARAMS, DEFAULT_MATERIALS);
mix.cutoff; // 1200
mix.pan; // 0.6