Nintendo Switchからデジタル音声を「直接」取り出す。FPGAで!

f:id:puhitaku:20200814153753j:plain

Switchはイヤホン出力の音がひどいことで有名だ*1

ブズズズズ………バババババ……

と熱雑音では明らかに説明できない周辺回路のお気持ちが音となって伝わってくる。

そこでUSB DACを使いたいのだが、どうも手持ちだとハイエンド機に限って動かない*2。別なやり方でデジタル音声を取り出さなければ。

取り出し方は様々ある。

  • ドックのHDMI出力 + S/PDIFスプリッター(TVモード必須)
  • Switch用Bluetoothトランスミッター(ぶっちゃけこれが一番オススメ)
  • Switchが認識できるUSB DDCで同軸デジタルなどにする
  • Raspberry PiのUACガジェット

Switchに直に接続できないDACを使いたい場合は、DDC必須*3でゴテゴテしてしまう。

「それなら本体に流れるデジタル信号(I2S)を直接取り出して、S/PDIFに変換するしかないよね!」と思いついた26の夜。早速やっていこう。なお、「自作が一番ゴテゴテするのでは?」というツッコミは受け付けておりません。

音声出力の解析

iFixitのページにあるように、Switchの中ではスマートアンプ Realtek ALC5639 が使われていて、DAC・ヘッドホン出力・スピーカー出力を一手に引き受けている。I2S(音声) + I2C(制御)でSoCと接続するこういうスマートアンプは組み込みでよく使われる*4

このどこかのピンにI2Sが入力されている!ということで早速プローブを当てる。結果は以下の通り。

f:id:puhitaku:20200814063512j:plain

そしてはんだ付け。

f:id:puhitaku:20200812032335j:plain

下はオシロで波を出してみた図。上からBCLK(ビットクロック)、LRCLK(左右のチャンネルを区別するクロック*5)、SDATA(シリアライズされた音声データ)。

f:id:puhitaku:20200814070540p:plain

S/PDIFへの変換

I2SとS/PDIFは信号の種類が違うので変換しなければならない。信号を変換する装置といえば!?

デケデケデケデケ…デン!!

そう!FPGA!!!(予定調和)

今回はこのSiPeed Tang Nanoという開発ボードを使う。Shigezone店頭価格でなんと800円。意味不明なくらい安い*6

f:id:puhitaku:20200814071346j:plain
画像は公式より拝借。

GOWIN GW1N-1 (LittleBee) が搭載されているので開発はGOWIN EDAで行う*7

FPGA内部では以下のタスクをやらせる。

  • I2Sの信号をデシリアライズして左右合計32bitのフレームを得る
  • フレームをFIFOに入れる*8
  • FIFOからフレームを取り出してS/PDIF TXに入れる
  • ええ感じにTXからS/PDIF信号が出る

前段のデシリアライズは手でSerdesを書いて対応。後段のS/PDIF TXはOpenCoresに実装があったのでこちらを使わせてもらった。

実装はもちろんGitHubに置いておきました。英語でのインストラクション付き。回路だけはんだ付けすればポン付けで動くようにしているので、GitHubのインストラクションやブレッドボードを参考に試してみて欲しい。

github.com

ちなみに面白いことに、Tang Nanoの赤LEDで光デジタルを直接出すことができるため、光デジタル対応DACであれば外付け部品ゼロで音を聴くことができる。最高!

今後

このままだと持ち運べないのでうまいことまとめて外でも使えるようにしたい。3Dプリンターも届いたし。

参考ページ

*1:現行機だと変わってるかもしれないが未確認

*2:UAC 1しか対応してない説が濃厚。ローエンド機はそれで十分だが、ハイエンド機は対応ビットレートが高いので必然的にUAC 2/3を選択することになる。

*3:ところでDDCを好き好んで「「ジッタの減少」」に使ってる人々は果たして【体感】できてるんだろうか?そんな人間に体感できないオーダーのジッタなんてプラセボに比べたら数桁小さいですよ。個人的な感想です。

*4:NVIDIA Jetson TXで同じフットプリントの姉妹チップALC5640が使われているので、ひょっとするとNVIDIAの開発ボードのデザインを任天堂がそのまま採用したんじゃないかな〜とか推測できるのが楽しい。5639のデータシートは見つからないが、5640のデータシートは普通に手に入るため解析にも役に立った。

