Share via


Want to Know What I'm Listening To?

 

Duncan Mackenzie
Microsoft Developer Network

April 30, 2003

Summary: Duncan Mackenzie describes how to use a new PowerToy for Windows Media Player to tell the world what you are listening to. (10 printed pages)

Applies to:
   Microsoft® Visual Basic® .NET

Download the source code for this article.

Extensible Software Is Great

Considering I program in Microsoft® Visual Basic®, this shouldn't come as a big surprise, but I just love it when software supports some form of extensibility model. Call them add-ins in Microsoft® Office, or plug-ins in Adobe Photoshop® and Microsoft® Windows Media® Player, it is all the same to me. All of these terms indicate some way that I can add my own little bit of functionality to an already existing piece of software.

Now, as I said in my first column, I often write complete applications for my own use, even when a perfectly good piece of software already exists. Often, I write these applications for fun, but sometimes I end up writing a full-blown application just because I want to add a single new feature. It is much easier when I can write a small chunk of add-in, plug-in, or even macro code to add that single feature I want without having to rebuild the complete system. In this article, I'm going to show you how I used a new PowerToy for Windows Media Player, in conjunction with a few different programs of my own, to let the world know what music I am listening to at any given time.

Blogging as a Motivator

I have recently started blogging, which is a curious practice rather akin to writing a diary or a journal, except that every entry is public the moment after you post it. This experience soon had me thinking about writing some blog-related code. Instead of writing a blog engine or a blog reader, I started thinking about a blogging-related utility that would add info about the song I was playing to each post I made. This seemed like a really cool idea, and I knew that similar software had been written before and was well received, so I set to work trying to understand the plug-in architecture of Windows Media Player.

After digging through the SDKs and looking at the interfaces I would need to implement, I realized that this was a relatively simple task in Microsoft® Visual C++® but not all that easy to accomplish using managed code. My C++ skills are quite rusty, so I wasn't really interested in writing anything in Visual C++, but implementing the necessary interface would be difficult (although not impossible) in Visual Basic .NET or C#. I kept working at it, though, and had started creating my own wrapper in managed code for the IWMPUIPlugIn interface, when I decided to ask the Windows Media Player team for help and advice.

Their answer was quite helpful, though it promptly killed my development project. They would be releasing a "blogging plug-in" for Windows Media Player (Zach Robinson, the writer of the add-in gives details on its intended use in his column) that does exactly what I was trying to do—expose the currently playing media item's information to other programs. Sure enough, it is out and available as part of the new Windows Media Player 9 Series Fun Pack, along with some other cool (and free!) PowerToys. But it isn't intended to be a complete solution on its own. It is designed to make the currently playing item information available to other programs by saving it to the registry (updating a set of four keys whenever the current music item changes), but that is it. You need to have or write a program that consumes this musical metadata before you can do anything with it, which leaves me something to demonstrate!

How I Decided to Use the MetaData

Several blog-posting software writers are incorporating the plug-in on their own, so I decided I had better write something else for my samples. First, I focused on Microsoft® Windows Messenger; wouldn't it be cool if everyone on my Messenger list could see what song I was listening to? I will warn you ahead of time, the reaction to this was not so great. My friends and family had a wide range of reactions from "you are a freak" to "you are weird" (yes, I know… my friends and family aren't very nice people). A few of my geekier associates thought it was cool, but for the most part the response was roughly, "What makes you think I want to know what you are listening to?" Still, I will show you the very simple code to get this to work, and you can find out just how polite your own friends are. After playing with Windows Messenger, I moved onto messing with my Microsoft® Outlook® e-mail, and then finally created a system-wide keyboard shortcut.

Writing One Big Utility

Instead of writing each sample as its own application, I decided to use the basic framework of a system tray utility (as shown in my previous column) and just add the code for each sample onto that foundation.

Accessing the Registry

As a first step, I decided that I should make the registry-accessing code independent of any little applications I build to use the information exposed by the plug-in. By breaking this code out into its own library, it will be pretty easy to add this functionality into any application where it would be useful (or at least fun). For a start, I created the library in Visual Basic .NET, but I also built it as a Microsoft® ActiveX® object using Visual Basic 6. If you aren't using .NET yet, or if you want to access this information from a non-.NET application, I thought creating a native COM library would make it as easy as possible for you. In both cases, the code is fairly simple, although I wrote them slightly differently.

In Visual Basic 6.0, I could have used Win32 API calls to access the registry, but instead I pulled down a little COM library from the Microsoft download center that handles it for me. Using this library, the registry access code wasn't all that different from what I wrote in Visual Basic .NET.

