LoginSignup
3
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-05-13

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

環境

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

実装

ソースコード
録音時間(秒数)をしてして、ボタン押下で録音開始。
録音完了後、ファイルダイアログを表示し録音データをWAVE形式のファイルとして保存する。
image.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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3