*5:厳密にはこれはクロックではなくデータ

*6:1万すら切れないAltera DE-0 Nanoとか鼻で笑えるような最高のFPGA学習キットが作れるのではないだろうかと妄想している。顧客が本当に必要としていた「入門機」はこれや!

*7:大手に比べると情報が少なくて最初ちょっと戸惑うが、素朴なぶんめっちゃ軽くて良い開発環境

*8:駆動するクロックが異なるため非同期FIFOを挟んでいる。I2S = 6.144MHz, S/PDIF TX = 6.144MHz * 8 = 49.152MHz。S/PDIFにはvalidフラグがあるので、FIFOのalmost_fullフラグをvalidにつなげることで予期せぬアンダーフローによるノイズを防ぐ意味合いもある。

NikonのカメラをWebカメラにする方法(もちろんマルチOSで)

がなかったので、自分で開発した。公式アプリやSparkoCamとの差についてはこちらを参照。

tl;dr

f:id:puhitaku:20200727221049j:plain
概観

github.com

  • カメラからライブビュー (LV) を取ってきてOBS経由で仮想カメラにするソフト mtplvcap をGoで書いた
  • PCやスマホからカメラのフォーカス等を制御できるリモコンもある
  • libusbとOBSのおかげで Windows/macOS/Linux 全対応!
  • マジめっちゃ動作確認情報を欲してるので、動いた/動かなかった情報を@puhitakuにガンガン送ってほしい(DM可)

最新情報(2020/8/27現在)

Version 1.1.0リリース。

  • D5000に対応(rch850さん、多大なるデバッグ協力ありがとうございます!)

新たに動作が確認されたもの

  • D3200
  • D3300(unasuke さんありがとうございます!)
  • D5000(rch850 さんありがとうございます!)
  • D5300
  • D5500(nasustim さんありがとうございます!)
  • D600(ohtayo さんありがとうございます!)
  • D610(hazlitt さんありがとうございます!)
  • D7000(takashi0314 さんありがとうございます!)
  • D7200(br_spike_love さんありがとうございます!)
  • Z6
  • Z7

Nikon公式やSparkoCamとの比較

Nikon公式のWebカメラ化アプリ「Webcam Utility」(以下、WU)やSparkoCam(以下、SC)との違いは以下の通り。(情報は2020年8月8日現在)

mtplvcap WU SC
OS Win, Mac, Linux Win(MacはTBA) Win
課金 Free Free Paid
PCからの撮影設定 Yes No Yes
機種 D3200, D3300, D5300, D5500, D600, D610, D7000, D7200, Z6 他随時追加中*1 D6, D850, D780, D500, D7500, D5600, Z5, Z6, Z7, Z50 D3, D3s, D3X, D300, D300s, D4, D4s, D5, D500, D5000, D5100, D5200, D5300, D5500, D5600, D6, D600, D610, D700, D750, D780, D7000, D7100, D7200, D7500, D800, D810, D810A, D800E, D850, D90, Df, 1 V3, Z6, Z7, Z50

Webcam Utility の動作分析とお気持ちは以下。

使い方

日本語のREADMEを書いておいたのでそれを参照してください。

経緯

昨今のアレにより手持ちの一眼をWebカメラにするのが流行っている。CanonのWebカメラソフトはBetaながらMacとWindowsに対応してきたし、SIGMA fpはなんとUVCに対応している。 Sonyも2020年8月現在Windowsのみながら公式対応を発表した*2。しかしどうもそれ以外のベンダーは対応がしょっぱい。Nikon*3ユーザーな僕は、家で寂しく指をくわえるしかなかった。

4月以降はインカメが最高なPixel 3で快適にビデオ通話できてたし、一眼をわざわざ使う理由は正直ないのだが、

やっぱり…一眼で撮れたら嬉しいよね…。

ということで、開発に踏み切った。

日頃はほぼMac + ヘッドレスなLinuxの構成なので、Macで写したい。既存手法で出ているv002 Camera Live謎の海外ソフト頑張る方法だと、Macのみ・Windowsのみの対応となり汎用性がない。ソフトでライブビューを表示してそのウインドウをキャプチャするなんていう記事もあるけど、ゲーム配信じゃないんだし…とてもエレガントとはいえない。

