PCのマイクから音声を取得し、WAVE形式のファイルとして保存する。
##環境
Windows 10
visual studio 2019
C#
Win32API(WinMM)
WPF(32bit)※WinMMが32bit対応のため
##実装
ソースコード
録音時間(秒数)をしてして、ボタン押下で録音開始。
録音完了後、ファイルダイアログを表示し録音データをWAVE形式のファイルとして保存する。
###Win32API(WinMM)
- waveInOpen 関数
入力のデバイスをオープンする。
戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。
[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」に設定する値。
public const int CALLBACK_WINDOW = 0x10000;
public const int CALLBACK_FUNCTION = 0x30000;
定数名 | 概要 |
---|---|
CALLBACK_WINDOW | Windowハンドルで受け取る場合 |
CALLBACK_FUNCTION | 通常の関数コールバック |
通常の関数コールバックで受ける場合は、下記のように「dwCallback」をデリゲートにしてもいい。
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 | 今回使用されていない |
コールバックメッセージ
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 構造体
録音時のデータ形式を設定する。
[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)」の場合、正常終了。
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInClose(IntPtr hwi);
引数名 | 概要 |
---|---|
hwi | 入力デバイスのハンドル |
- waveInPrepareHeader 関数
録音データを格納する「WaveHdr構造体」を入力デバイスで使用できるよう準備する。戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。
[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)」の場合、正常終了。
[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)」の場合、正常終了。
[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 構造体
[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)」の場合、正常終了。
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInStart(IntPtr hwi);
引数名 | 概要 |
---|---|
hwi | 入力デバイスのハンドル |
- waveInStop 関数
入力デバイスからの録音をストップする。
戻り値が「MMSYSERR_NOERROR(0)」の場合、正常終了。
[DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int waveInStop(IntPtr hwi);
引数名 | 概要 |
---|---|
hwi | 入力デバイスのハンドル |
###マイク入力開始
- 入力デバイスのオープン
// コールバック関数のデリゲートを作成
_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」はガーベージコレクタによって破棄されないようにする。ここではクラス変数を想定。
- 録音開始
録音は非同期で動作する。
// 録音データを書き込むバッファを作成
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);
###入力データ待受
非同期でメッセージ受信
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データ作成
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