カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

Win32APIを用いたウェーブデータ再生(.NET C#バージョン)

このセクションでは、C#.NET環境下でウェーブデータを再生する手順について解説します。用いるAPIは、伝統的なWin32APIです。

このセクションは再生方法について解説しています。ウェーブファイルを読み込む方法については、セクション「ウェーブファイルを読み込む(.NET C#バージョン)」を参照してください。
マネージ環境からWin32APIをコールするためには、マーシャリングの知識が欠かせません。wave関数とマーシャリングに関するトピック「プラットフォーム呼び出しとマーシャリングに関する話題」がありますので、合せてご覧ください。

デバイスをオープンする

waveOutOpen関数を用いてウェーブデバイスをオープンします。このサンプルでは、コールバックウィンドウ方式を用いているため、4番目のパラメータにはウィンドウハンドルを設定しています。

    public bool OpenDevice(
        int                 deviceID,
        IntPtr              hWnd,
        ref WaveFormatEx    waveFormat)

    {
        if (HWaveOut != IntPtr.Zero)        return false;

        int ret = waveOutOpen(
            ref HWaveOut,
            deviceID,
            ref waveFormat,
            hWnd,
            0,
            Callback.Window);

        return (ret == MmSysErr.NoError) ? true : false;
    }

以下にwaveOutOpen関数の宣言を示します。

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

hWaveOut

アンマネージ環境では「HWAVEOUT」で示されるウェーブデバイスのハンドルです。マネージ環境では、汎用ポインター「IntPtr」を用います。オープン関数では、ハンドルを取得すために参照を示す「ref」キーワードが必要です。

再生が終了するまで、ハンドルを保持する必要があるため、以下のプロパティを用います。

    public IntPtr           HWaveOut = IntPtr.Zero;

uDeviceID

デバイスを識別するID番号です。ゼロからwaveOutGetNumDevs関数で取得した数値-1までの整数を指定することができます。また「WaveMapper」(値は-1)を指定すると、システムが警告音などを再生するときに用いるデフォルトデバイスを指定することができます。

lpFormat

これから再生するウェーブデータのフォーマットへの参照です。ウェーブデータをロードしたとき取得したものを使います。

dwCallback

再生が済んだウェーブヘッダーは、アプリケーションに戻されます。このとき用いる関数やハンドルなどを設定します。ここで設定するパラメータは、コールバック方式によって異なります。

このサンプルでは、ウィンドウメッセージをフォームに送信するコールバックウィンドウ方式を取っているので、dwCallbackにはフォームのウィンドウハンドルを設定します。

dwInstance

追加で必要になるパラメータを設定します。コールバックウィンドウ方式では用いません。

dwFlags

どのコールバック方式を用いるかを指定します。以下に定数を定義したコード例を示します。

    public class Callback
    {
        public const int    Null     = 0x00000000;      // no callback
        public const int    Window   = 0x00010000;      // dwCallback is a HWND
        public const int    Task     = 0x00020000;      // dwCallback is a HTASK
        public const int    Function = 0x00030000;      // dwCallback is a FARPROC
        public const int    Thread   = Task;            // thread ID replaces 16 bit task
        public const int    Event    = 0x00050000;      // dwCallback is an EVENT Handle
    }

デバイスをクローズする

デバイスをクローズするには、演奏が停止している必要があるので、安全のためStopPlayをコールしています。デバイスがクローズしたことを示すために、HWaveOutをnullでクリアしています。

    public bool CloseDevice()
    {
        if (HWaveOut == IntPtr.Zero)    return true;

        StopPlay();

        int ret = waveOutClose(HWaveOut);
        HWaveOut = IntPtr.Zero;

        return (ret == MmSysErr.NoError) ? true : false;
    }

以下にwaveOutClose関数の宣言を示します。

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

WaveHdrを準備する

以下にWaveHdrを準備する手順を示します。

WaveHdrは、アンマネージ環境では構造体として実装されていますが、このプログラムでは、クラスとして実装しています。
    public WaveHdr PrepareBuff()
    {
        byte[] waveBuff = new byte[WaveBuffSize];
        int readSize = LoadWave.ReadData(ref waveBuff);
        if (readSize == 0)      return null;

        WaveHdr waveHdr = new WaveHdr();
        WaveHdrList.Add(waveHdr);

        GCHandle gch = GCHandle.Alloc(waveBuff, GCHandleType.Pinned);
        waveHdr.lpData = gch.AddrOfPinnedObject();
        waveHdr.dwBufferLength = readSize;
        waveHdr.dwUser = GCHandle.ToIntPtr(gch);
        waveHdr.dwFlags = WHFlags.None;
        waveHdr.dwLoops = 0;

        int ret = waveOutPrepareHeader(HWaveOut, waveHdr, Marshal.SizeOf(typeof(WaveHdr)));
        return (ret == MmSysErr.NoError) ? waveHdr : null;
    }

ウェーブデータの読み込みは、LoadWaveというヘルパークラスを用いています。LoadWave.ReadData関数は、バッファの参照を受け取り、読み込んだデータサイズを戻します。

WaveHdrListは、準備したWaveHdrクラスを保存するためのリストです。以下にコード例を示します。

    public List<WaveHdr>    WaveHdrList { get; set; }

マネージ環境でアロケートしたバッファは、直接Win32APIに渡すことができないため、GCHandleクラスを用いてバッファアドレスを取得します。GCHandleオブジェクトは、バッファの後処理に必要なため、waveHdr.dwUserに保存しておきます。

waveOutPrepareHeader関数では、WaveHdrのサイズを必要とします。C#では、構造体(クラス)のサイズを取得するために、Marshal.SizeOf(タイプ)を用います。WaveHdrのサイズは以下のコードで求めることができます。

Marshal.SizeOf(typeof(WaveHdr))

以下にwaveOutPrepareHeader関数の宣言を示します。

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

再生を開始する

WaveHdrの準備が整ったら、再生デバイスに書き込むことができます。StartPlay関数では、NumWaveBuff個のバッファを準備し、順次デバイスに書き込みます。waveOutWrite関数は、WaveHdrを受け取ると直ちに再生を始めます。

PrepPageでデバイスに書き込まれたバッファの数を管理します。バッファを書き込むときプラス1し、再生が終わったバッファが戻ったときマイナス1します。最後のバッファが戻ったとき、つまりすべての再生が終わったとき、PrepPageはゼロになります。

    public bool StartPlay()
    {
        if (HWaveOut == IntPtr.Zero)    return false;

        int ret = MmSysErr.NoError;

        PrepPage = 0;

        for (int i=0; i<NumWaveBuff; i++)
        {
            WaveHdr waveHdr = PrepareBuff();
            if (waveHdr == null)            break;

            ret = waveOutWrite(HWaveOut, waveHdr, Marshal.SizeOf(typeof(WaveHdr)));
            if (ret != MmSysErr.NoError)    break;

            ++PrepPage;
        }
        return (ret == MmSysErr.NoError) ? true : false;
    }

WaveHdrの後処理を行う

WaveHdrがアプリケーションに戻ると、waveOutUnprepareHeader関数で後処理を行い、バッファを開放します。

いくつかフラグのチェックを行っていますが、これはどんな状態のWaveHdrにも対応するためのもので、MmWomDoneメッセージで戻ったWaveHdrであれば、省略しても構いません。

固定していたウェーブバッファをここで開放します。そしてWaveHdrListからwaveHdrを削除します。

    public bool UnprepareBuff(
        IntPtr      hWaveOut,
        WaveHdr     waveHdr)
    {
        if ((waveHdr.dwFlags & WHFlags.Done) == 0)
        {
            return false;           // まだ演奏中である
        }
        if ((waveHdr.dwFlags & WHFlags.Prepared) == 0)
        {
            return false;           // 準備されたバッファではない
        }
        int ret = waveOutUnprepareHeader(
            hWaveOut, waveHdr, Marshal.SizeOf(typeof(WaveHdr)));

        GCHandle gch = GCHandle.FromIntPtr(waveHdr.dwUser);
        gch.Free();
        WaveHdrList.Remove(waveHdr);

        return (ret == MmSysErr.NoError) ? true : false;
    }

演奏を継続する

ContPlay関数の役割は、演奏が終わったWaveHdrの後処理を行い、次のWaveHdrをデバイスに書き込むことです。演奏すべきデータが残っている間は、PrepareBuff関数がWaveHdrを戻すので、それをデバイスに書き込みます。

    public bool ContPlay(
        IntPtr      hWaveOut,
        WaveHdr     waveHdr)
    {
        if (UnprepareBuff(hWaveOut, waveHdr) == false)      return false;
        --PrepPage;

        waveHdr = PrepareBuff();
        if (waveHdr == null)            return false;

        int ret = waveOutWrite(hWaveOut, waveHdr, Marshal.SizeOf(typeof(WaveHdr)));
        if (ret != MmSysErr.NoError)    return false;

        ++PrepPage;
        return true;
    }

演奏を停止する

演奏の停止には、一時停止と終了の二つがありますが、waveOutReset関数は、終了コマンドです。この関数を実行すると演奏は停止し、すべてのWaveHdrがアプリケーションに戻されます。

いずれMmWomDoneメッセージ経由でWaveHdrが戻るので、UnprepareBuff関数で後処理を行うことができます。しかしStopPlay関数では、MmWomDoneメッセージを待たずにその場で後処理を行っています。これは、急にアプリケーションを終了させなければならない状況でも、確実にWaveHdrの後処理を行うためです。

この場合、後処理の終わったWaveHdrがMmWomDoneメッセージで戻ってきます。ここで2重に後処理を行わないよう、処理済みヘッダーは無視する必要があります。
    public void StopPlay()
    {
        if (HWaveOut == null)           return;

        int ret = waveOutReset(HWaveOut);
        if (ret != MmSysErr.NoError)    return;

        foreach (WaveHdr waveHdr in WaveHdrList)
        {
            waveOutUnprepareHeader(HWaveOut, waveHdr, Marshal.SizeOf(typeof(WaveHdr)));
            GCHandle gch = GCHandle.FromIntPtr(waveHdr.dwUser);
            gch.Free();
        }
        WaveHdrList.Clear();
        PrepPage = 0;

        LoadWave.SeekPosition(0);
    }

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