November 13th, 2007

One is the Loneliest Number (Matt Gertz)

(This post assumes that you’ve read my previous post on Windows Media at http://blogs.msdn.com/vbteam/archive/2007/10/30/let-the-music-play-matt-gertz.aspx – I will be modifying that code in this post.)

After posting my media player blog sample a couple of weeks ago, I got a few questions from a reader called Saleem on how to adapt it to take multiple files as arguments when launching the app.  After a few exchanges, I figured that it made sense for me to write up a post on command line arguments, since it’s actually a really fascinating topic.

The first “given” in this problem is that your application is set to be a single-instance application.  A single instance application is an app which only ever has one instance loaded into memory (hence the title of this blog) – when you double-click a file which is registered to that application, it looks for an existing instance and uses it if available before trying to create a new instance.  Windows Media Player itself is a single instance application – double-clicking a .WMA file while the player is open will use the existing instance rather than creating a new one, so that you don’t get two media players competing for the sound card. J

To make your application single-instance, you’ll need to click a checkbox the project properties.  Right-click on the project in the Solution Explorer and choose “Properties,” and in the “Application” tab put a check in the “Make single instance application” checkbox. 

While you’re on that tab, you should also click the “View Application Events” button – that will bring up a file called “ApplicationEvents.vb”.  That file defines the event handlers for the application itself (not the main form).  Don’t touch anything in that file yet; we’ll get to it later.

Now, the goal of our application will be to allow the user to specify multiple playlists to be randomized — they should all get mixed up together, but arcs (songs that are required to play together) should be preserved.  In my last post, I hard-coded the original playlist path; I now want to modify the app so that it gets the names of the playlists from playlists I invoke.  There are two ways that this might happen:

(1)    Using the command line:  the user opens a command shell and calls the application with a list of playlist files as arguments.  In this case, the filenames are passed as arguments to the application and are available in the Load event of the form (as well as elsewhere).  However, a subsequent call from the command line would then trigger the StartupNextInstance event of our single-instance app, and I’ll have to examine the new command line in that handler to get each of those new filenames.

(2)    Selecting a bunch of file files and pressing “Enter” (or choosing “Open” from the context menu):  in this case, the app is launched with one of the files specified on a command line (usually the last selected), and then Windows attempts to relaunch the app with the names of the other files.  So, in the case where three files are invoked, the first filename is available in the Form_Load event via the My.Application.CommandLineArgs, and then StartupNextInstance gets called twice for each of the two remaining filenames.  Those arguments get retrieved from the EventArgs object passed to the event handler.

So, in order to handle filenames coming from two different sources, I’m going to need to centralize my randomization code from the last post so that both entry points can use it.  Also, I’m going to need to reverse the order in which I do the randomization.  That is, instead of picking a random file from an existing playlist and appending it (or its arc, if any) to a new playlist, I’ll instead go through the old playlist in track order and copy each track (or its arc, if any) to a random location in the new playlist.  If I didn’t do that, then the new playlist won’t be truly random since music from the second playlist will always follow music from the first playlist, etc.

The first thing I’m going to do is move all of my randomization code out of Form1_Load (which I’ve renamed to VBJukeboxForm_Load) and into a new method called MergePlaylist().  The resulting handler will just call MergePlaylist() for each argument that it finds in the command line:

    Private Sub VBJukeboxForm_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        ‘ Make sure we have a random series

        Microsoft.VisualBasic.Randomize()

 

        ‘ Initialization

        Player.settings.autoStart = False ‘ Playlist should not automatically play when added to the player

        Me.SavePlaylistBt.Enabled = False ‘ Don’t enable the “save playlist” button until there’s something to save

 

        ‘ This playlist is initially empty, and we’ll fill it with songs. 

        ‘ You could give the user the option of picking the name

        ‘ by reading it from a label control.

        newplaylist = Me.Player.newPlaylist(“Smart Shuffled Playlist”, “”)

 

        ‘ Merge in the old playlists that were passed via the command line

        For Each Argument In My.Application.CommandLineArgs

            MergePlaylist(Argument)

        Next

 

        ‘ Point player at the new playlist so it can be played

        Player.currentPlaylist = newplaylist

    End Sub

 

Note that I’ve added a button called SavePlaylistBtn to the form.  The user will click that button to save the sorted playlist to the library if desired.  It will be disabled until there are actually songs in the new playlist.  The button’s click handler is very simple:

    Private Sub SavePlaylistBtn_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles SavePlaylistBtn.Click

        ‘ Save new playlist to library and Music\Playlists

        Player.playlistCollection.importPlaylist(newplaylist)

    End Sub

 

Now, I need to merge in playlists for the case where StartupNextInstance is used to “relaunch” the application (or, rather, connect to the existing instance of the app with new filenames).  I’ll insert the following code in the ApplcationEvents.vb file we opened previously, into the My namespace:

    Partial Friend Class MyApplication

        Private Sub MyApplication_StartupNextInstance( _

            ByVal sender As Object, _

            ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs _

        ) Handles Me.StartupNextInstance

 

            For Each s As String In e.CommandLine

                My.Forms.VBJukeboxForm.MergePlaylist(s)

            Next

        End Sub

 

Note that in this case the filenames are passed in by the EventArgs object – we don’t have to query for them from the application, unlike in the Load handler.

MergePlaylist is very much like the code I wrote for the last post, except that we always take songs from the front of the old list and find a random spot to insert them into the new list:

    Sub MergePlaylist(ByVal Argument As String)

        Dim oldplaylist As WMPLib.IWMPPlaylist

 

        ‘ Make sure that the argument points to an actual file

        If My.Computer.FileSystem.FileExists(Argument) Then

            ‘ Get the file inforation and make sure it’s a playlist

            Dim fileData As FileInfo = My.Computer.FileSystem.GetFileInfo(Argument)

            If fileData.Extension = “.wpl” Then

                ‘ Create a copy of the playlist to merge in so that we don’t damage the original.

                ‘ Note that playlists get initialized with URLs.

                oldplaylist = Me.Player.newPlaylist(“Original Sorted Playlist”, “file:///” & Argument)

 

                ‘ Get the number of songs to merge in

                Dim numberOfSongs As Integer = oldplaylist.count

 

                ‘ 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.

                For songsRemaining As Integer = numberOfSongs – 1 To 0 Step -1

                    ‘ Pick the next song from whatever remains in the old list:

                    Dim mediaItem As WMPLib.IWMPMedia = oldplaylist.Item(0)

 

                    ‘ Check the “Part of set” attribute — see http://msdn2.microsoft.com/en-us/library/bb248408.aspx

                    ‘ 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) AndAlso CInt(sPartOfSet) = 1 Then

                        ‘ It’s a number.  We may have an arc of songs here. 

                        ‘ If we run into anything unexpected then just do a normal copy.

                        Dim currentSongToCopy As Integer = 0

                        Dim currentMediaItem As WMPLib.IWMPMedia = mediaItem

                        Dim sCurrentPartOfSet As String = sPartOfSet

 

                        ‘ 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.

                        Dim iPartOfSet As Integer = 0

                        Dim placeToInsert As Integer = FindInsertionLocation()

                        While sCurrentPartOfSet <> “” AndAlso IsNumeric(sCurrentPartOfSet) _

                            AndAlso CInt(sCurrentPartOfSet) = iPartOfSet + 1

                            Insert(placeToInsert + iPartOfSet, currentMediaItem)

                            oldplaylist.removeItem(currentMediaItem)

                            iPartOfSet = iPartOfSet + 1 ‘ Copied one song

 

                            ‘ Check next song if there are any remaining

                            If currentSongToCopy = oldplaylist.count Then Exit While

 

                            currentMediaItem = oldplaylist.Item(currentSongToCopy)

                            sCurrentPartOfSet = currentMediaItem.getItemInfo(“WM/PartOfSet”)

                        End While

 

                        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)

                        Else

                            ‘ Didn’t copy anything yet — must have been a discontinuity. 

                            ‘ Just copy the original song, since user apparently doesn’t care.

                            GoTo NormalCopy

                        End If

                    Else

                        ‘ Just copy like normal

