WaveOut API (低レベルAPI) を利用して WAVファイルのサウンドを再生する - C#

WaveOut API を利用して WAVファイルのサウンドを再生するコードを紹介します。
 

WaveOutを使う利点

WaveOut APIを利用すると、直接波形データにアクセスできますので、波形データを加工してサウンドデバイスに送信できます。
サウンドファイルを再生するだけの場合はSystem.Media.SoundPlayerを用いたほうが簡単に実装できます。System.Media.SoundPlayerを利用したサウンド再生はこちらの記事を参照してください。

プログラム

UI

下図のUIを作成します。

コード

下記のコードを記述します。
FormMain.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace LowLevelWavePlayBuffered
{
  public partial class FormMain : Form
  {
    private iPentecLowLevelWavePlay llwp = new iPentecLowLevelWavePlay();

    public FormMain()
    {
      InitializeComponent();
    }

    private void button_FileOpen_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
        llwp.OpenFile(openFileDialog1.FileName);
      }
    }

    private void button_FileClose_Click(object sender, EventArgs e)
    {
      llwp.CloseFile();
    }

    private void button_Play_Click(object sender, EventArgs e)
    {
      llwp.Play();
    }

    private void button_Stop_Click(object sender, EventArgs e)
    {
      llwp.Stop();
    }
  }
}
iPentecLowLevelWavePlay.cs (ライブラリ部)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.IO;

namespace LowLevelWavePlayBuffered
{
  public enum MMRESULT
  {
    MMSYSERR_NOERROR = 0,
    MMSYSERR_ERROR = (MMSYSERR_NOERROR + 1),
    MMSYSERR_BADDEVICEID = (MMSYSERR_NOERROR + 2),
    MMSYSERR_NOTENABLED = (MMSYSERR_NOERROR + 3),
    MMSYSERR_ALLOCATED = (MMSYSERR_NOERROR + 4),
    MMSYSERR_INVALHANDLE = (MMSYSERR_NOERROR + 5),
    MMSYSERR_NODRIVER = (MMSYSERR_NOERROR + 6),
    MMSYSERR_NOMEM = (MMSYSERR_NOERROR + 7),
    MMSYSERR_NOTSUPPORTED = (MMSYSERR_NOERROR + 8),
    MMSYSERR_BADERRNUM = (MMSYSERR_NOERROR + 9),
    MMSYSERR_INVALFLAG = (MMSYSERR_NOERROR + 10),
    MMSYSERR_INVALPARAM = (MMSYSERR_NOERROR + 11),
    MMSYSERR_HANDLEBUSY = (MMSYSERR_NOERROR + 12),
    MMSYSERR_INVALIDALIAS = (MMSYSERR_NOERROR + 13),
    MMSYSERR_BADDB = (MMSYSERR_NOERROR + 14),
    MMSYSERR_KEYNOTFOUND = (MMSYSERR_NOERROR + 15),
    MMSYSERR_READERROR = (MMSYSERR_NOERROR + 16),
    MMSYSERR_WRITEERROR = (MMSYSERR_NOERROR + 17),
    MMSYSERR_DELETEERROR = (MMSYSERR_NOERROR + 18),
    MMSYSERR_VALNOTFOUND = (MMSYSERR_NOERROR + 19),
    MMSYSERR_NODRIVERCB = (MMSYSERR_NOERROR + 20),
    MMSYSERR_MOREDATA = (MMSYSERR_NOERROR + 21),
    MMSYSERR_LASTERROR = (MMSYSERR_NOERROR + 21)
  }

