Want to Know What I'm Listening To?
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)
Microsoft® Visual Basic® .NET
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.
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: email@example.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!
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 firstname.lastname@example.org) 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).
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:
- A very cool Data-based TreeView by MadsNissen
- A much-needed FTP component by Vick
- Some great TimeZone code by MichaelRBrumm
Have your own ideas for hobbyist content? Let me know at email@example.com, and happy coding!