UAC GadgetでNintendo Switchの音声出力をRaspberry Piに取り込む

前回の記事でHDMI映像入力をRaspberry Piで扱う方法を紹介し、その最後に音声の取り込みについて、まだ課題が残っていると書きました。 HDMI入力からの音声取り込みといった、本質的な課題の解決を試みているものの、なかなかに難しい問題に直面しているので、対象を限定して部分的解決に挑みます。

主に今HDMI入力の対象として使おうと思っているデバイスは、Nintendo Switchです。 なので、Nintendo Switchの音声出力に限定して、それをRaspberry Piで取り込むことだけを目的とし、音声が取り込めていない問題を解決していきます。

Table of Contents

  1. Nintendo Switchの音声出力
    1. スピーカー
    2. 3.5mmイヤホンジャック
    3. HDMI
    4. USB
  2. UAC (USB Audio Device Class)
  3. USB Gadget API
  4. UACデバイスシミュレート
    1. Pro Controller + UAC
  5. 音声取り込み
  6. 映像と音声をWebRTC
  7. まとめ

Nintendo Switchの音声出力

Nintendo Switchには、4つの音声出力方法があります。これらのいずれかから音声を取り込む必要があります。

スピーカー

body-switch01-front

引用元: Nintendo Switch|任天堂

主にテーブルモードや、携帯モードでプレイする時に音声が出力されます。本体前面に搭載されたステレオスピーカーから音声が出力されますが、これを取り込むにはマイクを用いる必要があり、 遅延の発生とノイズが乗るので現実的ではありません。

3.5mmイヤホンジャック

スピーカーからではなく、本体上部のヘッドホンマイク端子から音声を出力することもできます。スピーカー出力の音声をマイクを使って取り込むよりも、イヤホンジャックの出力を取り込んだ方がノイズは少ないですが、アナログ・デジタル変換が必要なので、やや手間がかかります。

HDMI

TVモードで映像を出力しているとき、スピーカーの代わりにHDMIケーブルを通して音声がテレビから出力されます。信号がデジタルなのと、サラウンドに対応しているため高音質ですが、機器に取り込む方法が限定されるため、障壁が高いです。

USB

body-switch02-front

引用元: Nintendo Switch|任天堂

本体底面のUSB Type-C端子や、Nintendo SwitchドックのUSB端子に接続しているUSBサウンドデバイスから、音声を出力する方法です。Sound Blaster G3などの製品を接続して、イヤホンジャックからの出力より高品位な音声を楽しむことができるそうです。 通信はUAC(後述)というプロトコルを通して、音声出力をデジタル信号でやり取りしています。UACで取り込みたい機器と通信ができれば、最もこれが手軽かつノイズの少ない出力が得られます。

UAC (USB Audio Device Class)

USB Audio Devices Rev. 3.0 and Adopters Agreement | USB-IF

USB接続のサウンドカードや、USB Type-C接続のイヤホンなどが採用しているUSB経由で音声を転送するプロトコルです。 マイクなどの入力装置と、スピーカーなどの出力装置とのどちらとも、このプロトコルで転送できます。 USB Audio Device Classには大きく分けて、Class 1(UAC1)、Class 2(UAC2)、Class 3(UAC3)の3つの実装レベルの異なったクラスが定義されています。 WindowsやmacOS、LinuxやAndroid含め、ほとんどのOSに標準でUAC1とUAC2のドライバが導入されています。

USB Gadget API

聞き覚えがありますね。スマホでNintendo Switchを操作する手法を紹介したの記事で登場しています。 聞いたことない人はそちらをチラッとみていただくとして、そこに挙げたドキュメント記載の例にsound subsystem (for audio gadgets)があり、USB Audio Device Classのシミュレートもできるのです。 これでRaspberry PiをUACデバイスとして振舞うようにできれば、Nintendo SwitchのUSBサウンド出力を取り込めるかもしれません。

UACデバイスシミュレート

