Advanced Basics

The Sound of Music

Brad McCabe

So you've got a burning desire to accompany your application's splash screen with a rousing chorus of "Funky Cold Medina" (that's Tone Loc for those of you who aren't children of the 80s). Prior to working with Visual Studio® 2005, adding even simple tunes and system sounds to your application could be a challenge. There are numerous new classes and namespaces that have been added to the Microsoft® .NET Framework 2.0 to help you do just that. I'm going to take a look at one of them, the System.Media namespace, in this month's column.

Let's start by bringing this application into the 1960s with a simple beep, reminiscent of Pong. To do this, simply use the SystemSounds class, which has five public properties: Asterisk, Beep, Exclamation, Hand, and Question. Each of these properties returns an object of type SystemSound, which in turn exposes a Play method. So to make the computer beep, use System.Media.SystemSounds.Beep.Play. Pretty impressive, huh?

What's nice about the SystemSounds class is that if the user has modified his system sounds or applied any themes, they will be reflected in your application. Now, with one line of code before displaying a message box, you can play a sound to match the urgency of the message, such as the system exclamation or question sound, to get a user's attention based on the content of your message:

System.Media.SystemSounds.Exclamation.Play()
MessageBox.Show("Visual Basic!", "Advanced Basics", _
    MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

Playing system sounds is useful, but what if you want to further customize your application with more complex sounds? In System.Media, you'll find a SoundPlayer class for controlling the playback of .wav files. The SoundPlayer supports playing sounds from a file, a URL, a stream, or an embedded WAV resource.

The first step to playing a file is to load it. You are required to explicitly load the file first if you are going to play it from a stream or a URL. If you are playing from an embedded resource or a file, the Play method will load the file for you if you have not already done so. As with so many other aspects of .NET, you have flexibility in your approach: you can load the files synchronously or asynchronously, depending on the needs of your application.

If you are loading the .wav file close to the time when you will play it back, and you cannot guarantee that the file will be loaded by the time you call the Play method, you should choose a synchronous approach. The downside to loading in this manner is that you will block any further execution of your application on the main thread and lock up the user interface during the load.

The asynchronous approach is effective if you are loading from a slower source, such as a URL, or if you have a large file. You should call the asynchronous load method at an early stage in your execution. Unlike its synchronous counterpart, the asynchronous approach allows application code to continue to execute while your file is loaded. You will be notified that the file has loaded with a LoadCompleted event in case you want to do any additional processing or take any other action. In addition, you can check the IsLoadCompleted property at any time to determine the status of the load. The drawback to this approach is that it takes a bit more code to capture the events, and it adds a slight extra bit of complexity to your code. To load a file asynchronously, your code would look similar to this:

Dim Player As New SoundPlayer
Player.SoundLocation = "C:\Program Files\Messenger\newemail.Wav"
Player.LoadAsync()

Now, you are ready to play your .wav file. Like with loading, you have two choices here: either synchronously or asynchronously. Notice a pattern? The same pros and cons apply. Synchronous playing requires the user to listen to your file completely before the application logic continues executing, and you don't have to worry about any other code or pesky user interaction. But once again your user is locked out and stuck until your file has finished playing. If your sound is only a second or two long this might not be an issue, but any longer and your user will swear that your application is too slow and will tell everyone how awful it is. (Isn't perception just grand?)

In fact, the SoundPlayer.Play method will play the file asynchronously. The designers of the Framework require you to go out of your way here to block the UI thread and lock up your application code:

Dim Player As New SoundPlayer
Player.SoundLocation = "C:\Program Files\Messenger\newemail.Wav"
Player.LoadAsync()
If Player.IsLoadCompleted Then Player.Play()

If you want to play your file synchronously, you will have to call SoundPlayer.PlaySync. Remember, if your file is large, your application's performance will be visibly affected.

What if you want to continue playing your .wav file until the user either hits a stop button or you programmatically end it? To do this, you can call the PlayLooping method and the Framework will asynchronously loop your file. Stopping any asynchronously playing file is as simple as calling the SoundPlayer.Stop method.

Now let's take a second to talk about the My namespace. It was designed as a "speed dial" into the .NET Framework and to give a boost to developer productivity. For example, almost everything discussed so far can be done in a line or two of code using My.Computer.Audio. To play a file, you can use My.Computer.Audio.Play("C:\Program Files\Messenger\newemail.Wav"). No need to worry about manually creating the SoundPlayer object or loading files, even for streams or URLs.