'Visual Basic6 Code
Public Sub RefreshMetaData()
    Dim newMetadataAvailable As Boolean
    Dim newAuthor As String
    Dim newTitle As String
    Dim newAlbum As String
    Dim newDuration As String
    Dim CurrentMetadata As RegObj.RegKey

    CurrentMetadata = _
        RegObj.RegKeyFromHKey( _
            RegObj.HKEY_CURRENT_USER)
    CurrentMetadata = _
        CurrentMetadata.ParseKeyName( _
            "Software\Microsoft\MediaPlayer\CurrentMetadata")

    If CurrentMetadata.Values.Count = 0 Then
        newMetadataAvailable = False
    Else
        newMetadataAvailable = True
        With CurrentMetadata
            newAlbum = CStr(.Values("Album").Value)
            newAuthor = CStr(.Values("Author").Value)
            newDuration = CStr(.Values("durationString").Value)
            newTitle = CStr(.Values("Title").Value)
        End With
    End If

    If (newMetadataAvailable <> m_MetadataAvailable) Or _
        (newAlbum <> Me.Album) Or _
        (newAuthor <> Me.Author) Or _
        (newTitle <> Me.Title) Or _
        (newDuration <> Me.durationString) Then
        'new data
        m_MetadataAvailable = newMetadataAvailable
        m_Album = newAlbum
        m_Author = newAuthor
        m_Title = newTitle
        m_durationString = newDuration
        m_LastRefresh = Now
        OnMediaChange()
    End If
End Sub

In Visual Basic .NET, I used the Microsoft.Registry classes to access the four registry keys and store them into some class properties.

Public Sub RefreshMetaData()
    Dim newMetadataAvailable As Boolean
    Dim newAuthor, newTitle, newAlbum, newDuration As String
    Dim CurrentMetadata As RegistryKey
    CurrentMetadata = Registry.CurrentUser.OpenSubKey _
        ("Software\Microsoft\MediaPlayer\CurrentMetadata", _
         False)
    If CurrentMetadata.GetValueNames.GetLength(0) = 0 Then
        newMetadataAvailable = False
    Else
        newMetadataAvailable = True
    End If

    With CurrentMetadata
        newAlbum = .GetValue("Album", "No Album")
        newAuthor = .GetValue("Author", "No Author")
        newDuration = .GetValue("durationString", "No Duration")
        newTitle = .GetValue("Title", "No Title")
    End With

    If (newMetadataAvailable <> Me.m_MetadataAvailable) OrElse _
        (newAlbum <> Me.Album) OrElse _
        (newAuthor <> Me.Author) OrElse _
        (newTitle <> Me.Title) OrElse _
        (newDuration <> Me.durationString) Then
        'new data
        m_MetadataAvailable = newMetadataAvailable
        m_Album = newAlbum
        m_Author = newAuthor
        m_Title = newTitle
        m_durationString = newDuration
        m_LastRefresh = Now

        Dim mcArgs As New _
            MediaChangedEventArgs(Me.Album, _
                Me.Author, Me.Title, _
                Me.durationString)
        OnMediaChange(mcArgs)
    End If
End Sub

From your actual application(s) you create and use this component (by trapping the MediaChanged event), instead of performing any direct registry access yourself. Using the Visual Basic 6 component works about the same as the .NET component, but their implementation is quite different. The Visual Basic 6 version uses a timer to poll the registry to trap changes, which creates a lot of unnecessary registry access. When I built the Visual Basic .NET version, I was able to make it a bit better by using a background thread combined with a Win32 API call (RegNotifyChangeKeyValue) to monitor the appropriate registry key, so no polling was required. I wrapped up the .NET code for monitoring the registry key into its own class so that you can use it in other projects.

Imports Microsoft.Win32
Imports MonitorRegistry.NativeMethods

Public Class MonitorKey

    Public Event KeyChanged As EventHandler
    Dim m_SubKey As String
    Dim m_RootKey As RegistryKey

    Public Sub New(ByVal key As String, _
        ByVal rootkey As Microsoft.Win32.RegistryKey)
        m_SubKey = key
        m_RootKey = rootkey
    End Sub

    Public Sub StartLoop()
        Do
            WaitForChange()
        Loop
    End Sub

    Private Function WaitForChange() As Boolean
        Dim notifyEvent As New Threading.AutoResetEvent(False)
        Dim KeyHandle As IntPtr
        Dim KeyToMonitor As RegistryKey
        KeyToMonitor = Me.m_RootKey.OpenSubKey _
            (Me.m_SubKey, False)
        Dim regKeyType As Type = GetType(RegistryKey)
        Dim field As System.Reflection.FieldInfo
        field = regKeyType.GetField("hkey", _
            Reflection.BindingFlags.Instance Or _
            Reflection.BindingFlags.NonPublic)
        KeyHandle = CType(field.GetValue(KeyToMonitor), IntPtr)

        Dim err As Integer
        err = NativeMethods.RegNotifyChangeKeyValue(KeyHandle, _
            True, NotifyFilterFlags.REG_NOTIFY_CHANGE_ATTRIBUTES _
            Or NotifyFilterFlags.REG_NOTIFY_CHANGE_LAST_SET _
            Or NotifyFilterFlags.REG_NOTIFY_CHANGE_NAME _
            Or NotifyFilterFlags.REG_NOTIFY_CHANGE_SECURITY, _
            notifyEvent.Handle, True)

        If err = 0 Then
            notifyEvent.WaitOne()
            RaiseEvent KeyChanged(CObj(Me), New EventArgs())
            Return True
        Else
            Debug.WriteLine(err)
            Return False
        End If
    End Function
