18 febrero, 2015

Reproducir sonido para juegos en vb.net varios métodos

Reproducir sonido en vb.net es una tarea muy sencilla siempre y cuando nos atengamos a ciertas limitaciones. La manera más común de reproducir sonido es el espacio de nombres My.Computer.Audio, que sólo es capaz de reproducir ficheros .wav, cumple su trabajo si se trata de introducir un sonido a un botón o a una notificación, pero en proyectos más complejos es una trampa. 

Algunos sabreis que estoy actualmente creando un engine arpg en vb.net, un lenguaje contraindicado para programas donde los gráficos y sonidos deben moverse muy rápido. Algunos os preguntareis que porqué no uso XNA games studio, pero la verdad, dicha solución de microsoft no me convence y además creo que ya está desmantenida. ¿Y directx? la verdad es que no quiero portar este proyecto a directx aun, desde el principio he usado Gdi+ y ya está demostrado que sirve aunque hay que usar ciertos trucos, en el sonido no es diferente.

En el desarrollo del engine noté picos muy grandes de lag usando My.computer.audio.play() y peor aún, no podía reproducir varios sonidos en paralelo lo cual es vital para el engine. He pasado un par de semanas informándome y probando y he llegado a soluciones más rápidas o menos limitadas para reproducir audio en .net

Método1: winmm.dll / mciSendString
Al principio encontré un objeto escrito por un tal Blake Pell (bpell@indiana.edu) que durante un tiempo me sirvió para añadir música en bucle al engine mientras que las llamadas a my.computer.audio() no interferían con esta. Así podía tener algo así como  dos canales, uno para la música y otro para todo el sonido FX. Además que este objeto puede reproducir entre otros formatos mp3 lo que permite que la música del juego no ocupe una barbaridad. Por supuesto seguía teniendo el problema del lag, pero eso lo solucionaría más adelante. El código es bastante grande y sus funciones estan planteadas como un reproductor con controles de pausa etc. 

Public Class PlayFile
'***********************************************************************************************************
' Class: PlayFile
' Written By: Blake Pell (bpell@indiana.edu)
' Initial Date: 03/31/2007
' Last Updated: 02/04/2009
'***********************************************************************************************************

' Windows API Declarations
Private Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" (ByVal lpstrCommand As String, ByVal lpstrReturnString As String, ByVal uReturnLength As Int32, ByVal hwndCallback As Int32) As Int32
'''
''' Constructor: Location is the filename of the media to play. Wave files and Mp3 files are the supported formats.
'''

'''
'''
Public Sub New(ByVal location As String)
Me.Filename = location
End Sub
'''
''' Plays the file that is specified as the filename.
'''

'''
Public Sub Play()
If _filename = "" Or Filename.Length <= 4 Then Exit Sub
Select Case Right(Filename, 3).ToLower
Case "mp3"
mciSendString("open """ & _filename & """ type mpegvideo alias audiofile", Nothing, 0, IntPtr.Zero)
Dim playCommand As String = "play audiofile from 0 repeat"
If _wait = True Then playCommand += " wait"
mciSendString(playCommand, Nothing, 0, IntPtr.Zero)
Case "wav"
mciSendString("open """ & _filename & """ type waveaudio alias audiofile", Nothing, 0, IntPtr.Zero)
mciSendString("play audiofile from 0", Nothing, 0, IntPtr.Zero)
Case "mid", "idi"
mciSendString("stop midi", "", 0, 0)
mciSendString("close midi", "", 0, 0)
mciSendString("open sequencer!" & _filename & " alias midi", "", 0, 0)
mciSendString("play midi", "", 0, 0)
Case Else
'Throw New Exception("File type not supported.")
Call Close()
End Select
IsPaused = False
End Sub

'''
''' Pause the current play back.
'''

'''
Public Sub Pause()
mciSendString("pause audiofile", Nothing, 0, IntPtr.Zero)
IsPaused = True
End Sub

'''
''' Resume the current play back if it is currently paused.
'''

'''
Public Sub [Resume]()
mciSendString("resume audiofile", Nothing, 0, IntPtr.Zero)
IsPaused = False
End Sub

'''
''' Stop the current file if it's playing.
'''

'''
Public Sub [Stop]()
mciSendString("stop audiofile", Nothing, 0, IntPtr.Zero)
End Sub

'''
''' Close the file.
'''

