Ubuntu + PipeWire で「ゲーム機もPCもiPhoneも全部まとめて聴ける」低遅延ゲーミングオーディオミキサーを作った
3行でまとめ
- メインPC・Switch 2・iPhone など 5デバイス以上の音源を1台のUbuntu自宅サーバーでミックスして、遅延8ms で聴けるようにしました
- ソフトはほぼ PipeWire の標準機能だけ。自分で書いたのは配線スクリプトと、スマホ用のWebミキサー(Python標準ライブラリのみ)です
- 低遅延のミックスだけでなく、スピーカーやヘッドホンといった複数のDACに同時に音声出力できます
構成・実際のスクリプト・ハマりのポイントをGithubで公開しています → tomohiko-ohhashi/auras-public
動機
メインPC、ゲーム機、仕事用PC、スマホ・・・デスクの上に音の出るデバイスが複数あります。これらの音声入出力を切り替えることが非常に手間でした。
下記のことを同時に満たす市販のオーディオミキサーは見当たらず、今回作成しました。
- ゲームをするので遅延は一桁msに抑えたい
- アナログで混ぜたときに乗る「ジー」というノイズ(グラウンドループ)を避けたい
- 手元のスマホから音量やイコライザーを操作したい
買ったもの・使ったもの
ハードウェア
| 機材 | 役割 | 備考 |
|---|---|---|
| RME Digiface USB | 中核。光デジタル4入力/4出力+ヘッドホン出力のオーディオIF | 新規購入(約9万円)。この構成の要。このデバイスとWinかMacだけでも4系統のミキサーとしては成立する |
| 自作Ubuntuサーバー | ミックス処理(PipeWire) | 手持ち流用(Ryzen 5 8500G / ASRock B850) |
| ゼンハイザー BTA1 | Auracast送信機。ミックスをワイヤレス放送する | 新規購入(約2.5万円)。Bluetoothヘッドホンを切り替える手間を抑えるため。Auracast対応している音響機器はまだ市場に少ない。無くても成立 |
| ASTRO HDMIアダプタ | Switch 2 の音を光に分離 | 手持ち流用。Nintendo Switch2 はUSB音声が44.1kHz固定なので48kHzで出力するためにHDMI経由が必要 |
| FX-AUDIO FX-D03J | メインPCのUSB音声を光に変換するDDC | 手持ち流用(数千円) |
| 光ケーブル ×4〜5本 | 各機器 → Digiface | 数百円/本 |
必須なのは実質「光入力が複数あるオーディオIF」だけです。テレビのように最初から光出力がある機器はケーブル1本挿すだけで参加できます。
ソフトウェア
| ソフト | バージョン | 役割 |
|---|---|---|
| Ubuntu Desktop | 26.04 LTS | サーバーOS |
| PipeWire | 1.6.2 | 主役。ミックス・配線・EQ、全部これ |
| WirePlumber | 0.5.13 | デバイス管理(プロファイルやルール) |
| shairport-sync | 4.x | AirPlay受信(iPhoneやMacの音をミックスに入れる) |
| systemd (user unit) | - | サーバー起動時のミキサーの自動復元 |
| Python | 3.14(標準ライブラリのみ) | スマホ用Webミキサーの作成に使用 |
フレームワークやnpmパッケージは使っていません。あとで書きますが、PipeWireがCLIから何でも操作できるおかげで、Webミキサーは「CLIをHTTPに変換する薄い板」で済みました。
全体構成
設計のポイントは2つです。
① 音は全部「光」で集める。 光ケーブルは電気的に絶縁されているので、グラウンドループが原理的に発生しません。そして、BluetoothやHDMIキャプチャ経由の音(50〜100ms遅れます)と違って、遅延はほぼゼロです。
② ミックスはPipeWireにやらせる。 PipeWireは「Linuxで音を出す基盤」という印象が強いですが、実態は自由に配線できる音声ルーティングマトリクスです。ノード同士を pw-link で繋ぐだけで、ミキサーアプリを書かなくてもミキサーになります。
PipeWire側でやったこと
やったことは実質4つだけです。
1. 全系統を48kHzに固定する
サンプルレートが混ざると変換だらけになって音も遅延も濁るので、設定ファイル1枚で統一します。
context.properties = {
default.clock.rate = 48000
default.clock.allowed-rates = [ 48000 ]
default.clock.quantum = 64
}
2. 配線する
光入力のポートを出口(ヘッドホン)に pw-link で繋ぎます。同じ出口に複数の入力を繋ぐと、PipeWireが勝手にミックスしてくれます。これだけでもうミキサーです。
# 光入力1(左右)→ ヘッドホン出力(左右)
pw-link "alsa_input.usb-RME_Digiface_USB…:capture_AUX0" "alsa_output.usb-RME_Digiface_USB…:playback_AUX32"
pw-link "alsa_input.usb-RME_Digiface_USB…:capture_AUX1" "alsa_output.usb-RME_Digiface_USB…:playback_AUX33"
ポート名は pw-link -o(出力側)と pw-link -i(入力側)で一覧できます。まずこれを眺めると、自分のマシンの中の配線盤が見えてきて楽しいです。
3. 入力ごとの音量とEQを作る
各入力とヘッドホンの間に「音量とEQを持つ仮想ノード」(PipeWire標準の filter-chain)を1段ずつ挟みます。このノードの音量を wpctl で動かせば入力別フェーダーになり、バイキャッドフィルタを5個直列にすれば5バンドEQになります。遅延コストは1段あたり約1.3msです。
設定はこんな雰囲気です(抜粋)。EQのゲインは実行中に pw-cli で書き換えられるので、後述のWebミキサーからスライダーで操作できます。
{ name = libpipewire-module-filter-chain
args = {
filter.graph = {
nodes = [
{ type = builtin name = eq1 label = bq_lowshelf control = { "Freq" = 80.0 "Gain" = 0.0 } }
{ type = builtin name = eq2 label = bq_peaking control = { "Freq" = 250.0 "Gain" = 0.0 } }
# …1k / 4k / 10k と続く
]
}
capture.props = { node.name = "auras_in_opt1" media.class = Audio/Sink }
playback.props = { node.name = "auras_in_opt1_out" node.autoconnect = false }
}
}
4. systemdで永続化する
pw-link の配線はPipeWireの再起動で全部消えます。そこで「起動時に配線を張り直すサービス」を systemd のユーザーユニットで常駐させます。これで電源を入れるだけでミキサーが復元されるようになりました。
遅延の実測
光出力→光入力をケーブルで折り返して jack_iodelay で測りました。
| バッファ (quantum) | 往復遅延 | 結果 |
|---|---|---|
| 256 | 16.0ms | 安定 |
| 64(採用) | 8.0ms | 安定 |
| 48 | 6.2ms | 不安定(→ハマったポイント2) |
| 32 | - | 処理が破綻して音にならない |
quantum は「音をひとすくいする量」で、小さくするほど遅延が減る代わりに、処理の締切が厳しくなります。8msは音速に換算すると「スピーカーから約2.7m離れて聴く」のと同じで、体感では分かりません。
スマホから操作するWebミキサー
「スマホから音量を操作したい」の実装です。Python標準ライブラリの http.server だけで書いた小さなWebサーバーで、やっていることはリクエストに応じて wpctl や pw-cli を実行するだけです。それでもこれだけの機能になりました。
- 入力ごとの音量・ミュート・5バンドEQ(プリセット保存つき)
- 出力先の切り替え(USB DAC・AirPlayスピーカー・ワイヤレスドングルなど)と複数出力の同時ON
- バッファサイズ(quantum)のワンタップ切り替え
共有したい・ハマったポイント5選
作りっぱなしの記事にしても仕方ないので、実際に踏んだ落とし穴を共有します。
1. 既定の出力にミキサー自身を選ぶと世界が壊れる
「ミックスのコピーをOSの既定出力にも流す」機能を作ったところ、既定出力にミキサー自身が選ばれた瞬間、モニター→コピー→同じデバイスという循環グラフが完成して、PipeWire全体がスケジュール不能になりました。xrunが数万回積み上がり、無関係なブラウザの動画までカクカクに。循環を検出して配線を切るガード処理は必須です。
2. EQを1プロセスに5個入れたら音が消えた
5入力分のEQ(filter-chain)を1つのプロセスにまとめたら、1本のデータスレッドが5個ぶんを直列処理することになり、quantum 48(1ms周期)の締切に間に合わず、光入力の音が全出力で無音になりました。1入力=1プロセスに分割したら解決。処理を並列にできる形にしておくのは大事です。
なお、分割後も quantum 48 への切り替え自体がハード依存で不安定だったため(切り替え成功率 約25%)、最終的に既定は 64 に落ち着きました。1.8ms のために不安定さを抱えるより、確実な 8ms です。
3. 「固定遅延」が固定ではなかった
機器側の固定遅延だと思っていた255フレーム、quantumを下げたら201フレームに縮みました。ドライバ内部のバッファがquantumに比例していたようです。思い込みで計算せず、測る。今回一番の教訓かもしれません。
4. マザーボードのオンボード音声が実はUSB接続だった
最近のマザーボード(Realtek ALC4080系)は、オンボード音声が内部的にUSB接続になっています。lsusb に「Generic USB Audio」がいたらそれです。民生USBオーディオと同じく小さいquantumへの追従が苦手なので、低遅延設定と組み合わせるときは注意が必要でした。
5. Auracastが思った以上に便利だった
光出力の1本をAuracast送信機(BTA1)に挿すと、ミックスを台数無制限でワイヤレス放送できます。対応イヤホン・ヘッドホンを持っている家族がそれぞれ勝手に聴ける、小さな放送局のような状態になりました。遅延は約50msあるのでゲームには不向きですが、ながら聴きには十分です。
まとめ
- 光デジタルで集めて、PipeWireで混ぜる。これだけで「遅延8ms・ノイズなし・スマホ操作可・複数同時出力」のミキサーが組めました
- 実際に今回作成したミキサーとハマりポイントはGitHubリポジトリで公開しています → https://github.com/tomohiko-ohhashi/auras-public
