Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Fun with Sound

0.00/5 (No votes)
11 Jul 2014 1  
Playing your favorite music and sound files all at once

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

		' The file is done playing, do whatever
		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
				'To handle wav file play looping      
				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)
            {
                // The file is done playing, do whatever
                System.Diagnostics.Debug.WriteLine(m.ToString());          
                foreach (Form1.ListItem itm in ((Form1)this.parent).listBox1.Items)
                {
                    if (itm.DeviceId == (int)m.LParam)
                    {
                        //To handle wav file play looping      
                        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.

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here