'''
Public Sub Close()
mciSendString("close audiofile", Nothing, 0, IntPtr.Zero)
End Sub

Private _wait As Boolean = False
'''
''' Halt the program until the .wav file is done playing. Be careful, this will lock the entire program up until the
''' file is done playing. It behaves as if the Windows Sleep API is called while the file is playing (and maybe it is, I don't
''' actually know, I'm just theorizing). :P
'''

'''
'''
'''
Public Property Wait() As Boolean
Get
Return _wait
End Get
Set(ByVal value As Boolean)
_wait = value
End Set
End Property
'''
''' Sets the audio file's time format via the mciSendString API.
'''

'''
'''
'''
ReadOnly Property Milleseconds() As Integer
Get
Dim buf As String = Space(255)
mciSendString("set audiofile time format milliseconds", Nothing, 0, IntPtr.Zero)
mciSendString("status audiofile length", buf, 255, IntPtr.Zero)
buf = Replace(buf, Chr(0), "") ' Get rid of the nulls, they muck things up
If buf = "" Then
Return 0
Else
Return CInt(buf)
End If
End Get
End Property
'''
''' Gets the status of the current playback file via the mciSendString API.
'''

'''
'''
'''
ReadOnly Property Status() As String
Get
Dim buf As String = Space(255)
mciSendString("status audiofile mode", buf, 255, IntPtr.Zero)
buf = Replace(buf, Chr(0), "") ' Get rid of the nulls, they muck things up
Return buf
End Get
End Property
'''
''' Gets the file size of the current audio file.
'''

'''
'''
'''
ReadOnly Property FileSize() As Integer
Get
Try
Return My.Computer.FileSystem.GetFileInfo(_filename).Length
Catch ex As Exception
Return 0
End Try
End Get
End Property
'''
''' Gets the channels of the file via the mciSendString API.
'''

'''
'''
'''
ReadOnly Property Channels() As Integer
Get
Dim buf As String = Space(255)
mciSendString("status audiofile channels", buf, 255, IntPtr.Zero)
If IsNumeric(buf) = True Then
Return CInt(buf)
Else
Return -1
End If
End Get
End Property
'''
''' Used for debugging purposes.
'''

'''
'''
'''
ReadOnly Property Debug() As String
Get
Dim buf As String = Space(255)
mciSendString("status audiofile channels", buf, 255, IntPtr.Zero)
Return Str(buf)
End Get
End Property

Private _isPaused As Boolean = False
'''
''' Whether or not the current playback is paused.
'''

'''
'''
'''
Public Property IsPaused() As Boolean
Get
Return _isPaused
End Get
Set(ByVal value As Boolean)
_isPaused = value
End Set
End Property
Private _filename As String
'''
''' The current filename of the file that is to be played back.
'''

'''
'''
'''
Public Property Filename() As String
Get
Return _filename
End Get
Set(ByVal value As String)
If My.Computer.FileSystem.FileExists(value) = False Then
'Throw New System.IO.FileNotFoundException
End If
_filename = value
End Set
End Property
End Class
Método2: winmm.dll / PlaySound
Intentando dar con un modo de reproducir sonidos en varios canales encontré este pequeño objeto que sorprendentemente terminó con el problema del lag al cargar sonido. Reproduce wav pero resulta ser extraordinariamente rápido y sólo le encontré una pega, no permite reproducir varios sonidos a la vez. No solucionó mi problema de los canales, pero inesperadamente solucionó el del lag. Intenté hacer todo tipo de modificaciones para conseguir multicanal, pero nada funcionó.
Private Class SoundCore
Private Declare Auto Function PlaySound Lib "winmm.dll" (ByVal name As String, ByVal hmod As Integer, ByVal flags As Integer) As Integer
' name specifies the sound file when the SND_FILENAME flag is set.
' hmod specifies an executable file handle.
' hmod must be Nothing if the SND_RESOURCE flag is not set.
' flags specifies which flags are set.

' The PlaySound documentation lists all valid flags.
Public Const SND_SYNC As Integer = &H0 ' play synchronously
Public Const SND_ASYNC As Integer = &H1 ' play asynchronously
Public Const SND_FILENAME As Integer = &H20000 ' name is file name
Public Const SND_RESOURCE As Integer = &H40004 ' name is resource name or atom
'more
Public Const SND_APPLICATION = &H80
Public Const SND_NODEFAULT = &H2 '
Public Const SND_LOOP = &H8 '
Public Const SND_NOSTOP = &H10 '
Public Const SND_NOWAIT = &H2000
''' -------------------------------------------------
'''
''' Reproducir un archivo de sonido que esta en un archivo del disco
'''

'''
''' Referencia bibliografica Ayuda MSDM
''' ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.3082/dv_vbcode/html/vbtskCodeExamplePlayingSound.htm
'''

''' Si el archivo especificado no existe, se reproduce el
''' sonido de evento de sistema predeterminado y no devuelve ningun
''' error.

'''

'''
''' El nombre de archivo que debe hacer referencia a un archivo
''' de sonido que se encuentre en el sistema
'''
''' (System.String)
'''

'''
'''
''' Un valor booleano.: Devuelve
''' True.: cuando tiene exito y el archivo de sonido
''' se reproduce

''' False: cuando por la razon que sea no se reproduce el
''' sonido

'''
''' (System.Boolean)
'''

'''

''' -------------------------------------------------
Public Function Play(ByVal nombreFicheroSonido As String) As Boolean
Try
' Plays a sound from filename.
Return CBool(PlaySound(nombreFicheroSonido, Nothing, SND_FILENAME Or SND_ASYNC Or SND_APPLICATION))
Catch ex As Exception
Throw ex
End Try
End Function
End Class
Método3 WMPLib player
Ésta vez di con un método de reproducción multicanal aunque por desgracia no tan rápido como hubiera deseado. Se trata de un objeto COM (yo suelo evitarlos) que funciona como interface para manejar reproductores de windows media, decirle que archivo debe reproducir etc. Incluso permite crear playlists (supongo que tendrá utilidad para el engine). No es tan lento como my.computer.audio pero el pico de lag llega a ser perceptible. Cuando un sonido nuevo llega el anterior sigue reproduciendose lo cual lo convierte en el método adecuado para controlar SFX en la creación de juegos. Es el código más pequeño de todos y no lleva anotaciones puesto que es tan sencillo que símplemente sobran.


Private Class SoundCore_wmp
Private WithEvents player As New WMPLib.WindowsMediaPlayer

Public Sub play(nombrearchivo As String)
If Not (player.URL = nombrearchivo) Then
player.URL = nombrearchivo
End If
player.controls.play()
End Sub
End Class

Publicar un comentario