USBオーディオのフロー制御は複雑です。 2020年現在でも、買ってきたOSそのままではフロー制御に失敗しているケースが見られるので解説してみます。 自分でUSBオーディオのターゲットやOSのデバイスドライバを書く人向けの記述ですので、技術的バックボーンのない人にはチンプンカンプンかもしれません。
ついでにAmeneroやXMOSのDSDネイティブ転送路についても書きます。
ちょっとだけ、素人向けに導入を書きます。
USBオーディオは、PCMとかDSDを再生します。 それぞれサンプリング周波数というものがあり、一定周期で再生データを入れ替えます。この一定周期というのが曲者です。
CDのデータを無加工でUSB DACに送ろうとすると、1秒間に44100個のデータを送らなくてはなりません。 左右2チャンネルあって、それぞれ2バイトのデータから出来ています。 1秒間に送るデータは (44100 x 2 x 2)で176400バイトになります。 CDアルバム1時間分を再生するには、等速度の転送を3600秒続けることになります。
再生するには、正確なクロックが必要です。 2020年現在では、HiFiオーディオのUSB DACは非同期転送と言って、USB DAC側に正確な水晶発振器を搭載しています。 パソコン側にも水晶発振器がありますが、USB DACのクロックとごくわずかずれます。 パソコン側で1秒間に176400バイト送ったつもりでも、USB DACのクロックで見ると「1秒間あたり16バイト足りない」とか「毎秒4.5バイト多い」という状況が当たり前のように発生します。
この辻褄合わせをするために、USB DACの側から「データを少し減らして」とか「増やして」と細かく指図するのがフロー制御です。
ここから、玄人向けの記述になります。
USBオーディオのフロー制御が決められたのは、USBオーディオクラス1の時代です。 まだUSBの最高速度がFullSpeed = 12Mbpsで「24/96のDACで充分だろう」と思われていた時代です。
2020年の現在、USB HiSpeedで352.8kHzfsとかDoPで5.6MHzが再生できないと、HiFi機器として認めてもらえません。 規格を決めた人の想定外だと思いますし、いろいろ破綻しています。
破綻の理由は、転送量が多すぎて、転送速度が早すぎるからです。 フロー制御指示をするためのフィードバックが間に合わずに、バッファアンダーラン/オーバーランが発生しやすくなっています。
おまけにDoPなんていう規格もあります。 DoPが要求している動作は、「仕様を書くのは簡単」で「作るのは大変」なものです。 すこしだけ『エレキ工房No.5』で解説しました。
USBオーディオのAsynchronous転送プロトコルでハイレゾデータの転送が大変なため、バルク転送で逃げるメーカーも出てきました。 最初にUSBオーディオでバルク転送を使ったのはElectroArtこと田力氏が、某オーディオメーカーの下請けで作成したシステムでしょう。 Webmasterが『エレキ工房No.5』に書いたのはその次でしょうか。
バルク転送のフロー制御は、オーディオのフロー制御よりも単純です。 512バイトづつデータを送りつけて、Ackを確認します。 USBオーディオのターゲット側で受け取れなくなったらAckを返しません。 ホスト側はAckが返ってくるまで同じデータを再送します。
バルク転送でUSBプロトコル的には処理が簡単になりました。 ターゲットも作りやすいです。 でもホスト側は、大変になりました。
バルク転送でオーディオデータを送るには、Ackが返ってこなかった時のリトライを短い間隔で行う必要があります。 パソコンが用意しているハードウェアタイマーの割り込み間隔では足りないことがあるのです。 あるいは、OSのAPIに適切なタイマー設定がなかったりします。
バルク転送でオーディオデータを送るために、Ackが返ってこなかった時にイベントドリブンではなくポーリングでタイミングチェックして、CPUパワーを使ってしまうことがあるのです。
USBオーディオクラスのプロトコルには、もう一つ問題があります。 ホストからターゲットに対してサンプリング周波数やボリューム設定などのコマンドを出せます。 ターゲットでは、指示された処理の間ホストを待たせて、処理が終わったらAckを返します。 このAckを返すまでのタイミングが規格に決められていないのです。
Ackタイミングが決められていないため、あまりホストを待たせるとホスト側がタイムアウトして同じコマンドを繰り返し送ってきます。 すぐにタイムアウトする気の短いOSもあります。
ターゲット側にも事情があります。 サンプリング周波数設定時には、まず22MHz/25MHzの2系列のマスタークロックを切り替えます。 その後で、DAC LSIの設定をしますが、LSIによってはI2Cでコマンドを送らなければなりません。 丁寧に確認とリトライをしていると、すぐに数ミリ秒経過してホスト側がタイムアウトしてしまいます。
ホスト側が待ってくれない場合、ターゲット側はコマンドを受け取った時点でAckを返して、非同期に処理を進めることになります。 ホストから見て、「ターゲット側はすでに処理が終わっている」と思っているものだから、平気で次のコマンドを送ってきます。
サンプリング周波数のコマンドにAckを返した後で処理を続けていると、ホストから現在のサンプリング周波数が切り替わったかどうか問い合わせが来たりします。 まだ切り替え作業中だった場合、切り替え前後のどちらの周波数を返答するべきか迷います。 切り替え前の周波数を返すとホストに「このUSB DACはサンプリング周波数を切り替えてくれない」と思われます。 切り替え後の周波数を答えてしまうと、まだ切り替え中なのに音声データが送られて来たりします。
ここでホストを待たせるハンドシェークが規定されていないのは、USBオーディオプロトコルの欠点の一つです。
DoP以外に『DSDネイティブ転送』をうたったUSB DACデバイスがあります。 このネイティブ転送路について、調べた範囲で具体例で解説します。
まず、descriptors.zipに3種類のUSBオーディオインタフェースのディスクリプタを示します。 Amanero Combo384, DIYINHKのXMOS, SMSLのm100をそれぞれLinuxにつないで、lsusbコマンドで表示させた内容です。 以下、ディスクリプタの読み方を知らない人にもわかるように解説します。
まず、Amaneroのディスクリプタです。 通信路は2つあります。 どちらもPCMで2チャンネルの4バイトに32ビットデータを格納します。
AmaneroのDoPも32ビットで送ります。 DoPはそもそも3バイト24ビットに、8ビットのマーカーと16ビットのデータを格納する仕様でしたが、Amaneroはさらに8bitのパディングを加えて4バイト32ビット転送します。
次に、DIYINHKのXMOSのディスクリプタです。 通信路は6つありますが、3種類を2回繰り返しています。 PCMが二つ、2チャンネル4バイト24ビットと2チャンネル2バイト16ビットです。 RAWが一つ、2チャンネル4バイト32ビットです。
DIYINHKのXMOSのディスクリプタにはオーディオインタフェースの他にHIDインタフェースが登録されています。 おそらく、ファームウェア更新用のインタフェースだと思われますが、未確認です。
最後に、SMSL m100のディスクリプタです。 通信路は6つありますが、3種類を2回繰り返しています。 PCMが二つ、どちらも2チャンネル4バイト32ビットです。 RAWが一つ、2チャンネル4バイト32ビットです。
ざっとディスクリプタを眺めてみました。 AmaneroがどうやってDoPとDSDネイティブを切り替えているのかは不明ですが、XMOSを採用しているDIYINHKとSMSL m100ではRAWフォーマットがDSDネイティブの通信路のようです。 Linuxで強引にRAW通信路をSND_PCM_FORMAT_DSD_U32_LEとみなすパッチを作ってみました。
% diff sound/usb/format.c.org sound/usb/format.c 67c67 < pcm_formats |= SNDRV_PCM_FMTBIT_SPECIAL; --- > pcm_formats = SNDRV_PCM_FMTBIT_DSD_U32_LE;
このパッチをあてたカーネルも、このページで公開します。
DIYINHKのXMOSがDSDネイティブを32bit little endianで転送することは、テストデータを送りながらロジアナで監視して確認しました。 SMSL m100は、同じXMOSだから同じフォーマットだろうという当てずっぽうです。 その他のデバイスがRAWフォーマットを使うことがあるかもしれません。 それがDSDネイティブである保証も、32ビット little endian である保証もありません。
Ubuntu22.04を使っていてカーネルのソースコードを見ていたら、ちょうどこのパッチの辺りにDSD関係のフラグを立てるロジックが追加されていました。 試しにUbuntu22.04の標準カーネルにXMOSをつないでSNDRV_PCM_FMTBIT_DSD_U32_LEが使えるか試してみましたが、ダメでした。 まだ、このパッチは有効みたいです。
Ubuntu22.04向けのカーネルパッケージは、作成に手間取っています。 今までUbuntu18.04でカーネルのビルドをしていたのですが、最新カーネルのソースをコンパイルできません。 Ubuntu22.04にカーネルのビルド環境を構築するまで、気長に待っていてください。
以前ネットサーフィンしていた時の情報を、再発見しました。 AmaneroのネイティブDSD転送路については、githubのここに書いてありました。
WindowsにUSBオーディオクラス2のデバイスをつなぐ時は、つい最近までデバイスに対応したドライバをデバイスメーカーに供給してもらう必要がありました。 マイクロソフトのドライバは、クラス1までにしか対応していなかったのです。
Windows10の途中から、オーディオクラス2のデバイスドライバがマイクロソフトから供給されたようです。 Webmasterの自宅では、Amanro Combo384やSMSL m100をマイクロソフトのデバイスドライバで再生しています。
オーディオクラス1のドライバには、フロー制御のフィードバックに対する応答が遅いという問題がありました。 オーディオクラス2でも応答の遅さは同じようです。
マイクロソフトのUSBオーディオクラス2ドライバは、エレキ工房No.5に載せたADC/DACデバイスDAR-001TGをオーディオとして認識してくれません。 またDAR-001TGから派生してwebmasterが個人的に作成したDACデバイスも認識してくれません。 おそらく、USBターゲットのメーカーIDをチェックして、オーディオメーカーではないデバイスは弾いているものと考えられます。
コントロールパネルからサウンドを選択すると、サウンドデバイス一覧が出て、無効なデバイスとして表示されます。 この無効なデバイスをクリックすると、次のダイアログが出ます。
ダイアログ下側のコンボボックスからデバイスの有効化を選択し、OKボタンを押します。
3年ぶりくらいでWASAPIを使用するアプリをVisualC++でコンパイルしたら、コンパイルは通るのにライブラリがエラーを返します。 調べてみたら、ライブラリが要求するバッファサイズが増大していて今までのコードでは実行できません。 WASAPI側に1秒間くらいの大きな音声バッファを用意して初めて再生できます。 音切れの対策なのでしょう。 WindowsのUSBオーディオドライバは、フィードバックへの応答が遅いですから他のところで時間を稼がないと。
WASAPIの排他モードで再生できなくなったデバイスがあります。2023年4月現在、パソコンの付属スピーカーでよく使われるRealtek High Definition Soundのドライバ経由でWASAPIの排他モード再生がノイズになります。 マイクロソフトのQ&Aサイトに書いてありました。 またRatoc社のUAC1デバイス RAL-2496HA1 へWASAPIの排他モードで音声を送れません。
WebmasterはALSAのUSBオーディオドライバのソースコードを読んで、以前から気になっていることがありました。
フロー制御のフィードバックが返ってきた時に、「本来の速度の75%未満」だと勝手にフィードバック量を水増ししてしまいます。 逆に「本来の速度の150%」より大だと、フィードバック量を減らします。 例えば、USBオーディオ側が「60%に抑えて」とフィードバックを返してきた時に、「60%は範囲外だから120%に違いない」という判断をしてしまうのです。 減らさなければならない時に増やしてしまいます。
ソースコードのコメントを読むと、USBオーディオの規格から外れた動作をするUSB DACを救済する措置のようです。 規格外のDACは救済されるかもしれませんが、規格どおりに動作しつつフィードバックが75%未満や150%オーバーになった場合、誤動作します。 誤動作の結果、DACデバイス側でバッファアンダーラン/オーバーランが発生しやすくなります。
Webmasterは、ALSAのコードを書き換えてみました。 USBオーディオクラスの規格どおりに動作する代わりに、フィードバックが50%〜200%まで追従できるドライバです。 カーネルの sound/usb/endpoint.c に以下のパッチを当ててください。
初出のパッチにはバグがあったので、3月28日深夜にカーネルパッケージと合わせて差し替えました。
67a68 > * (fs in Q26.6) 76a78 > * (fs in Q29.3) 660,661c662,663 < /* assume max. frequency is 50% higher than nominal */ < ep->freqmax = ep->freqn + (ep->freqn >> 1); --- > /* assume max. frequency is double value of nominal */ > ep->freqmax = ep->freqn * 2; 1187c1189 < if (unlikely(sender->tenor_fb_quirk)) { --- > if (unlikely(ep->freqshift == INT_MIN)) { 1189,1191c1191 < * Devices based on Tenor 8802 chipsets (TEAC UD-H01 < * and others) sometimes change the feedback value < * by +/- 0x1.0000. --- > * This driver assumes all USB audio target follow standards. 1193,1212c1193,1194 < if (f < ep->freqn - 0x8000) < f += 0xf000; < else if (f > ep->freqn + 0x8000) < f -= 0xf000; < } else if (unlikely(ep->freqshift == INT_MIN)) { < /* < * The first time we see a feedback value, determine its format < * by shifting it left or right until it matches the nominal < * frequency value. This assumes that the feedback does not < * differ from the nominal value more than +50% or -25%. < */ < shift = 0; < while (f < ep->freqn - ep->freqn / 4) { < f <<= 1; < shift++; < } < while (f > ep->freqn + ep->freqn / 2) { < f >>= 1; < shift--; < } --- > shift = (urb->iso_frame_desc[0].actual_length == 3) ? 2 : 0; > shift -= ep->datainterval; 1214c1196,1197 < } else if (ep->freqshift >= 0) --- > } > if (ep->freqshift >= 0) 1219,1233c1202,1204 < if (likely(f >= ep->freqn - ep->freqn / 8 && f <= ep->freqmax)) { < /* < * If the frequency looks valid, set it. < * This value is referred to in prepare_playback_urb(). < */ < spin_lock_irqsave(&ep->lock, flags); < ep->freqm = f; < spin_unlock_irqrestore(&ep->lock, flags); < } else { < /* < * Out of range; maybe the shift value is wrong. < * Reset it so that we autodetect again the next time. < */ < ep->freqshift = INT_MIN; < } --- > spin_lock_irqsave(&ep->lock, flags); > ep->freqm = f; > spin_unlock_irqrestore(&ep->lock, flags);
このソースコードでビルドしたカーネルパッケージも公開します。 XMOSのRAW通信路を強引にSND_PCM_FORMAT_DSD_U32_LEとみなすパッチも入っています。
ALSAのソースコードが書き換わっていたので、新しいパッチを公開します。
git diff
の結果です。
diff --git a/sound/usb/endpoint.c b/sound/usb/endpoint.c index 647fa054d8b1..51413457caa6 100644 --- a/sound/usb/endpoint.c +++ b/sound/usb/endpoint.c @@ -1120,7 +1120,7 @@ static int data_ep_set_params(struct snd_usb_endpoint *ep, } /* assume max. frequency is 50% higher than nominal */ - ep->freqmax = ep->freqn + (ep->freqn >> 1); + ep->freqmax = ep->freqn *2; /* Round up freqmax to nearest integer in order to calculate maximum * packet size, which must represent a whole number of frames. * This is accomplished by adding 0x0.ffff before converting the @@ -1859,11 +1859,11 @@ static void snd_usb_handle_sync_urb(struct snd_usb_endpoint *ep, * differ from the nominal value more than +50% or -25%. */ shift = 0; - while (f < ep->freqn - ep->freqn / 4) { + while (f < ep->freqn / 2) { f <<= 1; shift++; } - while (f > ep->freqn + ep->freqn / 2) { + while (f > ep->freqn * 2) { f >>= 1; shift--; } diff --git a/sound/usb/format.c b/sound/usb/format.c index 4b1c5ba121f3..565b788d333f 100644 --- a/sound/usb/format.c +++ b/sound/usb/format.c @@ -57,7 +57,7 @@ static u64 parse_audio_format_i_type(struct snd_usb_audio *chip, sample_bytes = fmt->bSubslotSize; if (format & UAC2_FORMAT_TYPE_I_RAW_DATA) { - pcm_formats |= SNDRV_PCM_FMTBIT_SPECIAL; + pcm_formats = SNDRV_PCM_FMTBIT_DSD_U32_LE; /* flag potentially raw DSD capable altsettings */ fp->dsd_raw = true; }
Ubuntu 22.04でも動作することを確認しました。
linux-headers-5.15.57xmos_5.15.57xmos-2_amd64.deb linux-image-5.15.57xmos_5.15.57xmos-2_amd64.deb linux-libc-dev_5.15.57xmos-2_amd64.debUbuntu 22.04で動作することを確認しました。 2023年4月16日現在最新のカーネルなので、当分これが使えそうです。
linux-headers-6.3.0-rc6-xmos_6.3.0-rc6-00173-g7a934f4bd7d6-3_amd64.deb linux-image-6.3.0-rc6-xmos_6.3.0-rc6-00173-g7a934f4bd7d6-3_amd64.debビルド番号27は、XMOSへlittle endian の⊿Σを送る代わりに、間違ってbig endianを送っていました。 2024年1月23日にビルド番号35に差し替えます。
Ubuntu 22.04で動作することを確認しました。
linux-headers-6.5.13-xmos_6.5.13-35_amd64.deb linux-image-6.5.13-xmos_6.5.13-35_amd64.debUbuntu 22.04で動作することを確認しました。
linux-headers-6.7.0-xmos_6.7.0-09928-g052d534373b7-33_amd64.deb linux-image-6.7.0-xmos_6.7.0-09928-g052d534373b7-33_amd64.debUbuntu 24.04で動作することを確認しました。
linux-headers-6.11.0-rc7-xmos_6.11.0-rc7-00149-g0babf683783d-38_amd64.deb linux-image-6.11.0-rc7-xmos_6.11.0-rc7-00149-g0babf683783d-38_amd64.debALSAは、余計なお世話をしてくれることがあります。
再生アプリがPCMを送る時、その前後に余計なゼロをつけてくれるのです。 余計なゼロをつけるのは、ALSAライブラリとデバイスドライバの両方です。 ソースコードを読むとわかります。
DoPが出てきた初期には、PCM転送中にPCMからDoPに変化するデータに追従できないターゲットがありました。 この手のDACでは、DoPを送るときは、最初から最後までDoPを送らないと再生してくれません。 DoPの前後に余計なゼロをつけられると、DoP再生できないのです。 ALSAではDoP再生できないDACがあったのでした。
ALSAを組み込んだLinuxにUSB DACを接続すると、何も音声を送っていないのにモードが切り替わります。 これもALSAが勝手にやっています。
ALSAが新しいUSB DACを検出すると、最初に一番低いサンプリング周波数(例えば44.1kHzfs)に切り替えて少し0データを送ります。 次に一番高いサンプリング周波数(例えば384kHzfs)に切り替えて0データを送ります。
その後は、ミキサーあたりがデバイスを握るので、44.1kHzを送りっぱなしにして時々SEが再生されます。
Mac OS Xでも、Snow Leopardの時代にLinuxと同じフロー制御のロジックが入っていました。 Mavericksの頃には修正されたみたいです。
USBオーディオクラス2のフロー制御に限って言えば、Mac OS Xは一番安定しているOSです。 でも、ビットパーフェクト再生する時に24bitデータまでしか渡せないという問題があります。 32bitデータを送ることでDSDネィティブ再生するUSB DACには、専用のデバイスドライバを用意しなくてはなりません。
Snow Leopardの頃には、俗に「integerモード」と呼ばれるAPIがあって、32bit整数を送ることができました。 その後、スーパーユーザーでないとopenできないようになり、やがてだれもアクセス出来なくなりました。 もしかしたら、まだ裏口が残っているのかもしれませんが、webmasterは見つけていません。
Webmasterが以前RATOC社の社長さんブログで読んだ話です。 「OSによっては、USB DACにサンプリング周波数切り替えコマンドを送らずに、すぐにデータを送ってくるものがある」と書いていました。
Webmasterが調べた限り、RATOC社の社長さんの勘違いである可能性が高いです。 WebmasterはRATOC社のDAC製品『RAL-2496HA1』を持っています。 この機種のUSBディスクリプタを見ると、シンクロナス転送でもアシンクロナス転送でもなくてアダプティブ転送であると宣言しています。 アダプティブ転送は、ホストが送ってくるデータ量に合わせてサンプリング周波数を変更できる仕組みです。 サンプリング周波数設定コマンドなしでデータが送られても、アダプティブでは文句は言えません。
ディスクリプタをシンクロナス転送として宣言しておけば、データが送られてくる前に、サンプリング周波数切り替えコマンドが来たのではないでしょうか。
今日、数年ぶりにRAL-2496HA1を引っ張り出してWindows10につないでみました。 ミキサー経由では普通に再生できますが、WASAPIの排他モードでアクセスすると急速にデータを吸い込んで音が出てきません。
WASAPI で再生できない問題は、2023年6月に解決しました。 WASAPIのバグでした。 詳細は フリーソフトFPlayerに書いてあります。
2020年3月28日 初出
2024年9月18日 追記