C#  マイク入力・WAVEファイル保存 Win32API

PCのマイクから音声を取得し、WAVE形式のファイルとして保存する。

##環境
Windows 10
visual studio 2019
C#
Win32API(WinMM)
WPF（32bit）※WinMMが32bit対応のため

##実装

[ソースコード](https://github.com/Yuki4-dev/Audio_Sample)
録音時間（秒数）をしてして、ボタン押下で録音開始。
録音完了後、ファイルダイアログを表示し録音データをWAVE形式のファイルとして保存する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688992/3762e7d0-4a78-ed73-730c-d71a4a4fe1d2.png)

###Win32API(WinMM)
+ waveInOpen　関数

入力のデバイスをオープンする。
戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
public static extern int waveInOpen(ref IntPtr hwi, int uDeviceID, ref WaveFormatEx wfx, IntPtr dwCallback, IntPtr dwCallbackInstance, int fdwOpen);
```

|引数名|概要|
|:--|:--|
|hwi|オープンした入力デバイスのハンドルが格納される。|
|uDeviceID|オープンする入力デバイスの識別子を指定する。「WAVE_MAPPER(-1)」を指定するとデフォルトの入力デバイスを指定できる。|
|wfx|「WaveFormatEx構造体」|
|dwCallback|入力デバイスからのコールバックを受け取るハンドルを設定する。|
|dwCallbackInstance|コールバック時に受け取ることができるインスタンスを設定する。必要ない場合は0を設定する。|
|fdwOpen|コールバックを受け取るハンドルの種類を指定する。|

waveInOpen関数の引数「fdwOpen」に設定する値。

```NativeMethods.cs
public const int CALLBACK_WINDOW = 0x10000;
public const int CALLBACK_FUNCTION = 0x30000;
```

|定数名|概要|
|:--|:--|
|CALLBACK_WINDOW |Windowハンドルで受け取る場合|
|CALLBACK_FUNCTION |通常の関数コールバック|

通常の関数コールバックで受ける場合は、下記のように「dwCallback」をデリゲートにしてもいい。

```NativeMethods.cs
public delegate void DelegateWaveInProc(IntPtr hwi, uint uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2);

