プログラミング
エーテライト・ラジオの作成
Creating the Aetheryte Radio (haz.ee)
要約
著者は、FFXIVのエーテライトのアンビエントサウンドをYouTube動画でループ再生する際の不満から、ゲームアセットを抽出しWeb Audio APIを用いて独自のサウンドプレイヤーを作成しました。これにより、シームレスでランダムなアンビエントサウンド体験を実現しました。
全文翻訳
自宅で作業しているとき、アンビエントサウンドを聴くのが好きです。特に気に入っているのは、「エーテライトASMR」という、ゲーム内のエーテライトクリスタルの録音です。過去6年間、同じYouTube動画を繰り返し聴いてきました。このアプローチにはいくつかの問題があります。YouTubeプレイヤーの「ループ」機能は常にうまくいくわけではありません。20回か200回ループしてから失敗し、別の動画が自動再生されることがあります。これは非常に不快です。動画の音声は約7分間の録音ですが、ループはシームレスではありません(音楽プレイヤーの内部をギャップレス再生に対応させるために、私はかなりの時間を費やしました)。ループが途切れても気にならないほど長いのですが、シームレスであればなお良いでしょう。もちろん、ゲームを開いたままにしておくことはできません。リソースを大量に消費しますし、他のプレイヤー(FFXIVはMMORPGです)が近くに現れてノイズを発生させ、効果を台無しにする可能性があります。これらは私にとって決定的な問題ではありませんでした。6年間、いくつかの困難に直面してから、ついに問題を解決しようと決意するというのは、かなり長い時間です。それにもかかわらず、ある日、私はついに座って、「犠牲なしで最高のアンビエント体験を得るにはどうすればよいか?」と考えました。最初の答えは、ゲームアセットを取得する必要があるということでした。ゲームがそのノイズを再生するために使用していたものを、私は抽出する必要がありました。それらを手に入れれば、好きなように再作成したり、操作したりできます。
ソースアセットの取得
FFXIVは(少なくとも私はそう思いますが)それほど厳重に保護されているゲームではありません。もし「ffxiv data explorer」でウェブ検索すれば、ゲームのインストールディレクトリからアセットパッケージをロードし、圧縮されたアセットをエクスポートできる古いJavaアプリが見つかります。どのフォークを使ったかは覚えていませんが、ファイルと英語のファイル名をマッピングするハッシュテーブルが付いていました。私を笑うでしょうが(正直、それでも構いません)、私は4000個のオーディオアセットを4時間かけて調べ、オーディオアセットを探しました。文字通り、4000個の.wavファイルをすべてCtrl+Aで選択し、大きなmpvプレイリスト内で左右の矢印キーを押して前後に移動し、エーテライトのノイズを探しました。面白いのは、それを見つけられずに、午前3時に寝なければならなかったことです。しかし、私はエンジニアですよね?より賢く働き、よりハードにではなく(なぜ、手間のかかる作業をする前にこれをしなかったのか?わかりません。)音を取得するもっと良い方法はありましたか?午前3時の徹底的な捜索で見落としましたか?(ヒント:見落としました。)
調べてみると、ありました!FFXIVには広範なモッディングコミュニティとプラットフォームがあり、私はもうゲームをあまりプレイしませんが、人々が自分の体験をカスタマイズするためにどれだけの努力を注いでいるかを見るのは刺激的でした。xivlauncher、vfxeditor、soundfilter。xivlauncher(180日間のプレイ時間を持つ私のルームメイトが推奨しています)でFFXIVを起動すると、プラグインをロードできます。興味深いプラグインの1つがsoundfilterでした。名前は少し誤解を招くかもしれませんが、再生中のアセットの名前を表示する機能があります。これは非常に役立ちました。なぜなら、新しいエリアにテレポートして(エーテライトは基本的にウェイポイント/テレポートクリスタルです)、どの音が再生されているかを特定できたからです。すぐに、探していたアセットが "bgcommon/sound/fst/placednpc_ethelight_big_loop.scd/0" であることを発見しました。すぐにそのアセットをエクスポートし、作業を進めました。そのアセットに関する興味深い点は、末尾の /0 です。サウンド「アセット」には複数のサウンドが含まれている(ゲーム内では「トラック」と呼ばれていたと思います)と私は確信しています。0は単にアンビエントの「ハム」または低周波でした。アセットはここでプレビューできます。注意!これらは大音量です。完全性を保つために正規化していません。ハム、ウィア1、ウィア2。残念ながら、実際のアンビエントを取得するには、もう少し手間がかかります。Logic Proでこれらをまとめて、それで終わりにしたかもしれませんが、それでは面白みがありませんよね?それに、非常に長いトラックを作成しなければなりません。ループが繰り返されるタイミングを無意識に意識したくありませんでした。しかし、私はこの問題に対処するための適切なツールを持っていたので、あまり心配していませんでした。
Web Audio API
Web Audio APIに入りましょう。誰が(あるいは何が)最初にそれをやったのかはわかりませんが、AV処理はサンプルを生成、変換、または消費する「ノード」という考え方に収束する傾向があります。これは、コンピューターでのプロAVワークフローへの大規模な投資が行われる前の現状、つまりモジュラーシンセサイザーから来ていると私は推測します。
ハム
この特定のアセットについては、あまり分析しませんでした。ループはほとんど完璧なループであると仮定しました(そしてそれは正しかったです)。友人が、Web Audio APIで再生するときにクリックなしでループするように、サンプルを正確に取得するのを手伝ってくれましたが(「シンク」ノードにオフセットをリセットすることでサンプルの供給を継続するように指示できます)、そのアセットには他のポストプロセッシングはありませんでした。オーディオノードグラフは、この時点では非常にシンプルです。ソースノード(hum.wav)->ゲインノード(-25db)->出力ノード。ゲインノードは、ゲーム内で行われた音量調整に合わせるために存在します。しかし、グラフの興味深い(ただし相対的にのみ)部分は、ウィアノードです。
ウィア
FFXIVは、ウィアをランダムな間隔、ピッチ、ゲイン(音量)で再生します。これらはすべて、アセットが繰り返されていることを検知しにくくする、生命の錯覚(?)を作り出すために追加されています。幸いなことに、FFXIVは各ランダムパラメータの最小値と最大値を提供する値も提供していたため、JavaScriptに簡単に変換できました。
// ランダムなウィアを選択
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;
手動でグラフを構築したことがあるなら、通常は次のようになります。ノードを割り当て、メタデータを設定し、接続します。ハムを設定するために、私は次のことを行います。
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);
ノードをループを設定する前に接続しても問題ありません。なぜなら、startを呼び出すまでノードはサンプルを生成しないからです。もう一つの儀式はウィアのループです。これは実際には従来のループ制御フローを使用したループではありません。ランダムな「スリープ」を挟んだwhile(true)の代わりに、setTimeoutを使用してランダムな間隔で次のウィアをスケジュールします。
function chooseWhir() {
const whirSource = audioContext.createBufferSource();
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;
whirSource.buffer = whirs[whirIndex];
whirSource.playbackRate.value = whirPlaybackRate;
whirSource.connect(whirGainNode);
whirSource.start(0);
whirSource.onended = () => {
// 自分自身を切断し、別のウィアを再キューする
whirSource.disconnect(whirGainNode);
const nextWhirDelay = Math.floor(Math.random() * 2001);
setTimeout(chooseWhir, nextWhirDelay);
};
}
ここでも同じです。ソースノードを作成し、ランダムなアセット、再生速度(ピッチ)、ゲイン(音量)を選択し、ゲインノード(このプロセスでは一定に保たれ、値のみが変更されます)に接続します。重要なのは、ソースアセットが終了したときに、ループするのではなく、そのノードをグラフから削除し、新しいノードを追加することです(chooseWhirを再度呼び出すことによって)。次のウィアまでの予想される遅延があるため、ランダムな計算を行うことで追加される遅延を受け入れることに問題はありません。それはおそらくわずかでしょう。
結論
これは楽しいプロジェクトでした。最終結果のデモはここで確認できます。ブラウザでオーディオを再生することを台無しにしてしまったため、何かを聞く前にページと対話する必要があります。そのため、ウィアとハムの両方のミュートを解除し、「接続」をクリックして聴いてください。
バニラJS
エンジニアとして育つ中で、私はWebやWeb技術にはあまり関心がありませんでした。私はシステムアプリケーション、ゲーム、ネットワーキングに集中していました。いつからか、私は...