One is the Loneliest Number (Matt Gertz)

One is the Loneliest Number (Matt Gertz)

  • Comments 19

(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--*

 

Attachment: VBJukebox.zip
Leave a Comment
  • Please add 6 and 8 and type the answer here:
  • Post
  • PingBack from http://singlesgettogether.info/?p=7471

  • Hello,

    I have run into an interesting problem running VS2008 on a private LAN. Every time I stop debugging a simple Winforms “hello world” application, the IDE hangs for about 10 seconds. I have found this to be very reliable and reproducible across orcas beta and now RTM. I have tracked down the cause and it simple yet confusing. Every time I stop debugging, the IDE (or something related to its main thread) issues a DNS lookup for crl.microsoft.com. Now, I am on a private LAN, so it can’t get any response. It hangs (white-ed out IDE on Vista) until the two DNS queries fail to return. Then happily unloads and allows me to continue working. A simple entry for 127.0.0.1  = crl.microsoft.com in the hosts file cures the problem.

    But why is it doing this. I set up a web server on the private LAN and put in a DNS entry for crl.microsoft.com to see if it did anything after it found an address. It appears to be making a HTTP GET request for crl.microsoft.com/pki/crl/products/CSPCA.crl. Okay, but does it need to do this every time I stop debugging. A search on google for “CSPCA” only found the “Chinese Shar-Pei Club of America”. And who doesn’t love a chubby wrinkly dog…

    Any help with this would be nice. Hacking DNS is nice temporary workaround, but what is going on in the background every time I stop debugging.

    Thanks,

    Aaron

  • Hi, Aaron,

     I checked with the VB team on this; this seems to be a known problem caused by Component One License checking; I'm not aware of a workaround.  (If you're not using Component One controls, then please file a bug at Connect and the team will take a look at it.)

    --Matt--*

  • (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 coupl

  • (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 coupl

  • Well, that was… intense. You may have noticed the lack of articles coming from my direction. I have been

  • This post assumes you are running Win forms application, not WPF. Because in WPF "Make single instance application" option is not available.

  • Hi, Ivan,

    That is true.  However, if you do an Internet search on "single instance application WPF", you will find a number of posts that discuss how to work around this problem, including an MSDN sample at http://msdn.microsoft.com/en-us/library/ms771662.aspx which leverages the Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase functionality.

    Hope this helps!

     --Matt--*

  • Hello,

    I am facing problem in deleting item from My Listbox1(As Playlist) and Axwindowsmediaplayer1 playlist. I am using a button to remove any item from both of them. I was able to remove item from ListBox1 but failed to remove item from AxWindowsMediaPlayer1 playlist.

    I was using these commands

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

           ListBox1.Items.RemoveAt(ListBox1.SelectedIndex)

    AxWindowsMediaPlayer1.currentPlaylist.removeItem(ListBox1.SelectedItem)

    End Sub

    Thanks,

    Faraz

  • Hi, Faraz,

     The command should remove it immediately, even if it's playing.  The only thing I can think of is that the value returned from ListBox1.SelectedItem isn't correct -- that it's not returning an IWMPMedia object but is instead returning whatever the "ToString" representation for that object is (and is not being caught by syntax errors as long as it's an "object."  So, try something like:

    Dim x As IWMPMedia = ListBox1.SelectedItem

    and see if you get an error and/or what's returned from the assignment.  (Maybe it's corrupted, even.)

    --Matt--*

  • Hi Matt,

    I try to use your command but it is giving me this error

    "Object reference not set to an instance of an object."

    Before that i was using the following statement to remove the files from the listbox as well as the playlist

    "

    ListBox1.Items.RemoveAt(ListBox1.SelectedIndex)

    AxWindowsMediaPlayer1.currentPlaylist.removeItem(AxWindowsMediaPlayer1.currentPlaylist.Item(ListBox1.SelectedIndex + 1))

    "

    but there is a bug that it removes the item from the  listbox correctly but from the playlist it only removes the 1st item, not the selected one.

    Thanks for your help.

  • Good, that's what I figured would happen.  You need to refer to the instance via Item and not by Index.  Try removing ListBox1.SelectedItem() and see what happens.

    --Matt--*

  • Nope that doesn't work. Its giving me same error again so i add the new keyword to create instance but that doesn't work too cause "new" couldn't be applied in interface.

  • In that case, it means that you must not be inserting the correct information in the listview.  There are two things for you to check, and this is how I'd do it:

    First, add this code:

     If ListBox1.SelectedItem IsNot Nothing Then

       Dim x As IWMPMedia = ListBox1.SelectedItem

       Stop

     End If

    There are then three possibilities:

    If you don't hit the "Stop" when debugging, then nothing is actually selected, and you'd need to look at that.

    If, instead, the line throws an exception before you get to the stop, then what you're storing isn't a media item, but is something else instead.  For example, maybe you are wrapping the media item inside another class, and inserting that class into the media list.  In that case, you'd need to call through to the actual media item, which would look something like CType(ListBox1.SelectedItem,MySpecialClass).MyMediaItem, replacing "MySpecialClass" and "MyMediaItem" with whatever you called the class and the method to access the media item, respectively.  (Note that I am explicitly casting to the actual object type.)

    Otherwise, if you hit the "Stop" command, then examine x in a debugger window and see what's wrong with it -- did it get corrupted?

    --Matt--*

    If you hit the "Stop," then drag x down to a watch window and see what its value is.  Is is acta

  • (Ignore the last line; I had a copy/paste error.)

Page 1 of 2 (19 items) 12