NauZeated
NauZeated

Reputation: 21

VB.net - Simultaneous Audio

So I'm working on a simple VB.net game for school, in which you pop bubbles. We need to have a sound play when you pop a bubble, which is very simple with the audio play function;

Private Sub bubblePop(sender As Object, e As EventArgs) Handles bubble.Click
    My.Computer.Audio.Play(My.Resources.pop, _
        AudioPlayMode.Background)
End Sub

However we also have a little backing track for the game that we want to loop in the background infinitely. We tried this with a similar instance of that function;

Private Sub GameScreen_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    My.Computer.Audio.Play(My.Resources.musicLoop, _
        AudioPlayMode.BackgroundLoop)
End Sub

The function shown above only allows one audio file to be played at once, meaning when a bubble is popped the music disappears for good.

I've tried using two seperate windows media player things, but that isn't working either;

Public pop As String = "pop.wav"
Public minesound As String = "mine.wav"

Public Sub soundEffects(sound)
    If sound = pop Then
        GameScreen.AxWindowsMediaPlayer2.URL = pop
    ElseIf sound = minesound Then
        GameScreen.AxWindowsMediaPlayer2.URL = minesound
    End If
End Sub

Any help or advice is very appreciated! Thank you!

Upvotes: 2

Views: 3527

Answers (1)

ElektroStudios
ElektroStudios

Reputation: 20464

Basically you need to run an asynchronous operation to play more than one file at once.

I've started writting a solution using My.Computer method but even using a Task/Thread it seems that (strangely) is not sufficient to play a secondary file without stopping the playback of the first file ran so maybe other factor (unknown for me) could be involved, then I've solved it using MCI.

The usage can be this:

Dim TaskCancellationTokenSource As New CancellationTokenSource
Dim TaskToken As CancellationToken = TaskCancellationTokenSource.Token

Private Sub BubbleLoop(ByVal CancellationToken As Threading.CancellationToken)

    Dim AudioFileLoop = New MCIPlayer(Me, "C:\BubbleLoop.wav")

    Do Until CancellationToken.IsCancellationRequested
        AudioFileLoop.Play(AudioPlayMode.WaitToComplete)
    Loop

    AudioFileLoop.Close()

End Sub

Private Sub Test()

    ' This plays a file asynchronously into an infinite loop.
    Task.Factory.StartNew(Sub() BubbleLoop(TaskToken), TaskToken)

    ' Wait 2 seconds (just to demonstrate this example)
    Threading.Thread.Sleep(2 * 1000)

    ' Play any other file while the loop is still playing.
    Dim AudioFile = New MCIPlayer(Me, "C:\SingleBubble.mp3")
    AudioFile.Play(AudioPlayMode.Background)

    ' Cancel the Bubble Loop.
    TaskCancellationTokenSource.Cancel()

End Sub

And you need to add this basic MCI class that I've did (It's not full tsted):

' [ MCI Player ]
'
' // By Elektro H@cker

#Region " Usage Examples "

'Dim AudioFile As New MCIPlayer(Me, "C:\Audio.wav")
'AudioFile.Play(AudioPlayMode.BackgroundLoop)

'Dim sb As New System.Text.StringBuilder
'sb.AppendLine("Filename: " & AudioFile.Filename)
'sb.AppendLine("State...: " & AudioFile.State.ToString)
'sb.AppendLine("Mode....: " & AudioFile.PlaybackMode.ToString)
'sb.AppendLine("Channels: " & CStr(AudioFile.Channels))
'sb.AppendLine("Duration: " & TimeSpan.FromMilliseconds(AudioFile.Duration).ToString("hh\:mm\:ss"))

'MessageBox.Show(sb.ToString, "MCI Player", MessageBoxButtons.OK, MessageBoxIcon.Information)

'AudioFile.Stop()

#End Region

#Region " MCI Player "

