DOBON.NET DOBON.NETプログラミング掲示板過去ログ

Win32APIのwaveInXXXXのトラブル

環境/言語:[WinXP // VB.NET 1.1 (VS2003)]
分類:[.NET]

PCのオーディオ入力を、オシロスコープのように描画するアプリを計画しています。
下のURLのサイトとMSDNを参考にプログラムを書いているのですが、音声データの取得が
うまくいかなくて困っています。(具体的な現象は後述します)

http://black.sakura.ne.jp/~third/system/winapi/mm7.html


とりあえず、困っている現象が再現できる(と思われる)最低限のコードを提示します。

音声データを取得するクラスのコードです。ウィンドウコールバックを使うためダミーの
フォームになっているので、メインになるフォームのほかに適当な名前のフォームをプロジェクトに
追加して、そこに記述してください。(下のサンプルでは、cWaveInFormという名前の
フォームクラスになっています。)

少々長いですがお許しを。

Imports System.Runtime.InteropServices
Imports System.Threading

Public Class cWaveInForm
    Inherits System.Windows.Forms.Form

     <StructLayout(LayoutKind.Explicit)> Public Structure WAVEHDR
        <FieldOffset(0)> Public lpData As IntPtr
        <FieldOffset(4)> Public dwBufferLength As Integer
        <FieldOffset(8)> Public dwBytesRecorded As Integer
        <FieldOffset(12)> Public dwUser As IntPtr
        <FieldOffset(16)> Public dwFlags As Integer
        <FieldOffset(20)> Public dwLoops As Integer
        <FieldOffset(24)> Public lpNext As IntPtr
        <FieldOffset(28)> Public Reserved As IntPtr
    End Structure

    <StructLayout(LayoutKind.Explicit)> Public Structure WAVEFORMATEX
        <FieldOffset(0)> Public wFormatTag As Short
        <FieldOffset(2)> Public nChannels As Short
        <FieldOffset(4)> Public nSamplePerSecond As Integer
        <FieldOffset(8)> Public nAvgBytesPerSec As Integer
        <FieldOffset(12)> Public nBlockAilgn As Short
        <FieldOffset(14)> Public wBitsPerSample As Short
        <FieldOffset(16)> Public cbSize As Short
    End Structure

    Private Declare Function waveInOpen Lib "winmm" (ByRef phwi As IntPtr, _
                                                    ByVal uDeviceID As Integer, _
                                                    ByRef pwfx As WAVEFORMATEX, _
                                                    ByVal dwCallBack As IntPtr, _
                                                    ByVal dwCallbackInstance As Integer, _
                                                    ByVal fdwOpen As Integer) As Integer

    Private Declare Function waveInPrepareHeader Lib "winmm" (ByVal hwi As IntPtr, _
                                                    ByRef pwh As WAVEHDR, _
                                                    ByVal cbwh As Integer) As Integer

    Private Declare Function waveInUnprepareHeader Lib "winmm" (ByVal hwi As IntPtr, _
                                                    ByRef pwh As WAVEHDR, _
                                                    ByVal cbwh As Integer) As Integer

    Private Declare Function waveInAddBuffer Lib "winmm" (ByVal hwi As IntPtr, _
                                                    ByRef pwh As WAVEHDR, _
                                                    ByVal cbwh As Integer) As Integer

    Private Declare Function waveInStart Lib "winmm" (ByVal hwi As IntPtr) As Integer

    Private Declare Function waveInStop Lib "winmm" (ByVal hwi As IntPtr) As Integer

    Private Declare Function waveInReset Lib "winmm" (ByVal hwi As IntPtr) As Integer

    Private Declare Function waveInClose Lib "winmm" (ByVal hwi As IntPtr) As Integer

    Public Enum eSampleRate
        F44100 = 44100
        F22050 = 22050
        F11025 = 11025
    End Enum

    Public mSampleRate As eSampleRate = eSampleRate.F44100
    Public Const BitRate As Integer = 8

    '音声データを他のオブジェクトに渡すイベントを発生させる一秒当たりの回数の目標値
    Private Const RoughFrameRate As Single = 20

    'RoughFrameRateに近い値レートで(バッファがいっぱいになって)
    'MM_WIM_DATAメッセージがくるようなバッファのサイズを決定。
    Private mBufferSize As Integer = CInt(mSampleRate / RoughFrameRate)

    Private mFrameRate As Single = mSampleRate / mBufferSize
    Private mWaveBufferHeader1, mWaveBufferHeader2 As WAVEHDR
    Private mWaveBuffer1, mWaveBuffer2 As IntPtr

    Private mDeviceHandle As IntPtr
    Private mWaveFormat As WAVEFORMATEX
    Private mIsWaveInDeviceOpened As Boolean = False

    '他のオブジェクトに渡される音声データのキャッシュ
    Private mMyOutPut As Byte()

    '音声データを他のオブジェクトに渡すイベント
    Public Event WaveDataArrive(ByVal Wave As Byte())

― 続く ―
― 続き ―
    'バッファ領域の確保(更新)
    Private Sub Update_BufferSize()
        mWaveBuffer1 = Marshal.AllocHGlobal(mBufferSize)
        mWaveBuffer2 = Marshal.AllocHGlobal(mBufferSize)
        ReDim mMyOutPut(mBufferSize)
    End Sub

    'waveInOpenが引数にとるWAVEFORMATEX構造体のメンバの設定(更新)
    Private Sub Update_WaveFormat()
        Const WAVE_FORMAT_PCM As Short = 1S

        With mWaveFormat
            .wFormatTag = WAVE_FORMAT_PCM
            .nChannels = 1S
            .nSamplePerSecond = mSampleRate
            .nAvgBytesPerSec = .nSamplePerSecond
            .wBitsPerSample = 8S
            .nBlockAilgn = 1S
            .cbSize = 0S
        End With
    End Sub

    '音声データ取得開始
    Public Sub OpenDevice()
        If mIsWaveInDeviceOpened = True Then Exit Sub

        With mWaveBufferHeader1
            .lpData = mWaveBuffer1
            .dwBufferLength = mBufferSize
            .dwBytesRecorded = 0
            .dwFlags = 0
            .dwLoops = 1
            .lpNext = IntPtr.Zero
            .dwUser = IntPtr.Zero
            .Reserved = IntPtr.Zero
        End With

        With mWaveBufferHeader2
            .lpData = mWaveBuffer2
            .dwBufferLength = mBufferSize
            .dwBytesRecorded = 0
            .dwFlags = 0
            .dwLoops = 1
            .lpNext = IntPtr.Zero
            .dwUser = IntPtr.Zero
            .Reserved = IntPtr.Zero
        End With

        Dim Result As Integer
        Const CALLBACK_WINDOW As Integer = &H10000
        Const WAVE_MAPPER As Integer = &HFFFFFFFF

        Result = waveInOpen(mDeviceHandle, WAVE_MAPPER, mWaveFormat, Me.Handle, 0, CALLBACK_WINDOW)
        Result = waveInPrepareHeader(mDeviceHandle, mWaveBufferHeader1, Marshal.SizeOf(mWaveBufferHeader1))
        Result = waveInPrepareHeader(mDeviceHandle, mWaveBufferHeader2, Marshal.SizeOf(mWaveBufferHeader2))

    End Sub

    Public Sub CloseDevice()
        If mIsWaveInDeviceOpened = False Then Exit Sub

        Call waveInReset(mDeviceHandle)
        Call waveInClose(mDeviceHandle)
    End Sub

    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        Const MM_WIM_OPEN As Integer = &H3BE
        Const MM_WIM_CLOSE As Integer = &H3BF
        Const MM_WIM_DATA As Integer = &H3C0
        Const MMSYSERR_NOERROR As Integer = 0

        Dim Dbg As Integer

        Select Case m.Msg
            Case MM_WIM_OPEN
                Dim Result As Integer = 100

                mIsWaveInDeviceOpened = True
                Result = waveInAddBuffer(mDeviceHandle, mWaveBufferHeader1, Marshal.SizeOf(mWaveBufferHeader1))
                Result = waveInAddBuffer(mDeviceHandle, mWaveBufferHeader2, Marshal.SizeOf(mWaveBufferHeader2))
                Result = waveInStart(mDeviceHandle)

            Case MM_WIM_DATA
                Dim Temp As WAVEHDR
                Dim Result As Integer
                Static DataCount As Integer = 0

                DataCount += 1 : Console.WriteLine("DataCount = " & DataCount.ToString)

                Temp = m.GetLParam(GetType(WAVEHDR))
                If Temp.lpData.ToInt32 = mWaveBufferHeader1.lpData.ToInt32 Then
                    '開放されたバッファはmWaveBuffer1
                    Marshal.Copy(mWaveBuffer1, mMyOutPut, 0, mBufferSize)
                    Result = waveInAddBuffer(mDeviceHandle, mWaveBufferHeader1, Marshal.SizeOf(mWaveBufferHeader1))
                    If Result = MMSYSERR_NOERROR Then
                        Console.WriteLine("Add Buffer1 OK!")
                    Else
                        Console.WriteLine("Add Buffer1 Falied ! = " & Result.ToString & "Count = " & DataCount.ToString)
                    End If

                ElseIf Temp.lpData.ToInt32 = mWaveBufferHeader2.lpData.ToInt32 Then
                    '開放されたバッファはmWaveBuffer1
                    Marshal.Copy(mWaveBuffer2, mMyOutPut, 0, mBufferSize)
                    Result = waveInAddBuffer(mDeviceHandle, mWaveBufferHeader2, Marshal.SizeOf(mWaveBufferHeader2))
                    If Result = MMSYSERR_NOERROR Then
                        Console.WriteLine("Add Buffer2 OK!")
                    Else
                        Console.WriteLine("Add Buffer2 Falied ! = " & Result.ToString & "Count = " & DataCount.ToString)
                    End If

                Else
                    '受信したメッセージが指示するバッファのポインタは不正
                    Console.WriteLine("Invalid Pointer !")
                End If

                RaiseEvent WaveDataArrive(mMyOutPut)

            Case MM_WIM_CLOSE
                mIsWaveInDeviceOpened = False
                waveInUnprepareHeader(mDeviceHandle, mWaveBufferHeader1, Marshal.SizeOf(mWaveBufferHeader1))
                waveInUnprepareHeader(mDeviceHandle, mWaveBufferHeader2, Marshal.SizeOf(mWaveBufferHeader2))

            Case Else


        End Select

        MyBase.WndProc(m)

    End Sub

    Public Sub New()
        MyBase.New()

        ' この呼び出しは Windows フォーム デザイナで必要です。
        InitializeComponent()

        ' InitializeComponent() 呼び出しの後に初期化を追加します。
        Call Update_BufferSize()
        Call Update_WaveFormat()

    End Sub

    ' Form は、コンポーネント一覧に後処理を実行するために dispose をオーバーライドします。
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        Marshal.FreeHGlobal(mWaveBuffer1)
        Marshal.FreeHGlobal(mWaveBuffer2)

        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub


#Region " Windows フォーム デザイナで生成されたコード "  
     〜 略 〜
#End Region

End Class
― 続き ―

メインのフォームでcWaveInFormのインスタンスを作成したあと、
コマンドボタンなど適当なタイミングでOpenDeviceメソッドをコールすると、
音声データの取得を開始し、状況を出力ウィンドウに報告します。

それで、困っている点とは

● データの取得を開始してしばらくすると、MM_WIM_DATAメッセージで
  OSから返されたバッファを再びキューに追加するwaveInAddBufferが
  WAVERR_STILLPLAYING( = 33 )を返して失敗します。(ちなみにこのエラーコードは、
  「バッファがまだキューにある」という意味らしいのですが、MSDNのwaveInAddBufferの
  解説には、この関数が返す値として記述されていません。)

● メインのフォームなど他のオブジェクトでWaveDataArriveイベントを受信して
  引数として渡されるデータを使って何らかの処理を行うと、これを行わなかった
  時に比べてwaveInAddBufferが失敗するタイミングが目に見えて早くなります。


気づいた点などありましたらご教授よろしくお願いします。
なお、そのほか判明していることを書いてみます。

○ 音声データ取得開始時にコールされるwaveInOpen、waveInPrepareHeader及び
  その直後のMM_WIM_OPEN受信時にコールされるwaveInAddBuffer、waveInStartは
  すべてMMSYSERR_NOERROR(成功)を返し、また取得されたデータを画面にオシロスコープ状に
  描画するコードを書いてみると、正しい音声の波形になっているようなので、構造体の定義や
  APIの宣言が不適切である可能性は低いと思います。

○ 定数RoughFrameRateにもっと小さな値(例えば2)を設定してもやはり同じ問題がおこるので、
  キューに入れるバッファのサイズが小さすぎる可能性は低いと思います。

○ サンプリングレート(mSampleRate)は、低い値を設定した時の方が、waveInAddBufferが
  失敗するタイミングが遅れるようです。

○ WndProcでRaiseEventしているのがまずいのかとも思ったのですが、
  これを削除してもやはりそのうちwaveInAddBufferが失敗します。

○ MM_WIM_DATA受信時、WndProcを抜ける際に参考にしたサイトのサンプル通り
  MyBase.WndProc(m)の代わりにm.Result = IntPtr.Zeroとしても結果は変わらず。
■No5317に返信(CRTさんの記事)

 CRTさん、こんにちは。深山と申します。
 パッと見の印象だけなので、的外れかも知れませんけど‥‥

・WndProc 内の各メッセージ処理が終わった後に Return していない(MyBase に流れている)
・OpenDevice 内で設定している dwBufferLength の値が元のコードと異なる

というのが気になりました。
 これらは特に問題ないのでしょうか?
■No5319に返信(深山さんの記事)

こんにちは、返信ありがとうございます。

> ・WndProc 内の各メッセージ処理が終わった後に Return していない(MyBase に流れている)
> ・OpenDevice 内で設定している dwBufferLength の値が元のコードと異なる
> 
> というのが気になりました。
>  これらは特に問題ないのでしょうか?

前者については、実は私はいわゆる「Windowsプログラミング」の経験がないので
参考にさせてもらったオリジナルのCコードのWndProcの中のReturn 0の意味が
あまりよくわからないのですが、.NETでこれに相当する処理をするには、おそらく
WndProcを抜けるときにMyBase.WndProc(m)の代わりにm.Result = IntPtr.Zeroを
実行してやればいいのではないかと思います。

http://www.microsoft.com/japan/msdn/library/default.asp?url=/japan/msdn/library/ja/cpref/html/frlrfsystemwindowsformsmessageclassresulttopic.asp

で、実際やってみると結果に違いはないようでした。


後者については、コードの書き方が違うだけで意味的には同じだと思いますので、
恐らく問題ないのではないかと思います。(いま確認できる環境が手元にないので......。)
2004/08/03(Tue) 21:07:20 編集(投稿者)

■No5321に返信(CRTさんの記事)

 CRTさん、こんばんは。深山です。

>>・WndProc 内の各メッセージ処理が終わった後に Return していない(MyBase に流れている)
> 前者については、実は私はいわゆる「Windowsプログラミング」の経験がないので
> 参考にさせてもらったオリジナルのCコードのWndProcの中のReturn 0の意味が
> あまりよくわからないのですが、.NETでこれに相当する処理をするには、おそらく
> WndProcを抜けるときにMyBase.WndProc(m)の代わりにm.Result = IntPtr.Zeroを
> 実行してやればいいのではないかと思います。

 はい。戻り値に関する扱いについては仰る通り、 m.Result に設定してあげれば大丈夫です。
 私が言ったのはそういうことではなくて‥‥えっと、 return 0 とすることで戻り値を
設定するのと同時に、関数から抜け出すというのはご存知ですよね?
# 失礼な質問かと思いますが、『「Windowsプログラミング」の経験がない』だけでは
# 何の言語の知識をどの程度お持ちなのか判らないもので(汗)


>>・OpenDevice 内で設定している dwBufferLength の値が元のコードと異なる
> 後者については、コードの書き方が違うだけで意味的には同じだと思いますので、
> 恐らく問題ないのではないかと思います。(いま確認できる環境が手元にないので......。)

 元コードでは SRATE ( 11025 ) を設定してる箇所で、 mBufferSize
( mSampleRate / mBufferSize = 44100 / 20 = 2205 ) を設定してるように見受けられたので。
勘違いでしたらごめんなさいm(__)m


■追記‥‥したものが勘違いだったので削除。<ご迷惑をお掛けしますm(__)m
■No5324に返信(深山さんの記事)

>  はい。戻り値に関する扱いについては仰る通り、 m.Result に設定してあげれば大丈夫です。
>  私が言ったのはそういうことではなくて‥‥えっと、 return 0 とすることで戻り値を
> 設定するのと同時に、関数から抜け出すというのはご存知ですよね?
WndProc内のSelect...CaseのMM_WIM_OPEN、MM_WIM_DATA、MM_WIM_CLOSEの各ブロックの末尾に、

m.Result = IntPtr.Zero
Exit Sub

を付け加えたコードで試していますが、やはり同じようにwaveInAddBufferが
失敗する問題が発生するようでした。


>  元コードでは SRATE ( 11025 ) を設定してる箇所で、 mBufferSize
>  ( mSampleRate / mBufferSize = 44100 / 20 = 2205 ) を設定してるように見受けられたので。
ああ、2つのWAVEHDR構造体のdwBufferLengthメンバに設定している値が、件のサイトの
サンプルコードの値と違うというご指摘ですね。よく読まずに早とちりしたようですみません。

MSDNによると、単にバッファのサイズをバイト単位で指定するだけのようですので、
実際に確保した領域より大きな値を指定してさえいなければ、たぶん問題ないのでは
ないでしょうか?

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/multimed/htm/_win32_wavehdr_str.asp

実際、件のサンプルコードと条件を全て同じにしたものも確認していますが、
やはり同じように問題が発生するようでした。
■No5325に返信(CRTさんの記事)

 CRTさん、こんばんは。深山です。
 あー、やっぱり全然的外れだったみたいですね(汗) お役に立てず申し訳ないですm(__)m

 その後こちらでもCRTさんのコードを動かしてみました。なるほど、 waveInAddBuffer の戻り値が
33 になることがあるようですね。ただ、その発生条件が判らない。
 何度も動かしてみましたが、カウントが 400 いかずに発生することもあれば、 1000 を軽く
越しても問題ないこともあったり‥‥なんなのでしょう(ーー;)

#  片方のバッファだけ失敗した時には継続して動作するのに、両方連続して失敗すると
# それ以上 MM_WIM_DATA メッセージがこなくなるし‥‥って、これは私がこの辺の仕組を
# よく理解してないから不思議に感じるだけなのでしょうけど(^_^;)


 バッファの確保の仕方に問題があるとか、今のところそれくらいしか思いつきません。
それすらも特に根拠があるわけではないというのがなんですが‥‥。
 もう少し調べてみたいと思います。

DOBON.NET | プログラミング道 | プログラミング掲示板