End Class

If you download the complete code sample, you can see the registry access and monitoring code in context, which makes it quite a bit easier to understand.

Manipulating Messenger

To enable everyone to share in my current musical experience, at least in their imagination, I wrote a little system tray application that connects to the Messenger client using the client's exposed COM library.

Note   I have to provide a clear caveat here; using this automation library is not supported and may cease to function with a future version of Messenger.

In my previous column, I built an application that didn't really have any UI except for an icon in the notification area of the Windows taskbar, and I am going to reuse that same concept for "ListeningTo," my music information utility. Just like in the previous column, I need a little bit of UI in the form of an Options dialog. Using this dialog, I can set up the format for my music-enabled Messenger name, and for my Messenger name for when nothing is playing.

Figure 1. The Option Dialog allows the user to set up their desired Messenger name format.

The code consists of four main sections:

  • Serializing/Deserializing settings to and from a file on the user's machine.
  • Handling menu clicks.
  • Showing the Options dialog.
  • Changing the user's Messenger name.

I will only cover the code for the last section, a routine that is called in response to the MediaChanged event of my library, and then changes the user's Messenger name if needed. For the rest of the functional areas, check out last month's column and the downloadable source for this application.

Imports Messenger
Public Class NameChanger

    Public Shared Sub ChangeName(ByVal newName As String)
        Try
            Dim myMsg As New MsgrObject()
            If Not (myMsg.LocalState = MSTATE.MSTATE_UNKNOWN _
              Or myMsg.LocalState = MSTATE.MSTATE_OFFLINE) Then
                Dim primaryService As IMsgrService
                primaryService = myMsg.Services.PrimaryService
                If primaryService.FriendlyName <> newName Then
                    primaryService.FriendlyName = newName
                End If
            End If
        Catch ex As Exception
            Debug.WriteLine(ex.Message)
        End Try
    End Sub
End Class

To make this code work, I had to add a reference to the Messenger 1.0 Type Library (shown in the COM references dialog) to my project. That is all the Messenger-related code I needed to write. I then used this NameChange class from my main program. Remember, it's not my fault if people think you are crazy once your Messenger name starts changing.

On To E-Mail

The easiest way for me to add "ListeningTo" information into your e-mail was to directly write to your signature files, assuming your e-mail program uses a text or HTML file on disk to store your signature. I'm using Outlook, so I have an RTF, HTML, and text version of my signature stored in my personal profile directory under "C:\Documents and Settings\Duncanma\Application Data\Microsoft\Signatures". The specific location of your signature files will depend on the e-mail program you are using and your personal machine setup. My little utility will work with them wherever they are. I didn't try this sample out with any RTF signatures, but since RTF is essentially just text, it would certainly be possible. Instead of creating a completely new sample, I decided to add some settings to the Option dialog (see Figure 2) of my first utility so that you can specify which signature files you wish to keep updated and what format should be used for information added to each file.

Figure 2. The Signatures section of the Option dialog allows you to configure the program to update one or more signature files.

The settings dialog is more complicated than the actual code in this example, tracking n files and associated settings took a little bit of work. I created a custom collection, like I so often do, to store (and serialize/deserialize) a set of SignatureFile objects. Binding that to a set of controls on my Option dialog was easy enough, and I used an OpenFileDialog to allow you to find your signature file. For simplicity, I replaced the entire signature file's contents with the output of this utility, instead of allowing you to specify what part of the file should be replaced. Whenever I receive a MediaChanged event from the ListeningTo library, I cycle through all of the signature files you have added through the Option dialog and tamper with each one. Yes, you read it right—I said tamper. This isn't a nice friendly touch here; I'm essentially blowing away the contents of your file and rewriting it. So unless you trust me implicitly, I'd back them up before running this.

Public Sub UpdateSignatureFiles()
    Dim sigFile As SignatureFile

    For Each sigFile In appSettings.sigFiles
        Dim newSignature As String
        If musicInfo.MetadataAvailable Then
            newSignature = _
                musicInfo.ToString(sigFile.NameFormat)
        Else
            newSignature = sigFile.NoMusicName
        End If
        sigFile.UpdateFile(newSignature)
    Next
End Sub