''' <summary>
''' Play Wave, MP3 or MIDI files
''' </summary>
Public Class MCIPlayer
    Inherits NativeWindow
    Implements IDisposable

#Region " API "

    ''' <summary>
    ''' Sends a command string to an MCI device.
    ''' The device that the command is sent to is specified in the command string.
    ''' </summary>
    ''' <param name="command">
    ''' Pointer to a null-terminated string that specifies an MCI command string. 
    ''' For a list, see Multimedia Command Strings.
    ''' </param>
    ''' <param name="buffer">
    ''' Buffer that receives return information. 
    ''' If no return information is needed, this parameter can be NULL.
    ''' </param>
    ''' <param name="bufferSize">
    ''' Size, in characters, of the return buffer specified.
    ''' </param>
    ''' <param name="hwndCallback">
    ''' Handle to a callback window if the "notify" flag was specified in the command string.
    ''' </param>
    <System.Runtime.InteropServices.
    DllImport("winmm.dll", SetLastError:=True)>
    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

#End Region

#Region " Variables "

    ''' <summary>
    ''' The form to manage Windows Messages.
    ''' </summary>
    Private WithEvents form As Form = Nothing

    ''' <summary>
    ''' Indicates the audio play command of mciSendString.
    ''' </summary>
    Private PlayCommand As String = String.Empty

    ''' <summary>
    ''' Buffer that receives return information.
    ''' </summary>
    Private ReturnInfo As New System.Text.StringBuilder() With {.Capacity = 255}

    ''' <summary>
    ''' The current filename of the file that is to be played.
    ''' </summary>
    Private _filename As String = String.Empty

    ''' <summary>
    ''' Indicates the current playback mode.
    ''' </summary>
    Private _PlaybackMode As AudioPlayMode

    ''' <summary>
    ''' Flag to cancel the BackgroundLoop PlaybackMode.
    ''' </summary>
    Private CancelLoop As Boolean = False

#End Region

#Region " Properties "

    ''' <summary>
    ''' The current filename of the file that is to be played.
    ''' </summary>
    Public Property Filename() As String

        Get
            Return _filename
        End Get

        Set(ByVal value As String)

            If Not IO.File.Exists(value) Then
                Throw New IO.FileNotFoundException
                Exit Property
            End If

            _filename = value

        End Set

    End Property

    ''' <summary>
    ''' Gets che current Playback State.
    ''' </summary>
    Public ReadOnly Property State As PlaybackState
        Get
            mciSendString("status file mode", ReturnInfo, ReturnInfo.Capacity, IntPtr.Zero)
            Return [Enum].Parse(GetType(PlaybackState), ReturnInfo.ToString, True)
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the playback mode of the current file.
    ''' </summary>
    Public Property PlaybackMode As AudioPlayMode
        Get
            Return _PlaybackMode
        End Get
        Set(value As AudioPlayMode)
            _PlaybackMode = value
        End Set
    End Property

    ''' <summary>
    ''' Gets the channels of the file.
    ''' </summary>
    ReadOnly Property Channels() As Integer
        Get
            mciSendString("status file channels", ReturnInfo, ReturnInfo.Capacity, IntPtr.Zero)
            Return If(IsNumeric(ReturnInfo.ToString),
                      CInt(ReturnInfo.ToString),
                      -1)
        End Get
    End Property

    ''' <summary>
    ''' Gets the file duration in Milleseconds.
    ''' </summary>
    ReadOnly Property Duration() As Integer
        Get
            mciSendString("set file time format milliseconds", Nothing, 0, IntPtr.Zero)
            mciSendString("status file length", ReturnInfo, ReturnInfo.Capacity, IntPtr.Zero)
            Return If(String.IsNullOrEmpty(ReturnInfo.ToString), 0, CInt(ReturnInfo.ToString))
        End Get
    End Property

#End Region

#Region " Enumerations "

    ''' <summary>
    ''' Audio File playback state.
    ''' </summary>
    Public Enum PlaybackState As Short

        ''' <summary>
        ''' File is playing.
        ''' </summary>
        Playing = 0

        ''' <summary>
        ''' File is paused.
        ''' </summary>
        Paused = 1

        ''' <summary>
        ''' File is stopped.
        ''' </summary>
        Stopped = 2

    End Enum

    ''' <summary>
    ''' Windows Message Identifiers.
    ''' </summary>
    Public Enum KnownMessages As Integer

        ''' <summary>
        ''' Notifies an application that an MCI device has completed an operation. 
        ''' MCI devices send this message only when the MCI_NOTIFY flag is used.
        ''' </summary>
        MM_MCINOTIFY = 953

    End Enum

#End Region

#Region " Constructor "

    ''' <summary>
    ''' Play Wave, MP3 or MIDI files.
    ''' </summary>
    ''' <param name="AudioFile">Indicates the filename of the media to play.</param>
    ''' <remarks></remarks>
    Public Sub New(ByVal form As Form, ByVal AudioFile As String)

        Me.Filename = AudioFile

        ' Set the Formulary.
        Me.form = form

        ' Assign the form handle.
        SetFormHandle()

    End Sub

#End Region