ではどうやって画を取ってきて仮想カメラに流し込むか?その算段はついている。カメラからMTPで画を取り出し、WebSocketで配信、それをOBSのBrowserSource(OBSにエンベッドされたChromiumの画面をソースにできるやつ)で表示してOBSの仮想カメラに流すというものだ。

MTPとはMedia Transfer Protocolの略で、デバイスの制御とファイルのやり取りを網羅しているプロトコルだ。ベネッセ個人情報流出事件で用いられたテクニックはMTPと言えば思い出す人もいるかもしれない。USB Mass Storageとは異なるのでWindows以外では扱いにくくよく厄介者扱いされるが、機能的にはMass Storageよりも豊富なため携帯プレーヤーなどではデファクトスタンダードとなっている。

開発

要件は以下のように決めた。

  • Windows/macOS/Linux全対応
    • 昨今のカメラベンダーの対応に業を煮やしていたので
  • Goで書く
    • cgoが
    • リンクが楽
    • ランタイムがいらない
  • USB部分と仮想カメラ部分はOS間で差異があるためlibusbとOBSに任せる

色々探していると、これまたドンピシャなOSS go-mtpfs を発見した。AndroidなどのMTPデバイスとlibusb経由で低レベル通信をしつつ、FUSEを使ってマウントするものだ。

github.com

USBの低レベル通信とWebサーバーによる配信を1プロセスでやるにあたって、Go + libusbで書きたいと思っていたので、まさに願ったり叶ったり。macOSでデバイスの初期化が上手くいかない問題はあったもののGoLandのデバッガーのお陰でスムーズに解決し、forkした。

まず取り掛かったのはライブビューの開始。MTPではuint16の命令番号的なのを送りつけることで動作を命令することができる。例えば、ライブビューの開始は0x9201が対応している。これを送りつけたり応答をパースしたりする部分はgo-mtpfsの力を借りる。

次に取り掛かったのはライブビューの画の取得。ここでちょっと心拍数が上がってくる。これもまたライブビューの開始と同様に 0x9203 を送りつけるとキャプチャできる。

この画像をOBSのBrowserSourceで表示するには、フロントエンドを用意して、画像シーケンスを配信する必要がある。これはWebSocketでサクッと解決できた。

そしてそして、お待ちかねのOBS + Zoomの結合も難なく成功!やったー!

ひとまずのマイルストーンを達成したので、LinuxとWindowsでも動作確認。結論から言うと、WindowsのWSL 1では動作しなかったが、MinGW (MSYS2) では楽勝で動かすことが出来た。やはり年の功というか、歴史が長いだけあってのことか。MinGWだとネイティブのexeが出てきてくれるので、環境を用意する障壁がWSLよりも圧倒的に低い。これは超嬉しかった。

この後、紆余曲折を挟みつつも*4、ISOや絞りも変えることができた。

画像が安定して撮れるようになったところで、リモコンの実装に着手した。

リモコンはBootstrap + jQueryで書いた古式ゆかしいフロントエンド*5で、絞り・ISO・オートフォーカス(一定間隔でAFさせるのも設定可)・フレームレート制限を設定できる。カメラのダイヤルでモードをAUTOにすれば自動で輝度が調整されるので、それで十分であれば絞りやISOを手で変更する必要はない*6

f:id:puhitaku:20200727234015p:plain
リモコン on Mac

./mtplvcap -host 0.0.0.0 という風に外からのアクセスもListenするようにすれば、スマホからでも操作できる。

f:id:puhitaku:20200727234042j:plain
リモコン on Android

まとめ

まだ動作確認がとれている機種がD5300とD3300しかないので、ぜひお手元のNikonカメラで動作を確認してみて欲しい。もちろん一眼でなくても動作するはずだ。

なお、MTPで通信をするカメラであれば、原理上他メーカーでも対応が可能なはずだ。やり方としては、USBのVendor IDとProduct IDを見て動作を振り分ける方法を想定している。今はまだこの仕組みはないが、もしMTPで通信すると判明しているカメラがあって、私に貸していただけるならば実装を引き受けることも可能かもしれない。

*1:現在動作確認ができているもののみを挙げている

