本文へスキップ

サウンド ​

音がプレイヤーに届くまでに Earshot が何を決めているか、その決め方をどう変えるかを説明します。

リスナー ​

createListener(ctx, options) は任意の BaseAudioContext を受け取ります。音源だけなら OfflineAudioContext でも動きます。

オプション意味デフォルト
pos()ワールド座標でのリスナーの位置。必須で、更新のたびに読み取られます
tile1 タイルあたりのワールド単位。位置をこの値で割ります1
output音の出力先コンテキストの destination
worldwallsBetween と reverbAt。どちらも省略できます壁なし、リバーブなし
paramsパラメータの上書きDEFAULT_PARAMS
materials素材の追加、または同じ名前の置き換えDEFAULT_MATERIALS
reverbsリバーブの追加、または同じ名前の置き換えDEFAULT_REVERBS

ゲームループから ear.update() を呼びます。1 秒に 10〜20 回で十分です。変化は params.smoothing 秒かけてなめらかにつながります。

音源 ​

ts
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 を使います。

ts
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 つずつ問い合わせます。

ts
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 Hz0 dB
thin が 1 枚materials.thin2000 Hz-6 dB
solid が 1 枚materials.solid1200 Hz-12 dB
種類を問わず 2 枚以上params.multiWall700 Hz-18 dB

名前をつけて追加できます。

ts
ear.materials.glass = { hz: 5000, db: -3 };

materials にない名前の壁が 1 枚だけあると、update() の中で earshot: unknown material "name" を投げます。2 枚以上の壁は multiWall を使うので、名前を調べません。

これらのデフォルト値は、1 つのデモで耳を頼りに調整したものです。信号への影響を壁は音に何をするのかで測定しています。

リバーブ ​

world.reverbAt(p) は音源がいるリバーブの名前を返します。乾いた音にするなら何も返しません。座標はタイル単位で、基準になるのは音源の位置です。

ts
const ear = createListener(ctx, {
  pos: () => player,
  reverbs: { hall: { seconds: 2.5, wetDb: -10 } },
  world: { reverbAt: (p) => (p.y > 20 ? "cave" : "hall") },
});
リバーブ長さウェット
room0.4 秒-18 dB
cave1.8 秒-12 dB

コンボルバーは負荷が高いので、同じリバーブの音源は 1 つのコンボルバーを共有します。最初にそのリバーブが使われたときにコンボルバーを作るので、以後 seconds は変わりません。wetDb は更新のたびに反映されます。未知の名前は earshot: unknown reverb "name" を投げます。

パラメータ ​

パラメータ意味デフォルト
fullRadiusこのタイル数の内側で最大音量2
silentRadiusこのタイル数からちょうど無音12
panWidthパンの最大値(0〜1)0.6
panRangepanWidth に達する水平距離(タイル)8
smoothing変化の時定数(秒)0.08
multiWall壁が 2 枚以上のときの素材{ hz: 700, db: -18 }

調整 ​

ear.params、ear.materials、ear.reverbs はライブなコピーです。変更は次の更新で反映され、DEFAULT_ の定数には影響しません。

ts
ear.params.silentRadius = 20;
ear.params.multiWall.hz = 500;
ear.materials.thin = { hz: 3000, db: -4 };
ear.reverbs.cave.wetDb = -8;

Web Audio なしで使う ​

computeMix は判断の全体を数値で返します。オーディオコンテキストなしで、表示やテストに使えます。座標はどちらもタイル単位です。

ts
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