mciSendStringで音が鳴らない???

タグの編集
投稿者 あせたけ  (社会人) 投稿日時 2020/10/21 20:35:37
お世話になります。
VSコミュニティ2019、Windows10の環境です。
VB.NETにてソフト開発を行っていますが、
元々音が鳴っているクラスをそのまま使用しようと思い、
実績のあるクラスを流用しています。

今回、マルチスレッドで処理させようと思い、
その部分から来た問題点だとは思っているのですが、どう解決して良いのかわかりません^^;

★元々音がなっていた作成済のアプリケーション
  ・ソフト全体がシングルスレッド

★今回音が鳴らない作成中のアプリケーション
 ・イベント処理からのマルチスレッド処理

参考ソース

呼出元
--------------------------------------------------------------------
Public Class Main
 Inhertits Sound

   Public Sub New()
   sound.sound(snd.Aleart)   '...←これは音が鳴る
 End Sub

  (~中略~)

 Private Sub OutputEventsCatch(ByVal sender As Object, ByVal e As LCEventArgs) Handles Snr.AleartEvent
     sound.sound(snd.Aleart)   '...←これが音が鳴らない…
 End Sub
End Class
--------------------------------------------------------------------

呼出先(ベースクラス:Sound)
--------------------------------------------------------------------
Public Class Sound
    Inherits File

    Private Const className As String = "Sound"

    <System.Runtime.InteropServices.DllImport("winmm.dll", CharSet:=System.Runtime.InteropServices.CharSet.Auto)>
    Private Shared Function mciSendString(ByVal command As String, ByVal buffer As System.Text.StringBuilder, ByVal bufferSize As Integer, ByVal hwndCallback As IntPtr) As Integer

    End Function

    Public Declare Function mciGetErrorString Lib "winmm.dll" Alias "mciGetErrorStringA" (ByVal fdwError As Integer, ByVal lpszErrorText As String, ByVal cchErrorText As Integer) As Integer

    ''' <summary>
    ''' 音声ファイル操作用
    ''' </summary>
    Private sound_Alert_Name As String = "Alert"
    Private sound_Warning_Name As String = "Warning"

   (~中略~)

    ''' <summary>
    ''' 音声鳴動処理
    ''' </summary>
    ''' <param name="s">音声の種類 [Enum]snd</param>
    Public Sub Sound(ByVal s As snd)
        Dim FileName As String = ""
        Dim aliasName As String = ""

        Dim Result As Integer

        If s = snd.None Then
            AleartSoundOff()
        ElseIf s = snd.Aleart Then
            FileName = sound_Alert
            aliasName = sound_Alert_Name
        ElseIf s = snd.Warning Then
            FileName = sound_Warning
            aliasName = sound_Warning_Name
        End If

        Dim cmd As String
        'ファイルを開く
        cmd = "open """ + FileName + """ type mpegvideo alias " + aliasName
        Result = mciSendString(cmd, Nothing, 0, IntPtr.Zero)
        If Result <> 0 Then
            'エラーメッセージを取得して表示します。
            Dim ErrorMessage As String = GetErrorString(Result)
            MsgBox(ErrorMessage, MsgBoxStyle.Exclamation, "Error1 " & Result)
        End If
        '再生する
        cmd = "play " + aliasName + " repeat"
        Result = mciSendString(cmd, Nothing, 0, IntPtr.Zero)
        If Result <> 0 Then
            'エラーメッセージを取得して表示します。
            Dim ErrorMessage As String = GetErrorString(Result)
            MsgBox(ErrorMessage, MsgBoxStyle.Exclamation, "Error2 " & Result)
        End If
    End Sub

    Private Sub AleartSoundOff()

        Dim cmd As String

        '停止する
        cmd = "stop all"
        mciSendString(cmd, Nothing, 0, IntPtr.Zero)
        '閉じる
        cmd = "close all"
        mciSendString(cmd, Nothing, 0, IntPtr.Zero)
    End Sub
End Class
--------------------------------------------------------------------

解決方法のヒントだけでも頂ければと思い、投稿させて頂きます。


※ 実現させたい事
 外部の(多数の)接点入力装置の情報をLCEventAugsにてイベントで入手している。
   (ここまでは正常に情報入手出来ている)
 上記の入手イベントで異常があった際(入力があった際)に音を鳴らしたい。
   (ここで音が鳴らない。上記参照)

※ mciSendString関数のエラーメッセージを確認する
https://www.umayadia.com/vbsample/dotnet-Samples151/Sample188GetErrorString.htm?pos=5900
による情報
ErrNo.266
 指定されたデバイス ドライバーの読み込み中に不明な問題が発生しました。
ErrNo.263
 指定されたデバイスが開かれていないか、または MCI で認識されません。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/10/21 20:56:31
> 今回、マルチスレッドで処理させようと思い、
> その部分から来た問題点だとは思っているのですが、

内容は精査していないのですが、下記の記事はどうでしょうか。
VB でも日本語でも無いですが、なんだか関係ありそうなことが記されているように見えます。

https://stackoverflow.com/questions/3597681/method-doesnt-work-when-executed-in-a-thread-but-works-otherwise-c-sharp
https://www.codeproject.com/Questions/183548/How-to-use-mciSendString-in-a-threaded-application
投稿者 あせたけ  (社会人) 投稿日時 2020/10/21 21:26:42
早速のご回答ありがとうございます。

参考になりそうな記事と感じました。
見てみます。
投稿者 るきお  (社会人) 投稿日時 2020/10/23 08:17:42
魔界の仮面弁士さんの情報で解決するかもしれませんが思ったことを書きます。

今回マルチスレッドで実行すると再生できないということなので、サンプルとしてはそのマルチスレッドで再生できないサンプルを投稿していただければ少なくともこちらで試すくらいのことはできます。
場合によっては、魔界の仮面弁士さんが紹介しているリンク先の解決方法をそれに適用してみることもやるかもしれません。

投稿いただいたプログラムが実行できないのもであったので、欠けている部分を補って、とにかく実行できるようにしようと少し作業し始めたのですが、肝心のマルチスレッド部分に関しては皆無でしたので実行を断念しました。

他の人がすぐ試せるとそれだけ解決につながる情報を得られる可能性が高くなります。そして、そのような簡単に試せる状態を作り出す過程で自分で解決方法を発見することもたくさんありますので、問題を再現できる最小限のプログラムを作るというようにアプローチされるのも良いなと思いました。

※技術的な中身としては私の第一印象はリンク先の意見とだいたい同じです。つまり、UIスレッドでしか実行できない処理なのではないかなと思いました。
投稿者 あせたけ  (社会人) 投稿日時 2020/10/24 12:19:59
ご意見ありがとうございます。

現在こちらでチェックしている項目としては、
https://www.codeproject.com/Questions/183548/How-to-use-mciSendString-in-a-threaded-application%20%E7%AE%A1%E7%90%86
のmciTestクラスを作成、mciTestクラスのバグ修正、
mciReplacement(ByVal Command As String) に上の『音声処理鳴動』のcmd変数の結果を投げてみましたが、音が鳴りませんでした。

説明がなかなかややこしいのですが、とりあえず概要を…

Public Class Class1
  Privete LCS as new List(Of Class2)

    Public Event LCEvents(sender As Object, e As LCEventArgs)

    Public Sub LCEvent(sender As Object, e As LCEventAugs)
        _RaiseEvent LCEvents(sender, e)
    End sub
End Class


Public Class2
    Private objLCS As Class1

    Private Polling_Thread As New System.Threading.Thread(AddressOf Polling_Exec)

    Public Sub New(ByVal objLCS As Class1)
    Me.ObjLCS = objLCS
        Try
            If Not Connection Then
                Task.Run(Sub()
                             Open()
                         End Sub)
            End If
            Polling_Thread.Name = "Lan Converter " & LCIndex
            Polling_Thread.Start()
        Catch e As Exception

        End Try
    End Sub

    Public Overridable Sub Open() Implements LCInterface.Open
        Connection = sckOpen()
        Send("W", 0)
    End Sub

    Private Function sckOpen() As Boolean Implements LCInterface.sckOpen
        Try
            If Me.Enabled Then
                Me.objSck.Connect(Me.IP, Me.PORT)
                Me.objStm = objSck.GetStream()
                Return True
            Else
                Return False
            End If
        Catch ex As Exception
            objLCS.LCEvent(Me, New LCEventArgs(Connect:=False, ErrNo:=LCEventArgs.ErrNoState.未接続, ErrDiscription:="接続できませんでした"))
            Return False
        End Try
    End Function

    Private Sub Polling_Exec() Implements LCInterface.Polling_Exec
        Do
            Send()
            Recieve()
            System.Threading.Thread.Sleep(Polling_Interval)
        Loop
    End Sub
   
    Private Sub Send()
        (状態を取得するコマンド送信)
  End Sub

  Private Sub Recieve()
   (状態を受信する)
   If (状態変化があった時) then
     ObjLCS.LCEvent(Me,New LCEventAugs (InputData:= InputData))
   End If
    End Sub
End Class

概要としてはこの様な感じでしょうか…
投稿者 あせたけ  (社会人) 投稿日時 2020/10/24 12:22:39
概要で抜けていますが、
Class1で多数のClass2をリストにAddして、
Class1の方でClass2のイベントをまとめて投げています。
投稿者 あせたけ  (社会人) 投稿日時 2020/10/24 12:40:09
> UIスレッドでしか実行できない処理なのではないか

ちょっと調べてみました。

参考ソース

呼出元
--------------------------------------------------------------------
Public Class Main
 Inhertits Sound

   Public Sub New()
   sound.sound(snd.Aleart)   '...←これは音が鳴る・・・・・①
 End Sub

  (~中略~)

 Private Sub OutputEventsCatch(ByVal sender As Object, ByVal e As LCEventArgs) Handles Snr.AleartEvent
     sound.sound(snd.Aleart)   '...←これが音が鳴らない…・・・・・②
 End Sub
End Class
--------------------------------------------------------------------

①はメインスレッドで処理されています。
②は(概要で言う)クラス2のスレッドで処理されています。


②をメインスレッドで処理させるにはどうしたら良いのか?と言うのが鍵となりそうです…
投稿者 あせたけ  (社会人) 投稿日時 2020/10/25 21:01:36
自己解決しました。

元々、スタートアップフォームであるForm1より、Mainクラスを呼出し、Mainクラスでメインの処理を担当、
前出、(概要のClass2)をマルチスレッド化して、イベントをMainクラスに投げ、Mainクラスで音を鳴らす予定でした。

(要点のみ)

※変更前
----------------------------------------------------------------------------------
Public Class Form1
    private sub Form1_Load(ByVal sender As Object, ByVal e As eventAugs) Handles Mybase.Load
        Dim m As New Main(Me)
    End Sub
End Class


Public Class Main
    Inherits Sound

    Public Property f As Form1
    Private WithEvents lcs As New LanConverter.LanConverters()
    Private WithEvents Sensers As New SenserS()
    Private Settings As New Settings()
    Private Olc As New List(Of String)

    Private Sub New(ByVal f as form)
        Me.f = f
    End Sub

    Private Sub OutputEventsCatch(ByVal sender As Object, ByVal e As LanConverter.LCEventArgs) Handles Sensers.AleartEvent

        Select Case e.InputStatus
            Case SenserInterface.SenserInputStatus.通常
                Sound(snd.None)
            Case SenserInterface.SenserInputStatus.発報
                Sound(snd.Warning)
            (・・・省略・・・)
         End Select
    End Sub
End Class
----------------------------------------------------------------------------------
投稿者 あせたけ  (社会人) 投稿日時 2020/10/25 21:04:35
魔界の仮面弁士さん、るきおさんにヒントを頂き、
るきおさんの
> UIスレッドでしか実行できない処理なのではないか
が決めてとなり、Form1に1度処理を戻し、デリゲート処理する事で音が鳴りました。

※変更後
----------------------------------------------------------------------------------
Public Class Form1
    private sub Form1_Load(ByVal sender As Object, ByVal e As eventAugs) Handles Mybase.Load
        Dim m As New Main(Me)
    End Sub

    Public Sub soundPlay(ByVal snd As Sound.snd)
        Sound.SoundCheck(Me, snd)
    End Sub
End Class


End Class

Public Class Main
    Public Property f As Form1
    Private WithEvents lcs As New LanConverter.LanConverters()
    Private WithEvents Sensers As New SenserS()
    Private Settings As New Settings()
    Private Olc As New List(Of String)

    Private Sub New(ByVal f as form)
        Me.f = f
    End Sub

    Private Sub OutputEventsCatch(ByVal sender As Object, ByVal e As LanConverter.LCEventArgs) Handles Sensers.AleartEvent

        Select Case e.InputStatus
            Case SenserInterface.SenserInputStatus.通常
                f.soundPlay(Sound.snd.None)
            Case SenserInterface.SenserInputStatus.発報
                f.soundPlay(Sound.snd.Warning)
            (・・・省略・・・)
          End Select
    End Sub

Public Class Sound
    Delegate Sub SoundDelegate(ByVal f As Form1, ByVal s As snd)

    Public Sub SoundCheck(ByVal f As Form1, ByVal s As snd)
        If f.InvokeRequired Then
            f.Invoke(New SoundDelegate(AddressOf SoundCheck), f, s)
            Return
        End If
        Sound(s)
    End Sub

    (Soundメソッドは前出)
End Class
----------------------------------------------------------------------------------

何かあまり納得は行っていませんが、とりあえず音は鳴りましたので、ヨシとします…^^;

私感
 大きなマルチスレッドプログラムは初めて組みますが、マルチスレッドプログラミングは難しい…^^;
 メインスレッド? ワーカースレッド? フォアグラウンドスレッド? バックグラウンドスレッド?
 もっと精進せねば…

ありがとうございました。
投稿者 るきお  (社会人) 投稿日時 2020/10/26 08:23:01
解決してよかったです。

マルチスレッドは難しいです。私は避けられるなら避けます。
例外処理やスレッド間の連携や、今回のようなスレッドアフィニティ(≒特定のスレッドで実施させたい)の問題。
こういった問題を解決するためにSyncronizationContextやExecutionContextなどの仕組みもあります。

System.Threading.Threadはマルチスレッドの機能としては古く、2008年ごろだったかにはTPL(System.Threading.Tasks,Task)が登場しています。その後、さらに簡単で非同期実行を実現する高機能な言語機能としてAyncとAwaitが登場しました。
どうしてもマルチスレッッド/非同期処理が必要な場合は、新しい機能から検討されると良いと思います。

私の知っている限りでは言語機能としてここまで強力なマルチスレッド機能があるのはVBとC#くらいではないかと思います。

下記の本は.NETのマルチスレッドについて体系的に説明してくれていてお勧めです。

プログラミング C#
https://www.amazon.co.jp/exec/obidos/ASIN/4873116503/vbschool-22



C#の本ですが、.NET FrameworkについてはVBもC#も同じですし、言語機能である Async, AwaitにつてもVBとC#で同じで参考になります。
この本はマルチスレッドだけの本ではありませんが、第17章 マルチスレッドが49ページ、第18章 非同期言語処理が20ページあり、かなり読み応えがあります。