物は試しということで、早速Nintendo SwitchとRaspberry Piを接続してconfigfsでUAC1デバイスを作ってみます。 手法を紹介したの記事と同様、/boot/config.txtと/etc/modulesに追記を済ませておく必要があります。 接続はシンプルに、それぞれのUSB Type-CポートにUSB 2.0 Type-Cケーブルを接続するだけです。

root権限でこれを実行すると、Nintendo SwitchがRaspberry Pi 4をUSBサウンドデバイスとして認識しました。

usb audio device detected

uac1.0の部分をuac2.0に書き換えることで、UAC2 Gadgetもシミュレートできますが、試したところNintendo SwitchのUSBサウンドデバイスは、USB GadgetにおいてはUAC1の出力装置のみを認識していました。 また、sudo modprobe g_audioでもUAC Gadgetは作成できますが、こちらもUAC2なので認識しませんでした。

Pro Controller + UAC

Nintendo SwitchはPro Controllerのシミュレートに関しては、製品ID(idProduct)と製造元ID(idVendor)を純正のものと同一にする必要がありました。 しかしUSBサウンドデバイスに関しては、なんでもいいみたいです。純正品やライセンス商品が販売されていないからでしょうか。

そして、なんと、Pro Controllerを模したUSB GadgetにUACの機能を追加しても、認識しちゃうのです!

この状態で、1.3inch LCD HATでの入力を試してみると、ちゃんとUSBサウンドデバイスとPro Controllerとして認識されています。

pro controller and audio device

音声取り込み

UAC1 Gadgetとして振る舞っている間、ALSAにはUAC1Gadgetという名前のサウンドカードとして認識されます。

pi@raspberrypi:~ $ lsmod | grep uac
usb_f_uac1             16384  2
u_audio                20480  1 usb_f_uac1
libcomposite           57344  11 u_audio,usb_f_uac1
udc_core               53248  4 dwc2,u_audio,libcomposite,usb_f_uac1
pi@raspberrypi:~ $ cat /proc/asound/cards
 0 [ALSA           ]: bcm2835_alsa - bcm2835 ALSA
                      bcm2835 ALSA
 1 [UAC1Gadget     ]: UAC1_Gadget - UAC1_Gadget
                      UAC1_Gadget 0
pi@raspberrypi:~ $ arecord -l
**** List of CAPTURE Hardware Devices ****
card 1: UAC1Gadget [UAC1_Gadget], device 0: UAC1_PCM [UAC1_PCM]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

以下のコマンドでUAC1ガジェットから、ステレオ2チャンネル、サンプリング周波数48000Hz、量子化ビット数 符号付き16ビット、VUメーターステレオ表示しつつ、/tmp/rec.wavに音声ファイルを保存できます。

arecord -v -D hw:UAC1Gadget -c2 -r 48000 -f S16_LE -t wav -V stereo /tmp/rec.wav

ちゃんと動きました。

arecord recording

映像と音声をWebRTC

前回の記事でも扱った映像のWebRTC配信に、音声も乗っけてみます。

HDMI出力はUSB Type-C端子からは取り出せないので、Nintendo Switchドックに接続する必要があります。 そして、Raspberry Pi 4とNintendo Switchドックは裏面のUSB 3.0ポートに、以下のようなUSB Type-A to USB Type−C 3.0ケーブルを使って接続します。 消費電力の関係で、USB 2.0ポートやUSB 2.0ケーブルを使うと電力不足でRaspberry Piが落ちます。 また、Nintendo SwitchをDockから抜き差しするとRaspberry Piへの給電が止まるので、間にセルフパワーのUSBハブを噛ませた方がいいです。 以下は実際に使ってるUSBケーブルとUSBハブです。

Amazon.co.jp: Anker USB Type C ケーブル PowerLine USB-C & USB-A 3.0 ケーブル Xperia / Samsung Galaxy / LG / iPad Pro MacBook その他 Android Oculus Quest 等 USB-C機器対応 1.8m ブラック: 家電・カメラ

