はつねの日記

Kinect, Windows 10 UWP, Windows Azure, IoT, 電子工作

NAudioをUWPで使うときの注意点

以下、NAudio v1.10での確認となります。
NAudioですが、音声入出力に非常に便利なライブラリで、.NET Framework以外にも様々なプラットフォームで動作します。
github.com

しかし、UWPで使用する場合、いつくかの注意点があります。

.NETネイティブコンパイル時の注意点

それは、ARM / ARM64に限らずx86をプラットフォームターゲットとしたときも.NETネイティブコンパイル時には次のような実行時エラーが発生します。

System.ArgumentException: 'Unsupported Wave Format'

この実行時エラーは、NAudioのサンプルコードでNAudioUniversalDemoがあるので、そちらで確認することができます。
このサンプルコードを.NETネイティブコンパイルして実行して「Record」ボタンをクリックすると、NAudioの中、具体的には、NAudio.Wave.WasapiCaptureRT.InitializeCaptureDeviceの中で下記のエラーが発生します。
こちらは録音デバイスがサポートしていないWaveフォーマットを指定したからだということです。
こちら、.NETネイティブではないときには、WaveFormatとしてサンプリングレート48000、32ビット、1チャンネルのIeeeFloatなフォーマットが自動的に指定されますが、.NETネイティブコンパイル時はその自動指定が行われず、ソースコードを読む限りはAudioClient.csの中のpublic WaveFormat MixFormatプロパティのGetの中でaudioClientInterface.GetMixFormatのところで取得ができていないようです。

注:Visual Studio 2019を使用し、ターゲットバージョン=2004 (ビルド19041)、最小=1803(ビルド17134)を指定

.NETネイティブコンパイルは必須

それでは、.NETネイティブコンパイルしなければいいのでは?となりますが、ストアに登録して配布するときは.NETネイティブコンパイル必須となっているので、配布手段はどうあれ.NETネイティブコンパイルで動作しないというのは、今後を考えると避けたいところです。

対応策

対応策としては、サンプルコードNAudioUniversalDemoでいえばMainPageViewModel.csのなかでrecorder.StartRecording()を実行する前に、明示的にWaveFormatを指定してしまうことです。
NAudio/MainPageViewModel.cs at master · naudio/NAudio · GitHub

recorder.WaveFormat = new WaveFormat(48000, 32, 1);

なお、この場合、IeeeFloatではなくPCMのサンプリングレート48000、32ビット、1チャンネルとなります。
また、万が一、指定したサンプリングレートおよびチャンネル数が、該当のデバイスでサポートしていない録音デバイスだったときも実行時エラーとなります。
こうなってくるとストアに登録して広く公開するのはなかなか難しい状況だといえるでしょう。
例えば、ARM64 Windows機のLenovo C630のInternal Microphoneは、サンプリングレート48000Hz、16ビット、1チャンネルですが、インテルWindows機だと2チャンネルの場合があります。

解決方法

解決補法としては3つ

  1. NAudioを使わない方法を考える
  2. ストア配布対象をx86に絞る
  3. NAudioのMixFormat取得ロジックを.NETネイティブ対応する

まずは、1か2の検討をして、なんとか3が解決される(もしくはOSSなので自力で解決してプルリクする)のを待つ形になります。

本当の解決を目指して

NAudioのMixFormat取得ロジックは.NETネイティブコンパイルしていないときと、.NETネイティブコンパイルしたときではどのように違うか確認してみましょう。
NAudioのGitHubからソースコードを取得して、AudioClient.csのMixFormatクラスにブレイクポイントを貼って、デバッグ実行をしてみます。

非.NETネイティブコンパイル時の動き

48000Hz、16bit、2チャンネルの録音デバイス
非ネイティブのときは、MixFormatの戻り値は、サンプリングレート48000Hz、32bit PCM、2チャンネルとなります。

{32 bit PCM: 48kHz 2 channels wBitsPerSample:32 dwChannelMask:3 subFormat:00000003-0000-0010-8000-00aa00389b71 extraSize:22}
AverageBytesPerSecond: 384000
BitsPerSample: 32
BlockAlign: 8
Channels: 2
Encoding: Extensible
ExtraSize: 22
SampleRate: 48000
SubFormat: {00000003-0000-0010-8000-00aa00389b71}
averageBytesPerSecond: 384000
bitsPerSample: 32
blockAlign: 8
channels: 2
dwChannelMask: 3
extraSize: 22
sampleRate: 48000
subFormat: {00000003-0000-0010-8000-00aa00389b71}
wValidBitsPerSample: 32
waveFormatTag: Extensible

この中のsubFormatの値がWAVE_FORMAT_IEEE_FLOATを意味しています。
この内部値は外から見えないので、外から見えるrecorder.WaveFormatの値を見てみましょう。
こちらは、IeeeFloatとして取り出せます。

{IeeeFloat}

WaveFormat未指定時に本来期待されている動きは、この動きになります。

.NETネイティブコンパイル時の動き

それでは.NETネイティブコンパイルしたときの値をみてみましょう。
MixFormatの戻り値は、サンプリングレート48000Hz、32bit PCM、2チャンネルとなりますが、クラスの型値がExtensibleのままでPCM表記になりません。

{NAudio.Wave.WaveFormatExtensible}
AverageBytesPerSecond: 384000
BitsPerSample: 32
BlockAlign: 8
Channels: 2
Encoding: Extensible
ExtraSize: 22
SampleRate: 48000
SubFormat: {System.Guid}
averageBytesPerSecond: 384000
bitsPerSample: 32
blockAlign: 8
channels: 2
dwChannelMask: 3
extraSize: 22
sampleRate: 48000
subFormat: {System.Guid}
wValidBitsPerSample: 32
waveFormatTag: Extensible

値を確認すると今回の原因はSubFormatにWAVE_FORMAT_IEEE_FLOATを意味する値が入っていないことがわかりました。
しかしここで注目すべきは、サンプリングレート、ビット数、チャンネル数などは取得ができてる点です。もしこれらの値を取得できれば、new WaveFormat(48000,32,1)のように指定してNAudioに該当録音デバイスのフォーマットが指定できることになります。

さあ、NAudioを書き換えよう

NAudioの内部状態のところでエラー判定されているので、NAudioの外側からどうにかできる問題じゃなさそうです。
そうなってくるとNAudioのどのクラスで対応するかですが、UWPのみの問題だと考えると、WasapiCaptureRTクラスの中で対応するのがよさそうです。

gist.github.com

audioClient.MixFormatメソッドの戻り値を変数名mixにいれて、それがWaveFormatExtensibleであればビット数に応じてIeeeFloatかPCMか明示してフォーマットを作成することにします。
例えば、先ほどの48000Hz、16bit、2チャンネルの録音デバイスであれば、内部変数のwaveFormatは下記のようにIeeeFloatとして処理される内容になります。

{NAudio.Wave.WaveFormat}
AverageBytesPerSecond: 192000
BitsPerSample: 32
BlockAlign: 4
Channels: 1
Encoding: IeeeFloat
ExtraSize: 0
SampleRate: 48000
averageBytesPerSecond: 192000
bitsPerSample: 32
blockAlign: 4
channels: 1
extraSize: 0
sampleRate: 48000
waveFormatTag: IeeeFloat

このオリジナルのNAudio.dllは、Releaseビルドすれば、NAudio\bin\Release\uap10.0フォルダに出力されます。
自分のUWPプロジェクトでnugetではなくからそのDLLを参照するように切り替えれば、.NETネイティブコンパイルしたUWPアプリも正常にNAudioが動作します。
どうせならばと、この件は本家にプルリクしてみました。