  public enum WAVE_FORMAT
  {
    WAVE_FORMAT_1M08 = 0x00000001,
    WAVE_FORMAT_1S08 = 0x00000002,
    WAVE_FORMAT_1M16 = 0x00000004,
    WAVE_FORMAT_1S16 = 0x00000008,
    WAVE_FORMAT_2M08 = 0x00000010,
    WAVE_FORMAT_2S08 = 0x00000020,
    WAVE_FORMAT_2M16 = 0x00000040,
    WAVE_FORMAT_2S16 = 0x00000080,
    WAVE_FORMAT_4M08 = 0x00000100,
    WAVE_FORMAT_4S08 = 0x00000200,
    WAVE_FORMAT_4M16 = 0x00000400,
    WAVE_FORMAT_4S16 = 0x00000800
  }

  public enum WAVEHDR_FLAG
  {
    WHDR_DONE = 0x00000001,
    WHDR_PREPARED = 0x00000002,
    WHDR_BEGINLOOP = 0x00000004,
    WHDR_ENDLOOP = 0x00000008,
    WHDR_INQUEUE = 0x00000010
  }

  public enum TIME_TYPE
  {
    TIME_MS = 0x0001,
    TIME_SAMPLES = 0x0002,
    TIME_BYTES = 0x0004,
    TIME_SMPTE = 0x0008,
    TIME_MIDI = 0x0010,
    TIME_TICKS = 0x0020
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct WAVEFORMATEX
  {
    public ushort wFormatTag;
    public ushort nChannels;
    public uint nSamplesPerSec;
    public uint nAvgBytesPerSec;
    public ushort nBlockAlign;
    public ushort wBitsPerSample;
    public ushort cbSize;
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct WAVEHDR
  {
    public IntPtr lpData;
    public uint dwBufferLength;
    public uint dwBytesRecorded;
    public uint dwUser;
    public uint dwFlags;
    public uint dwLoops;
    public IntPtr lpNext;
    public uint reserved;
  }

  [StructLayout(LayoutKind.Explicit)]
  public struct MMTIME
  {
    [FieldOffset(0)]
    public uint wType;
    [FieldOffset(4)]
    public uint ms;
    [FieldOffset(4)]
    public uint sample;
    [FieldOffset(4)]
    public uint cb;
    [FieldOffset(4)]
    public uint ticks;
    [FieldOffset(4)]
    public ushort hour;
    [FieldOffset(6)]
    public ushort min;
    [FieldOffset(8)]
    public ushort sec;
    [FieldOffset(10)]
    public ushort frame;
    [FieldOffset(12)]
    public ushort fps;
    [FieldOffset(14)]
    public ushort dummy;
    [FieldOffset(16)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
    public ushort[] pad;
    [FieldOffset(4)]
    public uint songptrpos;
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct WAVEINCAPS
  {
    public ushort wMid;
    public ushort wPid;
    public uint vDriverVersion;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    public char[] szPname;
    uint dwFormats;
    ushort wChannels;
    ushort wReserved1;
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct WAVEOUTCAPS
  {
    public ushort wMid;
    public ushort wPid;
    public uint vDriverVersion;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    public char[] szPname;
    public uint dwFormats;
    public ushort wChannels;
    public ushort wReserved1;
    public uint dwSupport;
  }

  public enum WaveOutMessage
  {
    Close = 0x3BC,
    Done = 0x3BD,
    Open = 0x3BB
  }


  public class iPentecLowLevelWavePlay
  {
    private const uint WAVE_MAPPER = unchecked((uint)(-1));

    private const uint CALLBACK_NULL = 0x00000000; /* no callback */
    private const uint CALLBACK_WINDOW = 0x00010000; /* dwCallback is a HWND */
    private const uint CALLBACK_TASK = 0x00020000; /* dwCallback is a HTASK */
    private const uint CALLBACK_FUNCTION = 0x00030000; /* dwCallback is a FARPROC */

    [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern MMRESULT waveOutOpen(ref IntPtr hWaveOut, uint uDeviceID, ref WAVEFORMATEX lpFormat, WaveOutProc dwCallback, IntPtr dwInstance, uint dwFlags);

    [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern MMRESULT waveOutClose(IntPtr hwo);

    [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern MMRESULT waveOutPrepareHeader(IntPtr hWaveOut, IntPtr lpWaveOutHdr, int uSize);

    [DllImport("winmm.dll")]
    public static extern MMRESULT waveOutUnprepareHeader(IntPtr hWaveOut, IntPtr lpWaveOutHdr, uint cbwh);

    [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern MMRESULT waveOutReset(IntPtr hwo);

    [DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern MMRESULT waveOutWrite(IntPtr hwo, IntPtr pwh, uint cbwh);

    //callbacks
    public delegate void WaveOutProc(IntPtr hdrvr, WaveOutMessage uMsg, int dwUser, IntPtr wavhdr, int dwParam2);

    private IntPtr WaveOutHandle;
    private IntPtr WaveHandlePtr;

    private WAVEFORMATEX WaveFormatEx;
    private byte[] WaveDataBuffer;

    private FileStream fs;
    private BinaryReader br;

    private WaveOutProc m_BufferProc;
    private int BufferSize=0;

    public iPentecLowLevelWavePlay()
    {
      WaveOutHandle = IntPtr.Zero;
    }

    public void OpenFile(string FileName)
    {
      fs = new FileStream(FileName, FileMode.Open);
      br = new BinaryReader(fs, Encoding.ASCII);

      char[] readbuf;
      readbuf = br.ReadChars(4);

      if (readbuf.ToString() == "RIFF") {
      }
      br.ReadBytes(4);//空

      readbuf = br.ReadChars(4);
      if (readbuf.ToString() == "WAVE") {
      }

      readbuf = br.ReadChars(4);
      if (readbuf.ToString() == "fmt ") {
      }

      // 'fmt 'チャンクのサイズ読み取り
      int chank = br.ReadInt32();

      // ヘッダを読み取り waveformatex構造体に代入     
      byte[] WaveFormatExBuffer = br.ReadBytes(Marshal.SizeOf(typeof(WAVEFORMATEX)) - Marshal.SizeOf(typeof(ushort)) * 2);
      GCHandle gch = GCHandle.Alloc(WaveFormatExBuffer, GCHandleType.Pinned);
      WaveFormatEx = (WAVEFORMATEX)Marshal.PtrToStructure(gch.AddrOfPinnedObject(), typeof(WAVEFORMATEX));
      gch.Free();
      WaveFormatEx.cbSize = (ushort)Marshal.SizeOf(typeof(WAVEFORMATEX));

      readbuf = br.ReadChars(4);
      if (readbuf.ToString() == "fact") {
        //'fact'チャンクのサイズの読み込み
        br.ReadBytes(4);
        br.ReadBytes(1);
        br.ReadBytes(4);
      }

      if (readbuf.ToString() == "data") {
      }

      BufferSize = (int)WaveFormatEx.nSamplesPerSec * (int)WaveFormatEx.nBlockAlign;
      OpenWaveOutHandle();
      WaveHandlePtr = PrepareHeader();
    }

    public void CloseFile()
    {
      br.Close();
      fs.Close();

      Marshal.FreeHGlobal(WaveHandlePtr);

      if (WaveOutHandle != IntPtr.Zero) {
        waveOutReset(WaveOutHandle);
        waveOutUnprepareHeader(WaveOutHandle, WaveHandlePtr, (uint)Marshal.SizeOf(typeof(WAVEHDR)));
        waveOutClose(WaveOutHandle);
      }
    }
    
    public void Play()
    {
      MMRESULT rc = waveOutWrite(WaveOutHandle, WaveHandlePtr, (uint)Marshal.SizeOf(typeof(WAVEHDR)));
    }

    public void Stop()
    {
      if (WaveOutHandle != IntPtr.Zero) {
        MMRESULT mr = waveOutReset(WaveOutHandle);
        if (mr == MMRESULT.MMSYSERR_NOERROR) {
        }
        mr = waveOutClose(WaveOutHandle);
        if (mr == MMRESULT.MMSYSERR_NOERROR) {
        }
        WaveOutHandle = IntPtr.Zero;
      }
    }

    public void OpenWaveOutHandle()
    {
      if (WaveOutHandle != IntPtr.Zero) {
        MMRESULT mr = waveOutReset(WaveOutHandle);
        if (mr == MMRESULT.MMSYSERR_NOERROR) {
        }
        mr = waveOutClose(WaveOutHandle);
        if (mr == MMRESULT.MMSYSERR_NOERROR) {
        }
        WaveOutHandle = IntPtr.Zero;
      }

      m_BufferProc = new WaveOutProc(WaveOutProcCallback);
      MMRESULT rc = waveOutOpen(ref WaveOutHandle, WAVE_MAPPER, ref WaveFormatEx, m_BufferProc, IntPtr.Zero, CALLBACK_FUNCTION);
    }

    //データをフォーマットヘッダに組み込み ヘッダの最終構成を行う
    public IntPtr PrepareHeader()
    {
      WAVEHDR WaveHeader = new WAVEHDR();
      IntPtr WaveHdrPtr = new IntPtr();
      WaveHdrPtr = Marshal.AllocHGlobal(Marshal.SizeOf(WaveHeader));

      WaveDataBuffer = br.ReadBytes((int)BufferSize);

      IntPtr parray = Marshal.AllocCoTaskMem(WaveDataBuffer.Length);
      Marshal.Copy(WaveDataBuffer, 0, parray, WaveDataBuffer.Length);

      WaveHeader.lpData = parray;
      WaveHeader.dwBufferLength = (uint)WaveDataBuffer.Length;

      WaveHeader.dwFlags = 0;
      Marshal.StructureToPtr(WaveHeader, WaveHdrPtr, true);

      MMRESULT mmrc = waveOutPrepareHeader(WaveOutHandle, WaveHdrPtr, Marshal.SizeOf(typeof(WAVEHDR)));
      
      return WaveHdrPtr;
    }

    public void WaveOutProcCallback(IntPtr hdrvr, WaveOutMessage uMsg, int dwUser, IntPtr wavhdr, int dwParam2)
    {
      switch (uMsg) {
        case WaveOutMessage.Close:
          break;
        case WaveOutMessage.Done:

          WaveDataBuffer = br.ReadBytes((int)BufferSize);

          if (0 < WaveDataBuffer.Length) {
            WAVEHDR wh = new WAVEHDR();

            wh = (WAVEHDR)Marshal.PtrToStructure(wavhdr, typeof(WAVEHDR));

            //読み込みサイズがバッファサイズを下回る場合はメモリ再確保 (Wavの終端)
            if (WaveDataBuffer.Length < BufferSize) {
              Marshal.FreeCoTaskMem(wh.lpData);
              wh.lpData = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(byte)) * WaveDataBuffer.Length);
              wh.dwBufferLength = (uint)WaveDataBuffer.Length;
            }

            Marshal.Copy(WaveDataBuffer, 0, wh.lpData, WaveDataBuffer.Length);
            Marshal.StructureToPtr(wh, wavhdr, true);

            MMRESULT rc = waveOutWrite(WaveOutHandle, (IntPtr)wavhdr, (uint)Marshal.SizeOf(typeof(WAVEHDR)));
          }
          break;
        case WaveOutMessage.Open:
          break;
      }
    }
  }
}

解説

ファイルの読み込み

最初にOpenFile()メソッドで、Wavファイルを開きます。ファイルの読み込みはBinaryReaderクラスを利用します。ファイルの先頭にはRIFFヘッダなどがありますので、それらを読み取り正しいWAVファイルか確認します。WAVファイルの構造は次の通りです。
名称バイト数
Chunk ID4"RIFF"
Chunk Size44
Format4"WAVE"
Subchunk 1 ID4"fmt "
Subchunk 1 Size4
FormatTag2多くの場合 1
Channels2ほとんどの場合 2
SamplesPerSec4多くの場合 44100
AverageBytesPerSec4多くの場合 176400
BlockAlign2多くの場合 4
Subchunk 2 ID4"data"
Subchunk 2 Size4
Audio data(Subchunk 2 Size)

Extra Parameterがある場合

ExtraParameterがある場合は以下の構造の場合があります。
名称バイト数
Chunk ID4"RIFF"
Chunk Size44
Format4"WAVE"
Subchunk 1 ID4"fmt "
Subchunk 1 Size4
FormatTag2多くの場合 1
Channels2ほとんどの場合 2
SamplesPerSec4多くの場合 44100
AverageBytesPerSec4多くの場合 176400
BlockAlign(Extra ParamSize)2(ExtraParameterのサイズが入る)
ExtraParameter
Subchunk 2 ID4"data"
Subchunk 2 Size4
Audio data(Subchunk 2 Size)

例:factチャンクの場合

名称バイト数
Chunk ID4"RIFF"
Chunk Size44
Format4"WAVE"
Subchunk 1 ID4"fmt "
Subchunk 1 Size4
FormatTag2多くの場合 1
Channels2ほとんどの場合 2
SamplesPerSec4多くの場合 44100
AverageBytesPerSec4多くの場合 176400
BlockAlign(Extra ParamSize)2
FACT Chunk4"fact"
FACT Chunk Size4(factチャンクのバイト数)
全サンプル数4
Subchunk 2 ID4"data"
Subchunk 2 Size4
Audio data(Subchunk 2 Size)
今回のコードでは、ExtraParameterがない場合か、factのExtra Parameter がある場合のみを想定したコードになっています。
WAVEファイルを読み込み、ヘッダの情報を取得してWAVEFORMATEX 構造体の値を設定します。

ウェーブフォームオーディオ出力デバイスを開く

続いて、waveOutOpen() Windows APIを呼び出し、ウェーブフォームオーディオ出力デバイスを開きます。コールバックが必要な場合は、waveOutOpen呼び出し時にコールバック関数のポインタを与え、第6引数にCALLBACK_FUNCTIONを与えます。また、先に設定したWAVEFORMATEX構造体の値も与えます。成功するとオーディオデバイスのハンドルが返ります。

ヘッダの準備

waveOutPrepareHeader() Windows APIを呼び出し、ヘッダを準備します。waveOutPrepareHeader() にはwaveOutOpen()で開いたオーディオデバイスのハンドルと、WAVEHDR構造体の変数を与えます。WAVEHDRにはオーディオデバイスで再生するデータとその長さを設定します。

再生

waveOutWrite() Windows APIでWAVEHDRの情報をオーディオデバイスに送信しサウンドを再生します。設定後、waveOutWrite()を呼び出し再生を継続します。

次のバッファの読み込み

再生が完了すると、WaveOutProcCallback()メソッドがコールバックされます。次のWAVデータを読み込み、WaveOutProcCallbackの引数で与えられるWAVHDRのlpDataにデータを設定します。設定後waveOutWrite() を呼び出して次の部分を再生します。
ファイルの末尾はバッファがすべて埋まらないため、バッファサイズを小さくしたバッファを再作成しています。別の実装方法として、末尾以降を0で埋める方法もあります。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


ウィンドウの[FileOpen]ボタンをクリックします。ファイルを開くダイアログが表示されます。Wavファイルを選択して開きます。


[Play]ボタンをクリックします。サウンドが再生されます。

注意

上記のコードはバッファサイズが小さいため、実際に再生をすると音飛びが頻繁にあります。実用のコードではバッファサイズを大きくするか、ファイル全部をバッファに読み込んだほうが再生品質は向上します。

著者
iPentec.com の代表。ハードウェア、サーバー投資、管理などを担当。
Office 365やデータベースの記事なども担当。
掲載日: 2015-04-08
iPentec all rights reserverd.