If you want to have other content in your signature, just include the rest of your signature text right in the format field. I'm doing exactly that now to produce my signature in this format:

Duncan Mackenzie (MSDN)
(Listening To: Clint Eastwood [Gorillaz / Big Shiny Tunes 6])
mail: duncanma@microsoft.com
web: www.duncanmackenzie.net
blog: dotnetweblogs.com/duncanma

Hot Hot Keys

If e-mail and messenger are not enough for you, I think I've figured out the sample that will handle your needs no matter what you work with: a system-wide keyboard shortcut that copies your current musical experience onto the clipboard for insertion into whatever program you are using. Of course, if you are using a program that doesn't support pasting in from the clipboard, you are out of luck, but that shouldn't be a common issue. To create a keyboard shortcut that can be used at any time involves the use of the Win32 API, specifically the RegisterHotKey API call, and you need a Form available to receive messages through Windows Messenger.

Public Class HotKeyHandler
    Implements IDisposable

    Public Event HotKeyPressed As EventHandler

    Dim m_HotKeyID As Int32
    Dim WithEvents myForm As hkHandler
    Dim atom_String As String

    Public Sub New(ByVal Key As Integer, _
            ByVal ALT As Boolean, _
            ByVal CTRL As Boolean, _
            ByVal SHIFT As Boolean, _
            ByVal WINKEY As Boolean)
        myForm = New hkHandler()
        myForm.Hide()
        Dim atomGUID As Guid = Guid.NewGuid

        m_HotKeyID = NativeMethods.GlobalAddAtom( _
                atomGUID.ToString)
        Dim modifiers As NativeMethods.hkModifiers
        If ALT Then
            modifiers = NativeMethods.hkModifiers.MOD_ALT
        End If
        If CTRL Then
            modifiers = modifiers Or _
                NativeMethods.hkModifiers.MOD_CONTROL
        End If
        If SHIFT Then
            modifiers = modifiers Or _
                NativeMethods.hkModifiers.MOD_SHIFT
        End If
        If WINKEY Then
            modifiers = modifiers Or _
                NativeMethods.hkModifiers.MOD_WIN
        End If
        NativeMethods.RegisterHotKey(myForm.Handle, _
            m_HotKeyID, modifiers, Key)
    End Sub

    Public Sub Dispose() _
            Implements System.IDisposable.Dispose
        NativeMethods.UnregisterHotKey( _
                myForm.Handle, _
                m_HotKeyID)
        NativeMethods.GlobalDeleteAtom( _
                m_HotKeyID)
        myForm.Close()
    End Sub

    Private Sub myForm_HotKeyPressed(ByVal sender As Object, _
            ByVal e As System.EventArgs) _
            Handles myForm.HotKeyPressed
        RaiseEvent HotKeyPressed(CObj(Me), New EventArgs())
    End Sub
End Class

When the keyboard shortcut is pressed, the form you registered will receive a WM_HOTKEY message (which you can catch by overriding WndProc) and then you can use Clipboard.SetDataObject to push your music information onto the clipboard. In my code I have the Form class raise an event up to the HotKeyHandler class, which in turn raises its own event that is then trapped by my main program.

In the hkHandler Form

Public Event HotKeyPressed As EventHandler

Protected Overrides Sub WndProc( _
        ByRef m As System.Windows.Forms.Message)
    If m.Msg = NativeMethods.WM_HOTKEY Then
        RaiseEvent HotKeyPressed(CObj(Me), _
            New EventArgs())
    End If
    MyBase.WndProc(m)
End Sub

In my main program

Dim WithEvents hotKeyHandler As HotKeyUtils.HotKeyHandler

Public Sub hotKeyHandler_HotKeyPressed( _
        ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles hotKeyHandler.HotKeyPressed
    If Not musicInfo Is Nothing Then
        Clipboard.SetDataObject( _
            musicInfo.ToString(appSettings.HotKeyFormat))
    End If
End Sub

By abstracting the creation and handling of the keyboard shortcut from the clipboard code, the HotKeyHandler class should be useable in other projects without any changes to its code.

Need More Windows Media Player Fun?

That is it for this article's sample, but I hope it gives you some ideas for your own code. If you want to see more music-related code, I have uploaded the complete source to my own personal music playing system (built around Windows Media Player) as a GotDotNet Workspace and a User Sample. Download it, check it, and then go build your own... its fun!

Coding Challenge

At the end of some of my Coding4Fun columns, I will have a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create anything related to Windows Media Player (not just Windows Media files). Managed code is preferred (Visual Basic .NET, C#, J#, or Managed C++ please), but an unmanaged component that exposes a COM interface would also be good. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).

Resources

See last month's column for a list of samples from GotDotNet that I used for the system tray icon and for creating a strongly typed collection, but here is a list of selected samples that you might find interesting:

Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!

 

Coding4Fun

Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.