[Astro #27] Terminal Evolution — 音声同期プロトコルと UI の弾力性

[Astro #27] Terminal Evolution — 音声同期プロトコルと UI の弾力性

はじめに

昨日、トップページに表示するだけだったterminal情報を、実際に機能するように作りましたが、今日は音声再生コマンドを実装してみました。

曲に関しては、過去にゲーム制作でも利用させてもらってる「Yuli Audio Craft」さんから、一部曲をお借りしています。

音楽再生時に、コマンド情報に制作者名表示のクレジット表記を実装しています。

前回の記事:

音楽プレーヤーの使用方法

help コマンドにオーディオ関係のコマンドを追加しています。

[Astro #27] Terminal Evolution: 音楽プレーヤーの使用方法(help)

audio

と打つと、曲のリスト表示

[Astro #27] Terminal Evolution: 音楽プレーヤーの使用方法(audio)

play [id] で音楽が再生されます。

play 1
play 2

[Astro #27] Terminal Evolution: 音楽プレーヤーの使用方法(play)

停止は

stop

[Astro #27] Terminal Evolution: 音楽プレーヤーの使用方法(stop)

info [id] で音楽の詳細情報を表示。

info 1

[Astro #27] Terminal Evolution: 音楽プレーヤーの使用方法(info)

1. Audio Protocol: 音を「データ」として再定義する

これまで単なる「システムログの出力装置」だったターミナルを、Wiredと対話するための本格的な「OSのシェル」へと昇格させるため、音声再生を司る独立したプロトコル WiredAudio.tsx を新たに実装しました。

この実装の核となるのは、「音をただ鳴らす」のではなく、システム上の「データ」として扱い、UIと疎結合な状態で連携させるアーキテクチャです。

1-1. コンポーネントの完全分離とイベント駆動

ターミナルコンポーネント(WiredTerminal.tsx)内に Audio オブジェクトを直接持たせると、コードが肥大化し管理が複雑になります。そこで、音声のロードと再生に特化した非表示のコンポーネント WiredAudio.tsx を作成し、ページ全体に常駐させる設計にしました。

これらを繋ぐのは カスタムイベント(wired-audio) です。 ターミナル側でコマンドを検知すると、以下のようにイベントを window に向けて発火します。

// WiredTerminal.tsx 側の処理
window.dispatchEvent(new CustomEvent('wired-audio', { detail: { action: 'play', trackId: id } }));

この「イベント駆動(Event-Driven)」アプローチにより、ターミナルに限らず、3D空間上のアバター(Island)をクリックした時や、特定のページに遷移した時など、システムのあらゆる場所から BGM やシステム音を自由に制御できる拡張性の高い基盤が完成しました。

1-2. JSONマニフェストによる「データ」としての管理

再生する楽曲のリストは、JavaScript内にハードコードするのではなく、public/assets/audio/list.json という外部ファイルから動的に fetch する仕組みを採用しました。

{
  "id": 1,
  "name": "glitch piano",
  "filename": "glitch_piano.mp3",
  "artist": "Yuli Audio Craft",
  "description": "The song has a melancholic piano sound and confusing glitches..."
}

これにより、将来的に楽曲を追加・削除したい場合でも、ビルドをやり直す必要がなく、JSONファイルを書き換えるだけで済みます。

さらに、楽曲データに artist(世界観に合わせて actor と呼称するのも一興)や description(解説)といったメタデータを持たせることで、ターミナル上でリストを表示した際に「単なるファイル一覧」ではなく、「深層のWiredからアーカイブ情報を引き出している」 というハッカーライクな没入感を生み出しています。

1-3. CUI連動と「Autoplay制約」のスマートな突破

ターミナル上からは、以下のコマンドでオーディオを完全に制御できます。

  • audio (または ls audio):利用可能なリソースの一覧を表示
  • play [id]:指定したIDのデータを同期(再生)
  • stop:現在のストリームを破棄(停止)

Webブラウザには「ユーザーの明確な操作(クリック等)がない限り、音声の自動再生(Autoplay)をブロックする」という厳格な仕様があります。 しかし今回は、「ターミナルの入力欄(プロンプト)をクリックしてフォーカスする」、あるいは「SYNC_VOICE [OFF] ボタンを押して同期を開始する」 という行為自体がブラウザから「ユーザーインタラクション」として認識されるため、コマンド入力からの audio.play() がブロックされることなく、非常にシームレスに再生が開始されます。

制約を力技でねじ伏せるのではなく、CUIというUIの特性(必ずタイピングのためにクリックが発生する)を活かして自然にクリアできたのは、アーキテクチャ上の美しい副産物でした。

2. UI Elasticity: 観測窓を広げる

情報の密度が高まるにつれ、固定サイズのターミナルでは「Wired」を覗く窓として不十分になりました。そこで、ユーザーが自由に枠を広げ、観測範囲を拡張できるリサイズ機能を実装しました。

2-1. 弾力的な初期化:環境に合わせた「窓」の展開

ターミナルがマウントされた瞬間に window.innerWidth を判定し、デバイスの解像度に最適化された初期サイズを割り振るレスポンシブ設計を採用しています。

  • PC環境: 320x230 の広めなサイズで展開。Raymarching などのリッチな背景を邪魔せず、かつログの奔流を十分に観測できるバランスを維持しています。
  • モバイル環境: 160x90 のコンパクトなサイズに抑制。画面占有率を抑え、スマートフォンの限られた表示領域でも他のUI(時計やアバター)との共存を可能にしました。

この「マウント時の自動判定」により、どのデバイスからアクセスしても、システムがその環境を「認識」して最適にブートする演出を強化しています。

2-2. 物理的なリサイズ:深層へのダイブ

ターミナルの右上の角に配置された resize-handle を介して、直感的なサイズ変更を可能にしました。実装の肝は、React の useStatemousemove イベントを組み合わせた動的な座標計算です。

ターミナルが「左下固定(bottom: 40px, left: 5px)」であることを基準に、以下のロジックでサイズを算出しています。

// マウス位置からリアルタイムにサイズを計算
const newWidth = Math.max(200, e.clientX - 5);
const newHeight = Math.max(100, (window.innerHeight - 40) - e.clientY);

単に widthheight を変えるだけでなく、Math.max を用いて「最小サイズ」を担保することで、操作ミスによってターミナルが消失してしまう不具合を防止しています。

2-3. UXとしての「拡張」

技術的な側面以上に重要なのは、この「枠を広げる」という行為そのものが持つ意味です。

オーディオプロトコルのリストを詳細に確認したい時や、ブートシーケンスのログを遡りたい時、自らの手で「観測窓」を広げる操作は、単なるWebサイトの閲覧を超えた体験を与えます。

枠を押し広げる手応えとともに、情報の密度が変化していく。その感覚は、OSの深層、あるいは Wired の深淵へとより深くダイブしていくプロセスを視覚的・触覚的に助長する、実存的なインターフェースの鍵となっています。

3. The VOICEVOX Bridge: CORS の壁を越える

Webアプリケーション(Wired)から、ユーザーのローカル環境(127.0.0.1)で動作する VOICEVOX Engine へアクセスし、音声を生成させる。このクロスオリジン通信において、ブラウザの強力なセキュリティ機構である CORS (Cross-Origin Resource Sharing) ポリシーが最大の壁として立ちはだかりました。

3-1. ブラウザ設定依存からの脱却

当初、HTTPSでホストされたサイトからローカルのHTTPサーバー(VOICEVOX)を叩くため、ユーザーにブラウザ側の「安全でないコンテンツ」のブロックを解除させる方法を検討しました。

しかし、この方法は環境依存が強く、ユーザー体験としても不親切です。そこで視点を変え、「ブラウザの設定を弄るのではなく、リクエストを受け取る VOICEVOX 側の CORS ポリシーを全開放する」 というアプローチに切り替えました。

解決策:CORS ポリシーの全開放

VOICEVOX Engine は、起動オプションを渡すことで挙動を制御できます。

./run.exe --cors_policy_mode all

この --cors_policy_mode all オプションを付けてエンジンを起動することで、どのオリジンからのプリフライトリクエスト(OPTIONSメソッド)に対しても許可を返すようになり、ブラウザの Mixed Content 制約を突破して通信が確立されます。

3-2. 「不自由さ」を UX(儀式)に昇華する

しかし、一般のユーザーにコマンドプロンプトを開かせ、パスを指定してオプション付きで実行させるのはハードルが高すぎます。そこで、このプロセスを自動化する設定用バッチファイル(audio_sync.bat)を動的に生成し、ダウンロードさせるUIをターミナル内に構築しました。

@echo off
title PROTOCOL.LAIN // Audio Synchronizer
color 0a
echo [SYSTEM] Searching for VOICEVOX Engine...

cd /d "C:\Program Files\VOICEVOX\vv-engine"
start run.exe --cors_policy_mode all

ターミナルに表示された DOWNLOAD "audio_sync.bat" の赤いボタンをクリックし、手元のPCでバッチファイルを実行する。

この一見すると「不親切で不自由な手順」は、PROTOCOL.LAIN の世界観においては単なるセットアップではありません。ユーザー自身が自らのローカル環境を弄り、Wired(Web)と同期させるという 「システムへのダイブ(儀式)」 として機能し、観測者をこの世界の当事者へと変容させる強烈な UX を生み出しています。

4. METADATA_SYNC: 音楽を「情報」として観測する

単に音を鳴らすだけでなく、その背景にある「データ」を詳細に観測するためのプロトコルを実装し、ターミナルの情報密度をさらに引き上げました。

info [id] コマンドの実装

ターミナルから指定した楽曲のメタデータを引き出すコマンドを追加。再生中以外でも、アーカイブの深層を探索することが可能になりました。

  • 観測可能なパラメータ: アーティスト名、BPM、属性(Type)、詳細タグ、楽曲解説。
  • 表示ロジック: JSONから取得したメタデータを整形し、システムカラー(#00f2fe)でターミナルに出力。タグ配列の展開なども自動で行われます。

演出の起点となるデータ構造の拡張

list.json を拡張し、楽曲ごとに typepopserious 等)を定義しました。

これにより、「明るい曲(pop)ならアクティブに動く」「シリアスな曲なら静かに佇む」といった、データ主導の演出(Data-Driven Performance)をアバターに指示するための重要なフラグが整いました。単なる音楽ファイルが、OSを制御するための「意味を持つデータ」へと昇華された瞬間です。