[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
public static extern int waveInOpen(ref IntPtr hwi, int uDeviceID, ref WaveFormatEx _wfx, DelegateWaveInProc dwCallback, IntPtr dwCallbackInstance, int fdwOpen);
```
関数コールバックの引数の説明

|引数名|概要|
|:--|:--|
|hwi|オープンした入力デバイスのハンドル|
|uMsg|コールバックメッセージ|
|dwInstance|waveInOpen関数で指定したインスタンス|
|dwParam1|録音された「WaveHdr構造体」|
|dwParam2|今回使用されていない|

コールバックメッセージ

```NativeMethods.cs
public const int WIM_OPEN = 0x3BE;
public const int WIM_CLOSE = 0x3BF;
public const int WIM_DATA = 0x3C0;
```
|定数名|概要|
|:--|:--|
|WIM_OPEN |入力デバイスがオープンした場合|
|WIM_CLOSE |入力デバイスがクローズした場合|
|WIM_DATA |バッファに貯まった録音データがいっぱいになった場合|

+ WaveFormatEx　構造体

録音時のデータ形式を設定する。

```NativeMethods.cs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WaveFormatEx
{
    public Int16 wFormatTag;
    public Int16 nChannels;
    public int nSamplesPerSec;
    public int nAvgBytesPerSec;
    public Int16 nBlockAlign;
    public Int16 wBitsPerSample;
    public Int16 cbSize;
}
```
|変数名|概要|
|:--|:--|
|wFormatTag|データ形式|
|nChannels|チャンネル数|
|nSamplesPerSec|サンプリング周波数|
|nAvgBytesPerSec|1秒間あたりのバイト数|
|nBlockAlign|1サンプルあたりのバイト数|
|wBitsPerSample|ビット数|
|cbSize|拡張データのバイト数|

+ waveInClose　関数

入力デバイスをクローズする。戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInClose(IntPtr hwi);
```

|引数名|概要|
|:--|:--|
|hwi|入力デバイスのハンドル|

+ waveInPrepareHeader　関数

録音データを格納する「WaveHdr構造体」を入力デバイスで使用できるよう準備する。戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInPrepareHeader(IntPtr hwi, ref WaveHdr wh, int cbwh);
```

|引数名|概要|
|:--|:--|
|hwi|入力デバイスのハンドル|
|wh|「WaveHdr構造体」|
|cbwh|「WaveHdr構造体」のサイズ|

+ waveInUnprepareHeader　関数

waveInPrepareHeader関数で準備した「WaveHdr構造体」を開放する。録音中、C#では「WaveHdr構造体」がガーベージコレクタによって自動で開放されないようする必要がある。戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInUnprepareHeader(IntPtr hwi, ref WaveHdr wh, int cbwh);
```

|引数名|概要|
|:--|:--|
|hwi|入力デバイスのハンドル|
|wh|「WaveHdr構造体」|
|cbwh|「WaveHdr構造体」のサイズ|

+ waveInAddBuffer　関数

録音データを書き込むバッファ「WaveHdr構造体」を入力デバイスに追加する。戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInAddBuffer(IntPtr hwi, ref WaveHdr wh, int cbwh);
```
|引数名|概要|
|:--|:--|
|hwi|入力デバイスのハンドル|
|wh|「WaveHdr構造体」|
|cbwh|「WaveHdr構造体」のサイズ|

+ WaveHdr　構造体

```NativeMethods.cs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WaveHdr
{
    public IntPtr lpData;
    public int dwBufferLength;
    public int dwBytesRecorded;
    public IntPtr dwUser;
    public int dwFlags;
    public int dwLoops;
    public IntPtr lpNext;
    public int reserved;
}
```
|変数名|概要|
|:--|:--|
|lpData|録音データ格納バッファ|
|dwBufferLength|バッファサイズ|
|dwBytesRecorded|データのバイト数|
|dwUser|ユーザーが使用できる領域|
|dwFlags|バッファの状態を示すフラグ|
|dwLoops|データ出力時の入力ループ回数を示す。|
|lpNext|予約語|
|reserved|予約語|

+ waveInStart　関数

入力デバイスからの録音をスタートする。
戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInStart(IntPtr hwi);
```
|引数名|概要|
|:--|:--|
|hwi|入力デバイスのハンドル|

+ waveInStop　関数
入力デバイスからの録音をストップする。
戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。

```NativeMethods.cs
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInStop(IntPtr hwi);
```
|引数名|概要|
|:--|:--|
|hwi|入力デバイスのハンドル|

###マイク入力開始

+ 入力デバイスのオープン

```MicSoundRecorder.cs
// コールバック関数のデリゲートを作成
_WaveProc = new NativeMethods.DelegateWaveInProc(WaveInProc);

// データ形式を設定する。
_WaveFormat = new NativeMethods.WaveFormatEx();
_WaveFormat.wFormatTag = NativeMethods.WAVE_FORMAT_PCM; // リニア PCM 
_WaveFormat.cbSize = 0;
_WaveFormat.nChannels = 1;
_WaveFormat.nSamplesPerSec = 11025;
_WaveFormat.wBitsPerSample = 8;
_WaveFormat.nBlockAlign = (short)(_WaveFormat.wBitsPerSample / 8 * _WaveFormat.nChannels);
_WaveFormat.nAvgBytesPerSec = _WaveFormat.nSamplesPerSec * _WaveFormat.nBlockAlign;

// 入力デバイスオープン
var result = NativeMethods.waveInOpen(ref _Hwi, NativeMethods.WAVE_MAPPER, ref _WaveFormat, _WaveProc, IntPtr.Zero, NativeMethods.CALLBACK_FUNCTION);

```
「_WaveProc 」「_WaveFormat」「_Hwi」はガーベージコレクタによって破棄されないようにする。ここではクラス変数を想定。

+ 録音開始

録音は非同期で動作する。

```MicSoundRecorder.cs
// 録音データを書き込むバッファを作成
var dataSize = _WaveFormat.nAvgBytesPerSec * recordSecond;
_WaveHdr = new NativeMethods.WaveHdr();
_WaveHdr.lpData = Marshal.AllocHGlobal(dataSize); // メモリを確保
_WaveHdr.dwBufferLength = dataSize;

// バッファを準備・追加
var cdwh = Marshal.SizeOf<NativeMethods.WaveHdr>();
NativeMethods.waveInPrepareHeader(_Hwi, ref _WaveHdr, cdwh);
NativeMethods.waveInAddBuffer(_Hwi, ref _WaveHdr, cdwh);

// 録音スタート
NativeMethods.waveInStart(_Hwi);
```

###入力データ待受

非同期でメッセージ受信

```MicSoundRecorder.cs
public void WaveInProc(IntPtr hwi, uint uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2)
{
    switch (uMsg)
    {
        case NativeMethods.WIM_OPEN:

            // オープン
            WaveOpen?.Invoke(this, EventArgs.Empty);
            break;
        case NativeMethods.WIM_CLOSE:

            // クローズ
            WaveClose?.Invoke(this, EventArgs.Empty);
            break;
        case NativeMethods.WIM_DATA:

            // var wh = Marshal.PtrToStructure<NativeMethods.WaveHdr>(dwParam1);
            // WAVEデータ
            OnWaveData();
            break;
        default:
            break;
    }
}
```

###WAVEファイル作成

録音データにWAVEヘッダを添付して、外部でも再生できるようにする。

+ WAVEヘッダ

WAVEヘッダサイズは44バイト
最高4GBまでのファイルを扱うことができる。

|項目名|設定値|サイズ|
|:--|:--|:--|
|RIFF識別子|“RIFF”固定|4バイト|
|チャンクサイズ|全体のファイルサイズ(ヘッダ＋録音データサイズ)-8バイト|4バイト|
|フォーマット|“WAVE”固定|4バイト|
|fmt識別子|“fmt “固定。|4バイト|
|fmtチャンクのバイト数|16+拡張バイト(リニア PCMの場合0)|4バイト|
|データ形式|1（リニア PCM）|2バイト|
|チャンネル数|1（モノラル）、2（ステレオ）|2バイト|
|サンプリング周波数|例　11025|4バイト|
|1 秒あたりバイト数の平均|例　11025|4バイト|
|1サンプルあたりのバイト数|例　8|2バイト|
|ビット数|例　8|2バイト|
|サブチャンク識別子|“data”固定|4バイト|
|サブチャンクサイズ|録音データサイズ|4バイト|

+ WAVEデータ作成

```MicSoundRecorder.cs
private void OnWaveData()
{
    // WAVEデータをコピーするバイト配列を作成
    var headerSize = 44;
    var dataSize = _WaveHdr.dwBufferLength + headerSize;
    var waveData = new byte[dataSize];

    // WAVEヘッダを設定
    Array.Copy(Encoding.ASCII.GetBytes("RIFF"), 0, waveData, 0, 4);
    Array.Copy(BitConverter.GetBytes((uint)(dataSize - 8)), 0, waveData, 4, 4);
    Array.Copy(Encoding.ASCII.GetBytes("WAVE"), 0, waveData, 8, 4);
    Array.Copy(Encoding.ASCII.GetBytes("fmt "), 0, waveData, 12, 4);
    Array.Copy(BitConverter.GetBytes((uint)16), 0, waveData, 16, 4);
    Array.Copy(BitConverter.GetBytes((ushort)(_WaveFormat.wFormatTag)), 0, waveData, 20, 2);
    Array.Copy(BitConverter.GetBytes((ushort)(_WaveFormat.nChannels)), 0, waveData, 22, 2);
    Array.Copy(BitConverter.GetBytes((uint)(_WaveFormat.nSamplesPerSec)), 0, waveData, 24, 4);
    Array.Copy(BitConverter.GetBytes((uint)(_WaveFormat.nAvgBytesPerSec)), 0, waveData, 28, 4);
    Array.Copy(BitConverter.GetBytes((ushort)(_WaveFormat.nBlockAlign)), 0, waveData, 32, 2);
    Array.Copy(BitConverter.GetBytes((ushort)(_WaveFormat.wBitsPerSample)), 0, waveData, 34, 2);
    Array.Copy(Encoding.ASCII.GetBytes("data"), 0, waveData, 36, 4);
    Array.Copy(BitConverter.GetBytes((uint)(_WaveHdr.dwBufferLength)), 0, waveData, 40, 4);
    Marshal.Copy(_WaveHdr.lpData, waveData, headerSize, _WaveHdr.dwBufferLength);

    // バッファを開放
    NativeMethods.waveInUnprepareHeader(_Hwi, ref _WaveHdr, Marshal.SizeOf<NativeMethods.WaveHdr>());

    // メモリ開放
    Marshal.FreeHGlobal(_WaveHdr.lpData);

    WaveData?.Invoke(this, waveData);
}
```

##参考

https://qiita.com/kob58im/items/aa6c7a4dc80946dbe3a7
https://github.com/ttsuki/ttsuki/blob/master/WinMM/WaveIO.cs
http://wisdom.sakura.ne.jp/system/winapi/media/mm7.html
http://eternalwindows.jp/winmm/wave/wave04.html
https://www.katto.comm.waseda.ac.jp/~katto/Class/01/GazoTokuron/code/audiocapture.html
https://kana-soft.com/tech/sample_0010_2.htm#WAVEFORMATEX

+ マイクロソフト

https://docs.microsoft.com/en-us/previous-versions/dd743849(v=vs.85)
https://docs.microsoft.com/ja-jp/windows/win32/api/mmeapi/ns-mmeapi-wavehdr
https://docs.microsoft.com/ja-jp/windows/win32/api/mmeapi/ns-mmeapi-waveformatex

+ WAVEヘッダに関して

https://tomosoft.jp/design/?p=9107
https://www.youfit.co.jp/archives/1418