Amazon | ORICO 4ポート USB3.0 ハブ 電源付き BC1.2 (5V2.4A) 急速充電対応 5Gbps転送 セルフパワー 12V2A電源アダプター付き アルミ筐体 ブラック A3H4-V2 | ORICO | USBハブ 通販

ソフトウェアは今回も動作確認できればいいので、デモコードを用います。

前回はgstwebrtc-demosのコードを用いてV4L2デバイスをそのまま扱ったところ、再接続してから映像が取得できなくなるという問題を抱えていました。 これはMMAL(Multi-Media Abstraction Layer API)の手続きを正しく実行することで解決できます。そして、それをやってくれるGstreamerのelement(プラグイン)が、前回少し登場したgst-rpicamsrcです。

thaytan/gst-rpicamsrc: GStreamer element for the Raspberry Pi camera module

これをv4l2srcの代わりに用いることで、再接続しても映像の取得が正しく行えるようになります。 加えて、このリポジトリのexamplesにWebRTCのデモがあるので、それをちょっと編集して音声を乗っけていきます。

gst-rpicamsrc/webrtc-unidirectional-h264.c at 79860a0b2a0f3beef6ccfab0cf7d531e55e3b06c · thaytan/gst-rpicamsrc

diff --git a/examples/webrtc-unidirectional-h264.c b/examples/webrtc-unidirectional-h264.c
index b8a716d..b9674b3 100644
--- a/examples/webrtc-unidirectional-h264.c
+++ b/examples/webrtc-unidirectional-h264.c
@@ -185,10 +185,12 @@ create_receiver_entry (SoupWebsocketConnection * connection)
 
   error = NULL;
   receiver_entry->pipeline = gst_parse_launch ("webrtcbin name=webrtcbin stun-server=stun://" STUN_SERVER " "
-      "rpicamsrc bitrate=600000 annotation-mode=12 preview=false ! video/x-h264,profile=constrained-baseline,width=640,height=360,level=3.0 ! queue max-size-time=100000000 ! h264parse ! "
-      "rtph264pay config-interval=-1 name=payloader ! "
-      "application/x-rtp,media=video,encoding-name=H264,payload="
-      RTP_PAYLOAD_TYPE " ! webrtcbin. ", &error);
+      "rpicamsrc preview=false ! video/x-h264,profile=constrained-baseline,width=1280,height=720,level=3.1 ! queue max-size-time=100000000 ! h264parse ! "
+      "rtph264pay config-interval=-1 ! "
+      "application/x-rtp,media=video,encoding-name=H264,payload=96 ! webrtcbin. "
+      "alsasrc device=hw:UAC1Gadget ! audioconvert ! audioresample ! queue ! opusenc ! "
+      "rtpopuspay ! queue ! "
+      "application/x-rtp,media=audio,encoding-name=OPUS,payload=97 ! webrtcbin. ", &error);
   if (error != NULL) {
     g_error ("Could not create WebRTC pipeline: %s\n", error->message);
     g_error_free (error);

上記パッチを当て、Readmeに従ってsudo make installまでできていると、examples/にWebRTCデモの実行ファイルができているはずです。

ビルド前の依存パッケージの導入と実行時の注意は、前回の記事を参考にしてください。 また、追加でsudo apt install gstreamer1.0-alsaをしてALSAのプラグインも導入しておく必要があります。

OPENSSL_CONF= ./examples/webrtc-unidirectional-h264で実行し、Chromeでみてみると、音声もWebRTCで転送できていることが確認できます。

まとめ

とりあえずNintendo Switchの音声出力が取れました。

前々回はNintendo Switch Pro ControllerのWeb対応。 前回はNintendo Switchゲーム画面のWeb対応。 今回はNintendo Switchゲーム音声のWeb対応。

何を作ろうとしてるか、だんだんワクワクしてきましたね?