*2:対応機種も幅広くて好感が持てる

*3:当初は非公式かつWinのみの仮想カメラソフトを公式が紹介する始末で、最近やっとWindows対応・macOS公開時期未定なWebカメラソフトを公開した。

*4:Arrayなどを内含できるDevice Propertyなる可変な構造体があり、例えばISO値はこれで送られてくる。Goは可変な構造体は扱いにくくたいていreflect祭りになる。この割と複雑なエンコード・デコード処理が実装されておらず、go-mtpfsのコードを手で紐解く必要があった。

*5:いわゆるモダンさはないけど、これでいい。当然node.jsなどに触れることもないので、超シンプルになってくれるのが最高。

*6:現在はカメラの動作モードを見に行く処理がないので、AUTOモードで絞りやISOを変更しようとするとAccessDeniedが返ってくることに注意

この便利な時代にファイルウォッチャーを自作した

f:id:puhitaku:20200615004836p:plain

コンピューターにまつわるたくさんの知見が存在する2020年。GoogleやGitHubを通じて世界の知見を見渡しても、意外と自分が欲しい道具が見つからないことがある。

今回自作したのは、既に星の数ほどありそうな道具、ファイルウォッチャー。名前から連想できるように、「ファイルの変化を監視して、変化をトリガーにコマンドを実行するソフト」だ。

開発の背景紹介や既存との比較は後の方でするとして、まずは今回作ったファイルウォッチャーその名も "r3build" の特徴をピッチしていく。

github.com

r3build紹介ピッチ

さて、あなたはいま組み込みLinuxのカーネルをいじっているとしよう(まあ、PythonでもGoでも対象は何でも良い)。試行錯誤のたびに make を手で起こすのは辛いので、makeを自動化したいとする。

ここでr3buildが登場する。r3buildの真髄は r3build.toml という設定ファイルで容易かつ直感的に設定できることにある。以下のような要求を、非常に簡潔に記述して監視させることができる。

  • コード(c, h, dts)を保存したら勝手にmake allしてほしい
  • defconfigを保存したら勝手にdefconfigしてほしい
  • 関係ないファイルの変更は無視して欲しい
  • make中のファイルの変化は無視して欲しい

この場合、r3buildに読ませる r3build.toml は以下のようになる。

[[job]]
  name = "All"
  type = "make"
  regex = [
    ".+\.[ch]$",
    ".+\.dts$",
  ]
  environment.ARCH = "arm"
  environment.CROSS_COMPILE = "arm-linux-gnueabi-"

[[job]]
  name = "Defconfig"
  type = "make"
  target = "mxs_defconfig"
  glob = [
    "./**/mxs_defconfig",
  ]
  environment.ARCH = "arm"
  environment.CROSS_COMPILE = "arm-linux-gnueabi-"

キーとバリューの説明は以下の通り。

name

Human-readableなJobの名前。処理には影響せず、ログでのみ登場する。

type

検知したファイルイベントを処理する実装を指定する。この「実装」はprocessorと呼ぶ。processorには make(makeのターゲットを実行), command(任意のコマンド実行), pytest(pytestを実行)がある。今回はターゲット指定(任意)がないので make がそのまま(= make all)実行される。

regex

ファイルパスをマッチするルール。名前の通りregexで指定する。これにマッチするファイルでイベントが検知されると、processorがトリガーされる。文字列を置くと単一ルールのマッチになり、文字列のArrayを置くとOR条件扱いでマッチする。同様に regex_exclude を指定すると、それにマッチするファイルは無視される。

glob

ファイルパスをマッチするルール。こちらはglobで指定する。使い方はexcludeも含めてregexと一緒、どちらでも好きな方を使える。一応両方同時に使うこともでき、OR条件としてマッチする。

environment

環境変数を指定する。今回は組み込みLinuxなのでARCHとCROSS_COMPILEを指定している。

target

makeのtarget。make processor特有のオプション。

その他

今回は盛り込んでいないが、みんな大好き -j オプションの数字は標準で自動推定される。自分で指定したい場合は、 jobs キーで設定できる。

[[job]]
...
type = make
target = hoge
jobs = 8  # 0にするとCPUコア数から推定させられる

