A group blog from members of the VB team
Oh, boy. Sorry I haven’t written any posts lately, but I’ve transitioned to a new job within Visual Studio and have been getting my sea legs there. One of the job tasks is getting Visual Studio 2008 out to you folks, and while I’ve always been involved with that aspect of the product in the past, it was always to a lesser degree. I could best describe each day now as “23 hours of nervous tension followed by one hour of utter panic” as we knock down the last few things that would otherwise keep us from shipping on time.
(Incidentally, in the spirit of making sure that the product works properly before we ship it, I’ve switched to writing code against Visual Studio 2008 using a build from last week. Everything in this post, though, will still work using Visual Studio 2005, provided you have the current Windows Media Player.)
So, anyway, going forward I hope to do a post or two each month. We’ll see how it goes, but it’s awfully hard to keep me from writing about Visual Basic, so I’m optimistic. J And on that note, on to today’s topic!
I’m a big music fan, with quite a large collection of songs that I’ve ripped onto my computer. One of the things that I really appreciate about playing music on my PC is that I can listen to random tracks as if I were listening to a radio station (a radio station that always plays what I like and has no chatter) – I don’t hear the same old thing in the same old order every time. The shuffle function gets a lot of use from me.
There’s a difference between “shuffle” and “random” function, of course. “Shuffle” generates a random list from an existing list and then plays that temporary list from beginning to end (so as to avoid replays), whereas with “random,” the next song is calculated on the fly. Unfortunately, statistics being what they are, you’ve got a good chance in the latter case of hearing the same song twice (or more) in a session before all of the songs have been heard.
Most good playback engines on PCs and cars do “shuffle” instead of “random” these days because of this, but shuffle isn’t a panacea, either. The problem is that there are songs which are meant to be played adjacent to another song or else they don’t make sense. The disc authors will break up two songs on a track which (in the listener’s opinion) are really the part of same song. Take, for example, Pink Floyd’s excellent “Dark Side of the Moon” album, which is replete with this sort of issue. Really, is there anyone out there who would listen to track 8 (“Brain Damage”) without listening to track 9 (“Eclipse”)? Not likely; it’s a jarring experience to just have the first track end abruptly.
So, what happens? I throw my favorite songs from various artists into a playlist. I sync that to my music player and head out to mow the lawn, hitting “shuffle” so I'm not always hearing the same thing. Sure enough, after a few minutes, I'm enjoying the last few lines of Jackson Browne’s “The Load-Out”:
But we’ll be scheduled to appear
A thousand miles away from here…
Is this the real life? Is this just fantasy?
Caught in a landslide…
Mentally, I was expecting the track to continue into Jackson Browne’s “Stay” instead of randomly switching to Queen’s “Bohemian Rhapsody,” and, although I do like the latter song, the transition is pretty darn jarring.
What I really want is a way to shuffle my tracks to get that nice sense of randomness, but also keep certain songs together to prevent any jarring playback from inappropriate segues. I’ll code that solution up in this blog, but first I’ll need to cover some of the basics of how you write code against the Windows Media Player.
The Windows Media Player control is a very powerful control with a lot of functionality, but understanding its object model involves a bit of a learning curve because there’s quite a lot to it. Fortunately, once you understand the basics, it’s pretty easy to work with. There are four really important concepts:
1. The player itself. Being a COM control, it gets wrapped for .NET so that you can write code against it without resorting to Declares, etc., and ends up being an object of type AxWMPLib.AxWindowsMediaPlayer. Properties on the player include which of its controls show and its visibility, whether it auto-plays (via its “Settings” property), actions such as Play and Stop (via the Ctlcontrols property), and so on.
2. The playlist. This is the object that tells the player what media it should play. You don’t “Dim” or “New” a playlist; instead, you ask the player for a new or existing playlist, and it returns an interface of type WMPLib.IWMPPlaylist to the result, on which you can then call methods.
3. The media. Again, these are not objects that you create yourself. There are methods on the player and the playlist which will return interface values (WMPLib.IWMPMedia) for them. The playlist is made up of media objects.
Once you’ve got that in hand, the rest is pretty easy – honest! Let’s run through some examples here.
The “Hello, world!” of a music player would be, of course, playing a song. First, create a new Windows Application and make sure that the toolbox is displayed for the resulting form. We’ll need to add the Windows Media control to the toolbox, so right-click on the toolbox and select “Choose Items…” In the resulting dialog, navigate to the “COM Components” tab, scroll down towards the bottom, and check the box to the left of the Windows Media Player. Press “OK,” and the Windows Media Player (which I will henceforth refer to as the WMP) will be added to your toolbox. Drag an instance of it over to your form and size it however you like. (By the way, I’m using the most recent version of WMP, which is version 11 and which you can download for free from Microsoft if you don’t already have it. Earlier versions of the Windows Media Player might not support this all of this code, depending on how old they are.)
While the WMP is still selected, go to the property grid and change its name to “Player” (as the default name is quite a mouthful otherwise). Now, double-click on the form background (not the WMP) to create the “Form1_Load” event.
In Form1_Load, we’ll create a new playlist for the WMP to play:
Dim playlist As WMPLib.IWMPPlaylist
playlist = Me.Player.newPlaylist("My groovy playlist", "")
The first argument to newPlaylist is the name of the new playlist, and the second is a URL to an existing Playlist with whose contents we want to initialize it. I’ve left it as the empty string because in this example I want to start with an empty playlist. (Note that the new playlist is not automatically added to your library, so don’t worry about cluttering up your library here. To actually make the playlist permanent, you’d need to call either importPlaylist or newPlaylist from the IWMPPlaylistCollection returned from Player.playlistCollection property.)
Now, I want to add a song to the new playlist:
Dim item As WMPLib.IWMPMedia = Player.newMedia("file:///C:\Users\Matt\Music\Dead Can Dance\Spleen and Ideal\08 Avatar.wma")
The argument to newMedia is a URL, so if I’m using a file from disk, I need to use a URL format (basically, prepend “file:///” to the absolute path in this case). Of course, I normally wouldn’t hardcode this path; I’d read the file name from a file dialog, but I’ve already covered file dialog usage in a previous post so I’ll skip it here for clarity’s sake.
Next, I’ll add the playlist to the player:
Player.currentPlaylist = playlist
Now, let’s press F5. The application launches and, assuming that there’s no mistake in URL we specified, the music automatically starts playing. You can use the WMP controls to control volume and so forth.
Now, it’s possible that you don’t want the music to play until the user actually pushes the “Play” button. This is easy enough to do – simply add this line somewhere *before* you assign the playlist to the player:
Player.settings.autoStart = False
And now you’ve got a way to play any media file (or set of media files) from inside your Windows application, with the user in full control of the playback. Cool, huh?
Let’s start out with the following code in Form1_Load:
Player.settings.autoStart = False ' Otherwise, playlists will automatically play when added to the player
' Create a new playlist
Dim oldplaylist As WMPLib.IWMPPlaylist
Dim newplaylist As WMPLib.IWMPPlaylist
oldplaylist = Me.Player.newPlaylist("Original Sorted Playlist", "file:///c:\Users\Matt\Music\Playlists\One True Playlist.wpl")
newplaylist = Me.Player.newPlaylist("Smart Shuffled Playlist", "")
Note that I’m creating two playlists here – one which identically matches a favorite playlist of mine, and one which is empty. I’ll use the empty one to store my smartly-shuffled playlist. Note that I could use Player.playlistCollection.getByName(“One True Playlist”).Item(0) to point to the existing playlist instead of a copy, but since I’m going to be removing media items from one list and moving them to another, that would be destructive to the original. (I could also have sorted within one copied list like I did with card shuffling in an earlier blog post, and avoid even using a second list, but since I’m reusing most of the memory here anyway – that is, the media objects – I’m opting for a more readable implementation this time. Either would work.) Again, I would normally use a file dialog to browse to the playlist rather than hard-coding it – I’m just trying to keep it simple here.
I’ll need a random number generator to pick songs to pull over. If you’ve read my earlier Euchre blog post, you’ll know I usually use a complex one to guarantee the best distribution I can get; however, this being just for a music player, I’ll go with plain-old Random() for brevity’s sake. For the range of the random number, I’ll need to know how many songs we’ll be moving, which I can get from the playlist count. For this first attempt, I’ll just shuffle without regard to disjointed songs:
' Randomize the values using system time as a seed
' Get the number of songs to use
Dim numberOfSongs As Integer = oldplaylist.count
' The value i will keep track of the number of songs left to copy,
' which in turn helps us keep track of the range for valid random numbers.
For songsRemaining As Integer = numberOfSongs - 1 To 0 Step -1
' Pick a random song from whatever remains in the old list:
Dim SongToCopy As Integer = Microsoft.VisualBasic.Rnd() * songsRemaining
Dim mediaItem = oldplaylist.Item(SongToCopy)
' Append it to the new list
' Remove it from the old list, which will have its count decrease
Player.currentPlaylist = newplaylist
The variable “songsRemaining” is pulling double-duty here – it makes sure (via the For loop) that I copy over exactly as many songs as possible, and it also constrains the random variable to however songs are remaining in the initial list.
That code works fine for a simple shuffle (go ahead & try it, using one of your own playlists!), but it doesn’t do anything that Windows Media Player can’t already do. So, now I want to massage this code into something which keeps certain songs together. My general plan will be to have some sort of “tag” on songs to indicate that they below with another song, and then if I encounter a random media item which is part of that duo (or trio, or whatever), I’ll bring the others along as well, in the proper order. The trick will be figuring out what tag to use. Fortunately, there are some custom fields associated with media items that I can leverage here.
Ideally, the information that gets downloaded with songs would pre-populate some field which would indicate that one song always belongs with another, but that unfortunately isn’t the case. So, we’ll have to do it ourselves. If you navigate to your music collection in File Explorer, you can right-click on a song and bring up a property sheet for it. On the "Details" tab, towards the bottom, you’ll notice a property called “Part of set”. We’re going to leverage that property to track that “togetherness” for an arc of songs. (I could just as easily have leveraged the “Comments” property, for example, but using this one made more sense to me and as far as I could tell, none of my other songs were using it for something else.) To set the property, first find an album which has songs that should play together. For example, I’m going to use The Moody Blues’ release “Long Distance Voyager ,” which includes an arc of three songs at the end of the album which should always go together (“Painted Smile,” “Reflective Smile,” and “Veteran Cosmic Rocker”). Right-clicking on the first song in the arc, I’ll set its “Part of set” field to 1 and apply the changes, then bring up the properties for the next song and set the field to 2, and then finally 3 for the last song of the arc.
I’ll then go into Windows Media (the desktop one, not the control you added) and create a playlist called “Test” which includes all of the songs from that album for my experiment. You can do this by clicking “Create playlist” on the left side of the player, giving it the title "Test," dragging the appropriate songs to the resulting playlist editor on the right, and then clicking "Save playlist." That’s the playlist I’ll be copying from – you can, of course, start with any playlist you like, but I wanted to keep the number of songs small while debugging the app. (The playlist I used in the previous example does include the aforementioned Moody Blues tracks, but it also has about 600 other songs and thus is not the best choice to use when stepping through the debugger, as you might imagine.)
Now, in my code, I can check for the presence of one of the “Part of set” values and react by copying the adjacent songs in order as well as the randomly selected song. Note that I’m assuming that the original list is sorted in track order per each album (or partial album) included on the playlist – if not, you’ve got to do some additional work to look up the album information and see what should be kept adjacent to each other, whether it's all in the playlist somewhere, etc., all of which is an exercise that I’ll leave to the reader. For my part, I’ll just check to make sure that the numbers are used contiguously and, if not, default to the “normal” copy behavior. Here’s the final code; I’ll let the comments speak for themselves:
Dim oldplaylist As WMPLib.IWMPPlaylist
' You should really use a file dialog to get this path to the existing playlist.
oldplaylist = Me.Player.newPlaylist("Original Sorted Playlist", _
' This playlist is initially empty, and we’ll fill it with songs.
' You could give the user the option of picking the name for it
' by reading it from a label control.
' Randomize the values using system time as a seed
' The value songsRemaining will keep track of
' the number of songs left to copy,
' which in turn helps us keep track of the range
' for valid random numbers.
Dim songToCopy As Integer = _
Microsoft.VisualBasic.Rnd() * songsRemaining
Dim mediaItem As WMPLib.IWMPMedia = oldplaylist.Item(songToCopy)
' Check the "Part of set" attribute -- see
' for a list of attributes for different media types.
Dim sPartOfSet As String = mediaItem.getItemInfo("WM/PartOfSet")
' See if the value is a number.
If sPartOfSet <> "" AndAlso IsNumeric(sPartOfSet) Then
' It's a number. We may have an arc of songs here.
' Get the number and rewind to the first one.
' If we run into anything unexpected then just
' do a normal copy.
Dim iPartOfSet As Integer = sPartOfSet
' Make sure we don't go past the beginning of the list!
If songToCopy - (iPartOfSet - 1) >= 0 Then
' Rewind to what should be the beginning of
' the arc and get the song. (Hopefully, they’re all
' there, but I’m not going to count on them being all
' all there and initially in the right order. My
' default when confused will be to just copy the
' originally picked song as if it wasn’t part of an arc.)
Dim currentSongToCopy As Integer = _
songToCopy - (iPartOfSet - 1)
Dim currentMediaItem As WMPLib.IWMPMedia = _
Dim sCurrentPartOfSet As String = _
' Do some error checking here -- the attribute had
' better be "1" – otherwise, do a normal copy instead
If Not sCurrentPartOfSet = "1" Then GoTo NormalCopy
' OK, we're probably good to go, unless we coincidentally
' got the beginning of another arc instead
' due to discontinuous numbers. Worse thing that happens
' in that case is that we just copy a different
' arc, and we’ll pick up these pieces later.
iPartOfSet = 0
While sCurrentPartOfSet <> "" _
AndAlso IsNumeric(sCurrentPartOfSet) _
AndAlso CInt(sCurrentPartOfSet) = iPartOfSet + 1
iPartOfSet = iPartOfSet + 1 ' Copied one song
' Check next song if there are any remaining
If currentSongToCopy = oldplaylist.count Then
currentMediaItem = _ oldplaylist.Item(currentSongToCopy)
sCurrentPartOfSet = currentMediaItem.getItemInfo("WM/PartOfSet")
If iPartOfSet > 0 Then
' We may have copied more than one song.
' Update the For loop variable
' appropriately to compensate, since For loop
' will only decrement by one.
songsRemaining = songsRemaining - (iPartOfSet - 1)
' Didn't copy anything -- must have been a
' discontinuity. Just copy the original song,
' since user apparently doesn't care.
' Error -- starting songs not available. Just copy
' the song, since user apparently doesn't care.
' Just copy like normal
' Point player at the new playlist so it can be played
Immediately on hitting F5, you can play the songs in the new order on your PC, without fear of bizarre transitions from one song to the other. However, to "take it on the road," you could also use the following code to add the playlist to your library and your file system:
' Save new playlist to library and Music\Playlists
You could then use that playlist to sync to your music player or, if your music is already on a flash card, just copy that resulting WPL file to your flash card so that your Windows Media supporting device can play your songs in the new shuffled order.
So, play your party mixes or head out to jog knowing that you can get a pleasant randomness in your playlists without those jarring segues between unconnected songs! ‘Til next time…
(Slight typo: I said "...pointing to the actual full filename..." As I mentioned later, the path contained int he playlist is actually a relative path -- the point I was trying to make was that the filename was just a filename and as long as it matches what's on disk, you're OK.)
Thanks for your rapid and useful reply as the problem is now solved.
The problem, I discovered by close inspection of the two playlists in Notepad, was that my(!) code was giving the wrong track number in reassembling the WMA file name.
Also, that track number now appears in the left most column as in other WMP generated playlists. The playlist now plays without the errror message.
Once again, many thanks
Excellent! Glad to be of assistance and that your code works now.
Does anyone know if a recent MS update has affected the WMP SDK? (I'm on Windows 7)
I have a vbscript that now errors on Set objNewList = objPlayer.PlaylistCollection.newPlaylist(strMixName). Error is "Invalid procedure call or argument" Code: 800A0005
Similarly, when I put the code in this blog post into either VB Studio 2005 or 2008, it throws an ArgumentException error ("Value does not fall within the expected range.") on the last line, Player.playlistCollection.importPlaylist(newplaylist) in order to create a permanent playlist.
Thanks in advance
I'm not aware of any updates that would have created a breaking change in the WMP object model. Unfortunately, I can't try this out at the moment! My hobby machine is, alas, in for repairs (and has been for the last month... every time they think they've got it fixed, they find another problem in the stress tests... grrr). When I get it back, I'll try this out to make sure it still works for me, since I haven't tried it for a while.
In the meantime, I'd suggest using the Object Browser to make sure that something screwy didn't happen to the reference and that all of the methods are there. But for the error you're getting for 2005 and 2008, it sounds like something completely different... like the newplaylist you created is mangled. Check the value in the watch window and expand it to make sure it's "something" and populated properly -- if not, then work backwards to figure out where it got reset to Nothing.
Thanks for the quick response, Matt. Using Debug, "newplaylist" is a valid playlist object and has a "count" property equal to the playlist count of the "oldplaylist"
Good luck with your hobby machine.
Hmm. Well, luckily, the UPS site tells me that my machine just got delivered to me at home, so I should be able to give this a whirl -- I will postpone my urge to jump right back into "Dragon Age: Awakening" :-) and, after ascertaining that the machine actually works & making sure that it is updated, will run my app to see if there are any problems...
(Didn't get to this last night... my machine's video is indeed fixed now, but the sound card is now out of whack and I spent most of last night wrestling with that. IGuh... 'm going to pick up a new card today and then, assuming that works and the whole MB didn't get mangled, will be able to answer your question.)
No worries. Thanks and good luck.
OK, sorry for the delay -- my machine's been a mess (and is still having some problems with reaching "post" on the boot cycle, alas). I was finally able to give this a run last night on my Win7 machine at home, running VS2008/.NET 4, fully updated. Everything worked like it used to -- I was able to read media items, play songs, and create playlists all through the APIs -- so I'm not running into the problem that you are seeing. Next step would probably be to head over to the WMP forums and see if they can guide you.
Hey Matt. Thanks for trying.
I think I might have stumbled on to what the issue might be. I believe Playlists are created in the path that you have set for your "Rip music to.. " path. I just noticed that my path for this in WMP 12 is blank and I cannot change it. Clicking the Change button does nothing. It makes sense that if the default playlist path is not valid, I'd get an error trying to programmatically save a new playlist.
Now my issue is that I cannot set the Rip path to a valid location to see if this addresses my orignial issue.
That would certainly cause the problem you are seeing. You should definitely be able to change your rip music location (I can change mine; I just tried it on my work machine which is Win7, 64-bit, up-to-date). I did some internet searching on Bing, and a one other person had run into this, and the solution seems to be to right-click the WMP shortcut, choose "Run as Administrator..." and then change it. (That then begs the question as to how it got into a state where a normal user cannot change that location, but that's not something I'm knowledgeable about; you'd want to check on the Windows board. I did some searching on how that might have gotten turned off but had no luck.) If your WMP shortcut is pinned in the taskbar, you'd actually right-click the pinned shortcut, and then right-click the "Windows Media Player" menu item that results, and *then* click "Run As Administrator."
There's also another (more unfortunate) possibility -- you may have another player installed that hijacks that functionality for itself. So, I hope the problem can be solved by "Run As Administrator," because otherwise you'll have to figure out which program is cutting you off at the knees.
Finally, I learned in my searching that there were some similar problems with Tools options in WMP12 not workgin correctly (greyed out, etc.) in the Windows7 beta. It's just possible that, if you upgraded from a beta or release candidate of Win7 directly to the final version without a clean install (there were apparently ways to do this via setting on registry keys), you might have retained such binaries (and ergo such behavior). Again, you'd have to talk to the WMP guys probably in such a case.
On an administrative note: as I type this, I am just over three hours from going away on my sabbatical -- I will be travelling on vacation for just over ten weeks (!) -- off to Myrtle Beach, Southern Pines, the Appalachians, England, Wales, Canada -- and lots of golf & fishing in between all of that. So, I regret that I'm not going to be able to answer followups until August 31 at the earliest!
So I did what you suggested, Run As Admin - didn't work. My OS is not a beta or RC upgrade, but the real deal. I uninstall some other third party media players, just in case, still nothing.
I even removed & added Media Player itself and still no fix.
Then, I came across this thread: social.answers.microsoft.com/.../dcc6644f-9122-45b2-be4f-6d0fe9e9056d and one of the commenters gave me an idea. My "Music" library contained just one path, "D\Music" where I store all music files and playlists. I had removed the default library location "C:\users\<username>\appdata\..." since I don't store anything there. Once I added the default folder back in, the Rip Path was there again and I could click the Change button to move it. And, best of all, my script and .NET apps work again :-D
Thanks for your help and time. Enjoy your wonderful sounding vacation. Wave to Western Canada (where I am) on your way through ;)
Glad to hear it! That all makes sense now. I'll wave from Victoria BC... :-)