#Region " Public Methods "

    ''' <summary>
    ''' Plays the file that is specified as the filename.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Play(ByVal PlayMode As AudioPlayMode)

        DisposedCheck()

        Select Case PlayMode

            Case AudioPlayMode.Background
                PlayCommand = "play file from 0"
                Me.PlaybackMode = AudioPlayMode.Background

            Case AudioPlayMode.BackgroundLoop
                PlayCommand = "play file from 0 notify"
                Me.PlaybackMode = AudioPlayMode.BackgroundLoop

            Case AudioPlayMode.WaitToComplete
                PlayCommand = "play file from 0 wait"
                Me.PlaybackMode = AudioPlayMode.WaitToComplete

        End Select

        ' Open command
        Select Case Me.Filename.Split(".").LastOrDefault

            Case "mp3"
                mciSendString(String.Format("open ""{0}"" type mpegvideo alias file", Me.Filename),
                              Nothing,
                              0,
                              IntPtr.Zero)

            Case "wav"
                mciSendString(String.Format("open ""{0}"" type waveaudio alias file", Me.Filename),
                              Nothing,
                              0,
                              IntPtr.Zero)

            Case "mid", "midi"
                mciSendString("stop midi", Nothing, 0, 0)
                mciSendString("close midi", Nothing, 0, 0)
                mciSendString(String.Format("open sequencer! ""{0}"" alias file", Me.Filename),
                              Nothing,
                              0, IntPtr.Zero)

            Case Else
                Throw New Exception("File type not supported.")
                [Close]()

        End Select

        ' Play command
        mciSendString(PlayCommand, Nothing, 0, If(PlaybackMode = AudioPlayMode.BackgroundLoop,
                                                  Me.Handle,
                                                  IntPtr.Zero))

    End Sub

    ''' <summary>
    ''' Pause the current playback.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Pause()
        DisposedCheck()
        CancelLoop = True
        mciSendString("pause file", Nothing, 0, IntPtr.Zero)
    End Sub

    ''' <summary>
    ''' Resume the current playback if it is currently paused.
    ''' </summary>
    Public Sub [Resume]()
        DisposedCheck()
        If Me.State = PlaybackState.Paused Then
            CancelLoop = False
            mciSendString("resume file", Nothing, 0, IntPtr.Zero)
        End If
    End Sub

    ''' <summary>
    ''' Stop the current playback.
    ''' </summary>
    Public Sub [Stop]()
        DisposedCheck()
        CancelLoop = True
        mciSendString("stop file", Nothing, 0, IntPtr.Zero)
    End Sub

    ''' <summary>
    ''' Close the current file.
    ''' </summary>
    Public Overloads Sub [Close]()
        DisposedCheck()
        CancelLoop = True
        mciSendString("close file", Nothing, 0, IntPtr.Zero)
    End Sub

#End Region

#Region " Event Handlers "

    ''' <summary>
    ''' Assign the handle of the target form to this NativeWindow,
    ''' necessary to override WndProc.
    ''' </summary>
    Private Sub SetFormHandle() _
    Handles form.HandleCreated, form.Load, form.Shown

        Try
            If Not Me.Handle.Equals(Me.form.Handle) Then
                Me.AssignHandle(Me.form.Handle)
            End If
        Catch ' ex As InvalidOperationException
        End Try

    End Sub

    ''' <summary>
    ''' Releases the Handle.
    ''' </summary>
    Private Sub OnHandleDestroyed() _
    Handles form.HandleDestroyed

        Me.ReleaseHandle()

    End Sub

#End Region

#Region " Windows Messages "

    ''' <summary>
    ''' Processes Windows messages for this Window.
    ''' </summary>
    ''' <param name="m">
    ''' Contains the Windows Message parameters.
    ''' </param>
    Protected Overrides Sub WndProc(ByRef m As Message)

        MyBase.WndProc(m)

        If m.Msg = KnownMessages.MM_MCINOTIFY Then

            If Not CancelLoop Then
                Play(AudioPlayMode.BackgroundLoop)
            Else
                CancelLoop = False
            End If

        End If

    End Sub

#End Region

#Region " IDisposable "

    ''' <summary>
    ''' To detect redundant calls when disposing.
    ''' </summary>
    Private IsDisposed As Boolean = False

    ''' <summary>
    ''' Prevents calls to methods after disposing.
    ''' </summary>
    Private Sub DisposedCheck()
        If Me.IsDisposed Then
            Throw New ObjectDisposedException(Me.GetType().FullName)
        End If
    End Sub

    ''' <summary>
    ''' Disposes the objects generated by this instance.
    ''' </summary>
    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    ' IDisposable
    Protected Overridable Sub Dispose(IsDisposing As Boolean)

        If Not Me.IsDisposed Then

            If IsDisposing Then
                [Close]()
                Me.form = Nothing
                Me.ReleaseHandle()
                Me.DestroyHandle()
            End If

        End If

        Me.IsDisposed = True

    End Sub

#End Region

End Class

#End Region

Upvotes: 1

Related Questions