Introduction
This article shows how you can make use of sound files in your application, playing multiple sound files at the same time.
Background
There are at least 3 ways of playing sound files in a Windows Forms application:
- Using System.Media.SoundPlayer
- Using Windows Media Player control
- Using winmm.dll mciSendString
If you need just to play a single sound file in wav format, then System.Media.SoundPlayer would be the simpliest. But you cannot play mp3 files and you cannot play more than one file at the same time.
Windows Media Player control would be ideal if you want to have a nice UI to control the playing of media file.
If you just want a simple way to play your sound files and need more flexibilty than System.Media.SoundPlayer can offer, you may want to consider using winmm.dll mciSendString function
The MSDN documenation for winmm.dll mciSendString are here
MSDN mcSendString Documentation
mcSendString command string Documentation
The winmm.dll mciSendString function can be used to play various type of media files and its functionalities are quite extensive. In this article, we will just deal with sound files in wav and mp3 format.
Using the code
The code below is the implemenation of the MciPlayer class to wrap the required functionalities that are commonly used.
Imports System.Collections.Generic
Imports System.Text
Imports System.Runtime.InteropServices
Namespace MCIDEMO
Class MciPlayer
<dllimport("winmm.dll")> _
Private Shared Function mciSendString(strCommand As [String], _
strReturn As StringBuilder, _
iReturnLength As Integer, _
hwndCallback As IntPtr) As Integer
End Function
<dllimport("winmm.dll")> _
Public Shared Function mciGetErrorString(errCode As Integer, _
errMsg As StringBuilder, _
buflen As Integer) As Integer
End Function
<dllimport("winmm.dll")> _
Public Shared Function mciGetDeviceID(lpszDevice As String) As Integer
End Function
Public Sub New()
End Sub
Public Sub New(filename As String, [alias] As String)
_medialocation = filename
_alias = [alias]
LoadMediaFile(_medialocation, _alias)
End Sub
Private _deviceid As Integer = 0
Public ReadOnly Property Deviceid() As Integer
Get
Return _deviceid
End Get
End Property
Private _isloaded As Boolean = False
Public Property Isloaded() As Boolean
Get
Return _isloaded
End Get
Set
_isloaded = value
End Set
End Property
Private _medialocation As String = ""
Public Property MediaLocation() As String
Get
Return _medialocation
End Get
Set
_medialocation = value
End Set
End Property
Private _alias As String = ""
Public Property [Alias]() As String
Get
Return _alias
End Get
Set
_alias = value
End Set
End Property
Public Function LoadMediaFile(filename As String, [alias] As String) As Boolean
_medialocation = filename
_alias = [alias]
StopPlaying()
CloseMediaFile()
Dim Pcommand As String = "open """ & filename & """ alias " & [alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
_isloaded = If((ret = 0), True, False)
If _isloaded Then
_deviceid = mciGetDeviceID(_alias)
End If
Return _isloaded
End Function
Public Sub PlayFromStart()
If _isloaded Then
Dim Pcommand As String = "play " & [Alias] & " from 0"
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
End If
End Sub
Public Sub PlayFromStart(callback As IntPtr)
If _isloaded Then
Dim Pcommand As String = "play " & [Alias] & " from 0 notify"
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, callback)
End If
End Sub
Public Sub PlayLoop()
If _isloaded Then
Dim Pcommand As String = "play " & [Alias] & " repeat"
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
End If
End Sub
Public Sub CloseMediaFile()
Dim Pcommand As String = "close " & [Alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
_isloaded = False
End Sub
Public Sub StopPlaying()
Dim Pcommand As String = "stop " & [Alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
End Sub
End Class
End Namespace
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace MCIDEMO
{
class MciPlayer
{
[DllImport("winmm.dll")]
private static extern int mciSendString(String strCommand, StringBuilder strReturn, int iReturnLength, IntPtr hwndCallback);
[DllImport("winmm.dll")]
public static extern int mciGetErrorString(int errCode, StringBuilder errMsg, int buflen);
[DllImport("winmm.dll")]
public static extern int mciGetDeviceID(string lpszDevice);
public MciPlayer()
{
}
public MciPlayer(string filename, string alias)
{
_medialocation = filename;
_alias = alias;
LoadMediaFile(_medialocation, _alias);
}
int _deviceid = 0;
public int Deviceid
{
get { return _deviceid; }
}
private bool _isloaded = false;
public bool Isloaded
{
get { return _isloaded; }
set { _isloaded = value; }
}
private string _medialocation = "";
public string MediaLocation
{
get { return _medialocation; }
set { _medialocation = value; }
}
private string _alias = "";
public string Alias
{
get { return _alias; }
set { _alias = value; }
}
public bool LoadMediaFile(string filename, string alias)
{
_medialocation = filename;
_alias = alias;
StopPlaying();
CloseMediaFile();
string Pcommand = "open \"" + filename + "\" alias " + alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
_isloaded = (ret == 0) ? true : false;
if (_isloaded)
_deviceid = mciGetDeviceID(_alias);
return _isloaded;
}
public void PlayFromStart()
{
if (_isloaded)
{
string Pcommand = "play " + Alias + " from 0";
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
}
}
public void PlayFromStart(IntPtr callback)
{
if (_isloaded)
{
string Pcommand = "play " + Alias + " from 0 notify";
int ret = mciSendString(Pcommand, null, 0, callback);
}
}
public void PlayLoop()
{
if (_isloaded)
{
string Pcommand = "play " + Alias + " repeat";
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
}
}
public void CloseMediaFile()
{
string Pcommand = "close " + Alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
_isloaded = false;
}
public void StopPlaying()
{
string Pcommand = "stop " + Alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
}
}
}
To use the MciPlayer class to play a sound file, we call its constructor, providing the full-path name of the sound file and a alias for the file, then call one of the play functions. In code below, the MciPlayer is instantiated by the MciPlayer(string filename, string alias) constructor, and the the file is played using the PlayFromStart play function.
Dim filename As String = "Accordion-SoundBible.com-74362576.mp3"
Dim m As New MciPlayer(Application.StartupPath + "\" & filename, "1")
m.PlayFromStart()
string filename = "Accordion-SoundBible.com-74362576.mp3";
MciPlayer m = new MciPlayer(Application.StartupPath + @"\" + filename, "1");
m.PlayFromStart();
Note that the alias must be unique for each file. We make use of the alias to inform the MCI system of the file that we want to play.
To play multiple files, just instantiate one MciPlayer for each file each using a unique alias. You can then play any of the MciPlayer simultaneously.
Dim filename As String = "Accordion-SoundBible.com-74362576.mp3"
Dim m As New MciPlayer(Application.StartupPath + "\" & filename, "1")
filename = "Music_Box-Big_Daddy-1389738694.mp3"
Dim m1 As New MciPlayer(Application.StartupPath + "\" & filename, "2")
m.PlayLoop()
m1.PlayLoop()
string filename = "Accordion-SoundBible.com-74362576.mp3";
MciPlayer m = new MciPlayer(Application.StartupPath + @"\" + filename, "1");
filename="Music_Box-Big_Daddy-1389738694.mp3";
MciPlayer m1 = new MciPlayer(Application.StartupPath + @"\" + filename, "2");
m.PlayLoop();
m1.PlayLoop();
If you are playing a file and you need to be informed if the file has finished playing, you can use the PlayFromStart(IntPtr callback), where callback is a handle to a Windows Form that will receive the callback.
The MCI system triggers the callback by sending a Windows message MM_MCINOTIFY ( value= 953)
To receive this Windows message, the callback handler overrides the default WinProc() function for the Form.
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = MM_MCINOTIFY Then
System.Diagnostics.Debug.WriteLine(m.ToString())
For Each itm As Form1.ListItem In DirectCast(Me.parent, Form1).listBox1.Items
If itm.DeviceId = CInt(m.LParam) Then
If (itm.Filename.Substring(itm.Filename.Length - 4).ToUpper() = ".WAV") _
AndAlso (CInt(m.WParam) = MCI_NOTIFY_SUCCESSFUL) _
AndAlso (itm.Playlooping) Then
Dim p As New MciPlayer()
p.[Alias] = itm.[Alias]
p.Isloaded = True
p.PlayFromStart(Me.Handle)
Exit For
Else
listBox1.Items.Add(DateTime.Now.ToString() & " " & _
DirectCast(itm.Filename, String))
Exit For
End If
End If
Next
End If
MyBase.WndProc(m)
End Sub
protected override void WndProc(ref Message m)
{
if (m.Msg == MM_MCINOTIFY)
{
System.Diagnostics.Debug.WriteLine(m.ToString());
foreach (Form1.ListItem itm in ((Form1)this.parent).listBox1.Items)
{
if (itm.DeviceId == (int)m.LParam)
{
if (
(itm.Filename.Substring(itm.Filename.Length - 4).ToUpper() == ".WAV")
&& ((int)m.WParam == MCI_NOTIFY_SUCCESSFUL)
&& (itm.Playlooping)
)
{
MciPlayer p = new MciPlayer();
p.Alias = itm.Alias;
p.Isloaded = true;
p.PlayFromStart(this.Handle);
break;
}
else
{
listBox1.Items.Add(DateTime.Now.ToString() + " " + (string)itm.Filename);
break;
}
}
}
}
base.WndProc(ref m);
}
In our demo, Form2 is used for receiving the callback when we click the Play Notify button
Private f2 As Form2
f2 = New Form2()
..
Private Sub button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles button1.Click
If listBox1.SelectedIndex < 0 Then
Return
End If
Dim itm As ListItem = DirectCast(listBox1.SelectedItem, ListItem)
Dim filename As String = itm.ToString()
Dim m As MciPlayer = Nothing
If itm.[Alias] <> "" Then
m = New MciPlayer()
m.[Alias] = itm.[Alias]
m.Isloaded = True
Else
Dim [alias] As String = ""
m = CreateMCIPlayer(filename, [alias])
itm.[Alias] = [alias]
itm.DeviceId = m.Deviceid
End If
itm.Playlooping = False
m.PlayFromStart(f2.Handle)
End Sub
Form2 f2;
f2 = new Form2();
....
private void button1_Click(object sender, EventArgs e)
{
if (listBox1.SelectedIndex < 0) return;
ListItem itm=(ListItem)listBox1.SelectedItem;
string filename = itm.ToString();
MciPlayer m=null;
if (itm.Alias != "")
{
m = new MciPlayer();
m.Alias = itm.Alias;
m.Isloaded = true;
}
else
{
string alias = "";
m = CreateMCIPlayer(filename, ref alias);
itm.Alias = alias;
itm.DeviceId = m.Deviceid;
}
itm.Playlooping = false;
m.PlayFromStart(f2.Handle);
}
Our demo just display the time the sound has stopped playing and the file name of the sound file. As "play.. repeat" is not supported by miciSendString() for wav file, we also make use of callback to implement Play Looping for wav file in our overriden Winproc() function.
File Loading Considerations
Public Function LoadMediaFile(filename As String, [alias] As String) As Boolean
_medialocation = filename
_alias = [alias]
StopPlaying()
CloseMediaFile()
Dim Pcommand As String = "open """ & filename & """ alias " & [alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
_isloaded = If((ret = 0), True, False)
If _isloaded Then
_deviceid = mciGetDeviceID(_alias)
End If
Return _isloaded
End Function
public bool LoadMediaFile(string filename, string alias)
{
_medialocation = filename;
_alias = alias;
StopPlaying();
CloseMediaFile();
string Pcommand = "open \"" + filename + "\" alias " + alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
_isloaded = (ret == 0) ? true : false;
if (_isloaded)
_deviceid = mciGetDeviceID(_alias);
return _isloaded;
}
Note that before we load a file to the MCI system via open command, we have to ensure that alias has not been used. To ensure this, we first stop playing the file and then close the media file using its alias.
Demo
When the demo application starts, it looks for all .mp3 and .wav file in the current directory and list them in the list box in Form1, At the same time Form2 will pop up by the side as a callback monitor.
Select any file from the listbox and click any of the play buttons:
"Play Loop" plays the file continuously, looping to the start once it reaches the end
"Play" plays the file once
"Play Notify" plays the file once and then tiggers a callback which is handled by Form2. When the file has finished playing, a row will appear in the listbox in Form 2 showing the time of play completion and the alias of the file.
To stop any file that is playing, select the file from the list and click "Stop Playing" button
If you cannot remember which file is played, you can stop all files by clicking "Stop All" button
Just for fun, you may want to put your favorite mp3 files into the current path (where MCIDEMO.exe is located) to be listed in the listbox and then play two or more of these files by clicking the "Play Loop" button.
Have fun!
Unicode Filename
One of the problems with mciSendString() is the support for Unicode file names. Windows explorer and C# has full support for Unicode file names. But mciSendString() does not seem to work with Unicode file names. To work around this problem, I have created a sub-directory to the current directory named "unicodenamesupport". If a file name in the listbox is found to be a non ANSI file name, we copy the file to this sub-directory with an incremental indexer as its name. This copy of the file together with the indexer as the alias would be used to instantiate the MciPlayer instead.
Private Function IsAnsiName(s As String) As Boolean
Dim u As New UnicodeEncoding()
Dim b As Byte() = u.GetBytes(s)
For i As Integer = 1 To b.Length - 1 Step 2
If b(i) <> 0 Then
Return False
End If
Next
Return True
End Function
Private Function CreateMCIPlayer(filename As String, ByRef [alias] As String) As MciPlayer
Dim isansiname As Boolean = Me.IsAnsiName(filename)
Dim m As MciPlayer = Nothing
nextnum += 1
If isansiname Then
m = New MciPlayer(Application.StartupPath & "\" & filename, nextnum & "")
[alias] = nextnum & ""
Else
Dim ext As String = filename.Substring(filename.Length - 4)
Dim relocatedfile As String = Application.StartupPath & _
"\unicodenamesupport\" & nextnum & ext
If System.IO.File.Exists(relocatedfile) Then
System.IO.File.Delete(relocatedfile)
End If
System.IO.File.Copy(Application.StartupPath & "\" & filename, relocatedfile)
[alias] = nextnum & ""
m = New MciPlayer(relocatedfile, nextnum & "")
End If
Return m
End Function
private bool IsAnsiName(string s)
{
UnicodeEncoding u = new UnicodeEncoding();
byte[] b=u.GetBytes(s);
for (int i = 1; i < b.Length; i += 2)
{
if (b[i] != 0) return false;
}
return true;
}
private MciPlayer CreateMCIPlayer(string filename, ref string alias)
{
bool isansiname = this.IsAnsiName(filename);
MciPlayer m = null;
nextnum++;
if (isansiname)
{
m = new MciPlayer(Application.StartupPath + @"\" + filename, nextnum + "");
alias = nextnum + "";
}
else
{
string ext = filename.Substring(filename.Length - 4);
string relocatedfile = Application.StartupPath + "\\unicodenamesupport\\" + nextnum + ext;
if (System.IO.File.Exists(relocatedfile))
System.IO.File.Delete(relocatedfile);
System.IO.File.Copy(Application.StartupPath + @"\" + filename, relocatedfile);
alias = nextnum + "";
m = new MciPlayer(relocatedfile, nextnum + "");
}
return m;
}
Points of Interest
In some application, especially games, sound makes it a lot more enagaging, if used effectively. You may want to consider adding support for sound in your next application.
History
Fun with Sound V1:
3 June 2014: Add the function getAliasFromFileName(string s) to replace all spaces in a filename, as the alias can not have any spaces as spaces.This will meddle up the final command string that is send to cmciSendString function.
4 June 2014: Add in workaround to support Non ANSI Unicode file name. for example a file that includes Chinese characters in its file name.
6 June 2014: Add in device id to identify the device (in our case the sound file name) so that we can track the lparam returned from callback to the overridden Winproc. In this case we can "Play notify" as many files as we want and yet are able to know which file has stopped playing.
8 June 2014: Add in workaround to support repeat play of wav file using callback to Winproc.