So why bother writing more code than that? Well, besides learning how to perform the functionality directly against the Framework (a useful skill to have if you ever need to code something in a language that does not easily benefit from My, like C#), you also need to work directly with the SoundPlayer class if you want to attach to and work with any of the three main events it will raise.

First, the LoadCompleted event that I briefly mentioned earlier will be fired at the end of an asynchronous file load. If you are loading your .wav file from a URL, this is an important event to capture, as you will want to check the event argument to see if the file was successfully loaded. In fact, even if you are loading asynchronously from a local file, it is a good idea to always code defensively and verify that the file is indeed loaded:

Private Sub Player_LoadCompleted(ByVal sender As Object, _
        ByVal e As ComponentModel.AsyncCompletedEventArgs) _
        Handles Player.LoadCompleted
    If e.Error IsNot Nothing Then
        ' Handle error
        isLoaded = False
    Else
        isLoaded = True
    End If
End Sub

Next, the SoundLocationChanged event is raised when a new audio source has been set for the SoundPlayer. When the location has been changed or updated, you might want to start loading the .wav file asynchronously, stop playing the previous file if one is looping, or perform any other action your application needs to take.

The third and final major event that you may need to interact with is the StreamChanged event. This event is similar in use to the SoundLocationChanged event and would be used if you were loading your .wav file from a MemoryStream or FileStream.

Making your computer beep, or playing .wav files, is hardly blasting out the tunes. If you want more than the ability to play .wav files, you'll need to reach outside the .NET Framework. One option is to make use of Windows Media® Player (you can download and install the player SDK). With the Windows® Media Player SDK installed and a reference to Windows Media Player libraries set up, you have access to most major file formats, including MP3 and WMA.

Just like the simple design of the System.Media namespace, playing files with Windows Media Player is very straightforward. Three lines of code and you are up and running:

Dim WMP As New WMPLib.WindowsMediaPlayer
WMP.URL = "C:\My Music\Funky Cold Medina.wma"
WMP.controls.play()

Now you have three ways to add sounds and media to your application. You can use the new System.Media namespace for a lightweight approach to system sounds and .wav files, you can use the Visual Basic® My namespace to have a more productive but slightly limited approach to accessing the System.Media namespace, or you can use Windows Media Player to support files like WMA and MP3. To simplify this, you can build an extension to the My namespace to combine these and pull together everything I have shown you (see Figure 1). An application that makes use of this is shown in Figure 2. See Joe Binder's article from the July 2005 edition of MSDN Magazine for more information on extending My (Visual Basic: Simplify Common Tasks by Customizing the My Namespace).

Figure 1 Extending My with Better Sound Support

Namespace My
    Partial Class MyComputer
        Overloads ReadOnly Property Audio() As Audio
            Get
                Return New Audio
            End Get
        End Property
    End Class
End Namespace

Public Class Audio
    Inherits Microsoft.VisualBasic.Devices.Audio

    Private Shared fileType As String = ""
    Private Shared WMP As New WMPLib.WindowsMediaPlayer

    '  You need to overload Play to switch between System.Media and WMP.
    Public Overloads Sub Play(ByVal location As String)
        ' If anything is already playing then stop it
        If fileType <> "" Then [Stop]()

        ' Find out what type of file you want to play and play it.
        Select Case New IO.FileInfo(location).Extension.ToLower
            Case ".wma", ".mp3"
                WMP.URL = location
                WMP.controls.play()
                fileType = "WMP"

            Case ".wav"
                MyBase.Play(location)
                fileType = "WAV"

            Case Else
                Throw New Exception("Invalid File Type")
        End Select
    End Sub

    ' While you are overloading let's just combine system sounds 
    ' into one big play function
    Public Overloads Sub Play( _
            ByVal systemSound As System.Media.SystemSound)
        My.Computer.Audio.PlaySystemSound(systemSound)
    End Sub

    '  You need to overload the Stop function to allow you to stop WMP
    Public Overloads Sub [Stop]()
        Select Case fileType
            Case "WMP"
                WMP.controls.stop()
            Case "WAV"
                MyBase.Stop()
            Case Else
                Throw New Exception("Unknown File Type Currently Playing")
        End Select

        fileType = ""
    End Sub
End Class

Figure 2 App with Extended My Capabilities

Figure 2** App with Extended My Capabilities **

To summarize, adding sound to your application can help build a connection with the user and provide valuable non-visual feedback when used properly. Prior to the introduction of the System.Media namespace with the 2.0 release of the .NET Framework, adding the ability to quickly play system sounds and .wav files was not very straightforward and therefore was not utilized to its potential. Now developers using Visual Basic have the ability to choose either to use the productive "speed dial" of My.Computer.Audio or call the System.Media classes directly.

Now for the common sense portion of the column. Remember that while you can easily add sound to your applications, do so wisely and sparingly. While you may think the best UI is accompanied by a blaring Metallica riff, there will always be users who much prefer something just a bit more subtle—Green Day perhaps! A good test is "would my Mom find this useful?"

Send your questions and comments to  basics@microsoft.com.

Brad McCabe is a Program Manager at Microsoft and, among other things, is responsible for the Visual Basic Developer Center on MSDN.