NormalCopy:             Insert(mediaItem)

                        oldplaylist.removeItem(mediaItem)

 

                    End If

                Next

            Else

                ‘ Random file.  Could be clever here and check the file info to see if it’s a media file, and if so

                ‘ insert it via newplaylist.insertItem(FindInsertionLocation(), mediaItem).

                ‘ But that’s left as an exercise to the reader.

            End If

        End If

 

    End Sub

 

That all should look rather familiar from my last post.  Note that I’ve used two new methods called “Insert” to do the actual insertion.  One takes just a media item and is used when dealing with non-arc songs, and the other also takes a place value and is used for arcs, since we want to keep songs in the arcs together (i.e., if the first song of an arc goes to position 7, the next song must go to position 8 – there’s no randomness needed for that case).  The Insert() methods look like this:

    Public Sub Insert(ByVal mediaItem As WMPLib.IWMPMedia)

        Dim place As Integer = FindInsertionLocation()

        Insert(place, mediaItem)

    End Sub

 

    Public Sub Insert(ByVal place As Integer, ByVal mediaItem As WMPLib.IWMPMedia)

        If place = newplaylist.count Then

            ‘ Insert at the end of the list

            newplaylist.appendItem(mediaItem)

        Else

            ‘ Insert exactly at the index indicated, moving other items down

            newplaylist.insertItem(place, mediaItem)

        End If

        Me.SavePlaylistBtn.Enabled = True ‘ We have something in the playlist

    End Sub

 

