
2026/08/28 23:59
エーテリアルラジオの開発
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
この技術実装の主な目的は、Final Fantasy XIVにおいて現実的な風の音をシミュレーションし、オーディオアセットの反復や検出システムが反復パターンを検知しないようにすることです。本システムは、FFXIV が提供する範囲内で再生パラメータを動的にランダム化することでこの目標を達成します:whir の再生レートは 0.794 から 1.0 の間で変動し、ゲイン(音量)は 0.6 から 1.0 の間で調整されます。ピッチとタイミングにおける自然な変化を模倣するために、whir の再生は
setTimeout を用いてスケジュールされ、ランダムな遅延(0〜2000 ミリ秒、最大間隔は約 2001 ミリ秒)に基づいて計算されます。これにより、音イベントの発生から直前または直後に最大 2 秒の不確実なPauseが導入されます。重要なのは、システムがブラウザの機能に応じて特定のオーディオ形式をロードすることです:Safari 向けに .wav.opus.aac を使用し、他の環境では標準的な .wav.opus ファイルがロードされます。コードは Math.floor(Math.random() * whirs.length) を用いてアセット配列からの資産選択を明示的に管理しており、再生レートを明示的に 0.63 に設定する専用の「hum」オーディオソースも含まれています。whir ソースが終了した際、新しいものが chooseWhir 関数を通じて作成される前にノードがグラフから切断されます。全体のグラフセットアップフローには、ノードの割り当て、メタデータ(レートとゲイン)の設定、接続の確立、そして start(0) の呼び出しが含まれます。ランダム遅延の追加とノードの切り替えによりわずかなレイテンシが発生しますが、whir イベント間の本来長い間隔を考慮するとその影響は軽微であるとみなされています。結局のところ、この方法は一貫性があり生々しい聴覚体験の創出に成功しています。本文
FFXIV 風のオーディオ「生命感」を実現するランダム制御手法
FFXIV は、ウィアの再生間隔、ピッチ(音高)、および**ゲイン(音量)をランダムに制御することで、資産が単純に繰り返していることを検知しにくい「生命感」**のような錯覚を生み出しています。
FFXIV 側で公開されている各パラメータの最小・最大値に基づき、JavaScript でこれらの挙動を実装することができました。
パラメータ設定の実装例
以下のコードは、再生速度と音量をランダム範囲内に収めるロジックです。
const whirIndex = Math.floor(Math.random() * whirs.length); // 再生レート:0.794 〜 1.0 の範囲 const whirPlaybackRate = Math.random() * (1 - 0.794) + 0.794; // ゲイン(音量):0.6 〜 1.0 の範囲 whirGainNode.gain.value = Math.random() * (1 - 0.6) + 0.6;
手動でのオーディオグラフ構築手順
一度もオーディオグラフを手作業で作成したことがなければ、通常は以下の流れを意識します。
- ノードの割り当て
- メタデータの設定
- コネクション(接続)の確立
ヒューム(低周波音)の例では、以下の手順で設定を行います。
const humSource = audioContext.createBufferSource(); // サンプルロード(ブラウザごとの拡張子判定あり) humSource.buffer = await loadSample( audioContext, isSafari ? "assets/hum.wav.opus.aac" : "assets/hum.wav.opus", ); // 設定値の固定 humSource.playbackRate.value = 0.63; humSource.connect(humGainNode); humSource.loop = true; humSource.start(0);
注意点:
loop プロパティを設定する前にノードへの接続を行っていても問題ありません。なぜなら、このノードは start() を呼び出すまでサンプリングを生成しないためです。
ウィアのループ制御戦略
儀式のもう一つの重要な要素は「ウィアのループ」の実装方法です。従来の単純なループ制御フローではなく、以下の手法を採用します。
ループの回避while(true)- 無限ループの中にランダムなスリープ時間を挟む方式は採用しません。
の活用setTimeout- 次のウィアの再生をランダムな間隔でスケジュールします。
実装コード(リフレッシュ方式)
function chooseWhir() { const whirSource = audioContext.createBufferSource(); // 1. ランダムなアセット選択とパラメータ設定 const whirIndex = Math.floor(Math.random() * whirs.length); const whirPlaybackRate = Math.random() * (1 - 0.794) + 0.794; whirGainNode.gain.value = Math.random() * (1 - 0.6) + 0.6; // 2. ノードの初期化と接続 whirSource.buffer = whirs[whirIndex]; whirSource.playbackRate.value = whirPlaybackRate; whirSource.connect(whirGainNode); whirSource.start(0); // 3. エンド時の処理(ループではなく置換) whirSource.onended = () => { whirSource.disconnect(whirGainNode); // 次の再生までのランダムな遅延(ms) const nextWhirDelay = Math.floor(Math.random() * 2001); setTimeout(chooseWhir, nextWhirDelay); }; }
この実装の重要なポイント
-
ソースアセットの置換
- ソースアセットが終了した際にループさせるのではなく、そのノードをグラフから削除して新しいものに置き換えます(再度
を呼び出す)。chooseWhir() - ゲインノード自体は不変であり、値のみを更新します。
- ソースアセットが終了した際にループさせるのではなく、そのノードをグラフから削除して新しいものに置き換えます(再度
-
レイテンシの許容性
- 次のウィアの再生には期待される遅延時間があります。
- ランダム計算によって生じるわずかなレイテンシを加えても差し支えないため、無視できるレベルのものとして扱えます。