このようにして設定を記述し、 r3build を常駐させておくと *.c, *.h, *.dts, mxs_defconfig の変更を検知次第makeが走る。r3buildを起動するには以下のように実行する。

$ r3build

もしくは

$ python3 -m r3build

r3buildのアーキテクチャ

r3buildでは、こういった「ファイルのマッチ条件」と「イベントを受け取るprocessor」と「processorの設定」をまとめた1単位を Job と呼んでいる。[[job]] はTOMLの Array-of-Tables文法を使ったもので、見た目にもスッキリと監視対象を列挙することができる。

また、Jobが実行中に検知したイベントは標準で捨てるようになっている(注: 並列実行はまだ実装していない)。これは event.ignore_events_while_run = false と指定することで無効化できる。

[event]
ignore_events_while_run = false

[[job]]
...

この例ではコードのコンパイルと自動configしかやっていないが、任意のコマンドをJobに指定することができるので、例えばコンパイルして実機に自動デプロイまでやるとか、もっと大掛かりな仕組みを作ることも可能なハズ。想像は無限大!

ドキュメント

r3build.toml の書き方についてはリファレンスやサンプルを色々用意した。

残念ながらSphinxで生成したような「いわゆるドキュメント」はまだない。疑問があれば随時Twitterで質問してもらって構わないし、バグや機能リクエストがあればCONTRIBUTING.mdに従ってIssueやPRを立てて欲しい。知り合いからのバグ報告であればASAPで直しにかかる…はず!

開発の背景

言うまでもなく、「ファイルの変化を監視して、変化をトリガーにコマンドを実行するソフト」は既にたくさんあるし、気軽に使えるCLIツールの範囲でいくつか試した。

例えば、joh/when-changed(gorakhargosh/watchdogのラッパー)。when-changedは、監視したいファイル名と実行したいコマンドを指定すればそれっぽく起動してくれる。秒で自動化できるのは良いが、複数の問題をはらんでいる。

  • exclude対象が作者の好みにハードコードされている
  • コマンドを発火させている間のファイルの変更も全部拾ってしまい、永遠に実行が終わらない
  • イベントの濁流みたいなことになる(debounceされていない)

別な例として、when-changedの中身である gorakhargosh/watchdog もまたCLIツール watchmedo を内蔵しているが、Tricksなどの便利そうな仕組みがどうも見た目に直感的でないというか、好みではなかった。ちなみにwatchdog自体はマルチプラットフォーム対応も手厚く使いやすいモジュールなので、r3build内部でイベントを拾うソースとして使用している。

上記では「そこが美しくない」とだけ書いているが、この中間処理の記述がめちゃくちゃめんどくさい。自分はただ「Linuxのソースコードをいじったらmakeしてほしい」だけなのに、なぜお手製Pythonスクリプトをわざわざ書かなければならないのか。これから先も似たような汚い手法の繰り返しは嫌だな、と早々にうんざりしてしまった。

結局の所、上に書いたことをそのままエレガントにしたツールが欲しいという結論に達した。

  • エレガントな設定ファイルで秒で自動化できて欲しい(自動化のための苦労をゼロに近づけたい)
  • 監視・除外対象をglobやregexでいい感じに指定したい
  • コマンドを発火させている間のファイルの変更は捨てたい(わざと拾うのも可能にしたい)
  • イベントの種類を絞り込みたい(作成・変更・削除など)
  • 謎の立て続けにくるイベントとかはdebounceしたい

2月の頭に「ぼくのかんがえたさいきょうの設定ファイル」の構想から着手し、だいたい4ヶ月半経った今日ついに完成した。せっかくなのでロゴも作った。

github.com

ちなみにr3buildという名前はどこかの国のプロテインと被ってるっぽいけど、それ以外はなかったのでGoogleabilityも良い。

今後

そもそも日頃の課題を解決したくて作ったものなので、これからもBrainハックとかで使いつつ改善を続ける。今のところ実装したいことは以下の通り。

  • 同じJobで設定を変化させながら何度も実行する
  • もっとエレガントなConfigの模索?
  • Coverageを上げてテストケースを増やす

あとはとにかくいろんな人に使って欲しい!これを呼んでいるあなたも、ファイルの変化をトリガーに自動化したい事があれば、是非r3buildを思い浮かべて欲しい。

フィードバック、お待ちしております!