Finally, we need to define the FindInsertionLocation() method used by the Insert() and MergePlaylist() methods.  Basically, this just involves getting a random number.  However, if that resulting random number points to a location within an arc, we need to crawl backward to the beginning of the arc, since we don’t want to interrupt it.  Furthermore, the random number should be based on the number of songs already in the list plus one, since you can always append after the existing songs as well as inserting in front of them.   Here’s the code:

    Public Function FindInsertionLocation() As Integer

        If newplaylist.count = 0 Then

            ‘ List is empty, so media will go at position 0.

            Return 0

        Else

            ‘ Get a random location from 0 to n, where n is the current number of songs in the playlist

            ‘ (*not* 0 to n-1, because given x existing songs,there are x+1 places to insert a new song

            ‘ — in front of any existing song, plus after them all).

            Dim placeToInsert As Integer = Math.Truncate(Microsoft.VisualBasic.Rnd() * (newplaylist.count + 1))

            If placeToInsert = newplaylist.count Then Return placeToInsert ‘ Insert at end

 

            Dim mediaItem As WMPLib.IWMPMedia = newplaylist.Item(placeToInsert)

            ‘ Check the “Part of set” attribute — see http://msdn2.microsoft.com/en-us/library/bb248408.aspx

            ‘ 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 placeToInsert – (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 currentPlaceToInsert As Integer = placeToInsert – (iPartOfSet – 1)

                    Dim currentMediaItem As WMPLib.IWMPMedia = newplaylist.Item(currentPlaceToInsert)

                    Dim sCurrentPartOfSet As String = currentMediaItem.getItemInfo(“WM/PartOfSet”)

 

                    ‘ Do some error checking here — the attribute had better be “1”

                    If Not sCurrentPartOfSet = “1” Then Return placeToInsert ‘ Partial arc — just use the original number

 

                    ‘ Return the beginning of the arc

                    Return currentPlaceToInsert

                End If

                ‘ Partial arc exists beginning of list — just use the original number

                Return placeToInsert

            End If

            ‘ No interesting information in PartOfSet — just use the original nuber

            Return placeToInsert

        End If

    End Function

 

And that’s pretty much it.  Now, to get the app to work properly when opening it from playlists in File Explorer, there are a couple of ways to proceed:

(1) Register WPL files to your application as the default application instead of Windows Media Player, which you can do from the Tools\Folder Options\File Types in Windows XP or the Folders applet in Windows Vista.  (Don’t forget to change it back when you’re done playing around.)  This can also be done in a setup project you create for your app — use the File Type editor to modify the registration.  Note that users generally don’t like it when installers quietly change the file type registration, so plan accordingly. 

(2) If you’d rather avoid mangling with the default for the file type, you can take a shortcut by simply dragging a set of playlists to your application’s icon, which will force them to be loaded by your app regardless of how the file type is registered on your system.  Running the playlist from the command line will have the same effect, since you’re explicitly specifing your app in that case.

Note that you can even invoke another playlist while your app is already running (via point (1) or (2)) and have that playlist merge into the existing list — very cool indeed. 

The final app is attached below – enjoy!  (I wrote it using a recent build of VS2008, incidentally, though none of the functionality I used is any different than what’s available in VS2005.)  ‘Til next time…

–Matt–*

 

VBJukebox.zip

0 comments