Tutorials/MP3Terminal/Page3

From NeoAxis Engine Wiki

Jump to: navigation, search

Contents

Media Player Terminal: Adding a Playlist

Introduction

Alright, if you made it this far, then good for you. I wouldn't say the hardest part is over, in fact it is yet to come, but having this basic example working means all that is left is to build upon that code and gui to add more and more functionality. In this section, we are going to go from loading a file from a hard coded string, to loading a playlist from a config file. By the end of this we will have the ability to play (as before), pause and stop a track as well as move through the playlist with next and previous track buttons and by double clicking on a playlist item. As we did wit the previous section we will have 3 steps:

  • The C# code: We will take a look at the MediaPlayer class in its entirety and I will (attempt) to explain what is going on
  • The GUI: From this point, since i showed you how to add controls to a gui, i will show a screen shot and label the elements with that names you should give them
  • The logic editor: We will take a look at the logic editor code and how we hook it all together in game

Enough of my yapping, lets get to it

Step 1: The C# Code

using System;
using Un4seen.Bass;
using Engine.FileSystem;
using System.Collections.Generic;
using Engine.UISystem;
using Engine.Utils;

namespace GameEntities
{
   public class PlaylistItem
   {
       private string name;
       public string Name
       {
           get { return name; }
           set { name = value; }
       }

       private string path;
       public string Path
       {
           get { return path; }
           set { path = value; }
       }
   }

   public static class MediaPlayer
   {
       public enum PlaybackState
       {
           Playing,
           Paused,
           Failed
       }

       private static bool initialized = false;
       public static bool Initialized
       {
           get { return initialized; }
       }

       private static List<PlaylistItem> playlist = new List<PlaylistItem>();
       public static List<PlaylistItem> Playlist
       {
           get { return playlist; }
       }

       private static int currentPlaylistIndex = -1;
       public static int CurrentPlaylistIndex
       {
           get { return currentPlaylistIndex; }
       }

       private static int stream = 0;

       public static void Initialize()
       {
           BassNet.Registration("angrywasp@iinet.net.au", "2X314252229298");
           initialized = Bass.BASS_Init(-1, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero, null);
       }

       public static void BuildPlaylist(string playlistFile, EListBox list)
       {
           List<TextBlock> children =
               TextBlockUtils.LoadFromVirtualFile(playlistFile).FindChild("Tracks").Children;

           foreach (TextBlock block in children)
           {
               PlaylistItem item = new PlaylistItem();
               item.Name = block.GetAttribute("name");
               item.Path = block.GetAttribute("path");
               playlist.Add(item);
           }

           foreach (PlaylistItem pi in playlist)
               list.Items.Add(pi.Name);
       }

       public static PlaybackState Play(int playlistIndex)
       {
           if (!initialized)
               return PlaybackState.Failed;

           switch (Bass.BASS_ChannelIsActive(stream))
           {
               case BASSActive.BASS_ACTIVE_PAUSED:
                   {
                       Bass.BASS_ChannelPlay(stream, false);
                       return PlaybackState.Playing;
                   }
               case BASSActive.BASS_ACTIVE_PLAYING:
                   {
                       Bass.BASS_ChannelPause(stream);
                       return PlaybackState.Paused;
                   }
               case BASSActive.BASS_ACTIVE_STALLED:
                   {
                       Stop();
                       return PlaybackState.Failed;
                   }
               case BASSActive.BASS_ACTIVE_STOPPED:
                   {
                       stream = Bass.BASS_StreamCreateFile(
                           VirtualFileSystem.GetRealPathByVirtual(playlist[playlistIndex].Path),
                           0, 0, BASSFlag.BASS_STREAM_AUTOFREE);
                       if (Bass.BASS_ChannelPlay(stream, true))
                       {
                           currentPlaylistIndex = playlistIndex;
                           return PlaybackState.Playing;
                       }
                       else
                           return PlaybackState.Failed;
                   }
           }

           //every possible state of the stream is covered above so
           //this is more so that the c# compiler can't 
           //complain that we are not returning a value on all paths
           return PlaybackState.Failed;
       }

       public static PlaybackState PlayNext()
       {
           int next = currentPlaylistIndex + 1;

           if (next >= playlist.Count)
               next = 0;

           Stop();
           return Play(next);
       }

       public static PlaybackState PlayPrevious()
       {
           int previous = currentPlaylistIndex - 1;

           if (previous < 0)
               previous = 0;

           Stop();
           return Play(previous);
       }

       public static void Stop()
       {
           Bass.BASS_StreamFree(stream);
           stream = 0;
       }
   }
}

As you can see we have added a few properties, methods and a new class and enumeration as well as changes to the Initialize and Play methods.

The PlaybackState enum is used to return a value indicating if the stream is playing, paused or failed. This is returned from the Play method and we will see what we do with it when we look at the logic editor code.

The PlaylistItem class is a basic class to hold the name and path of the mp3 file in the playlist. While it is possible with Bass.NET to get the name of the track from the mp3's ID3 tags, entering it this way is more conservative on resources, as well as simplifying the code immensely. Using this class is the playlist property that consists of a list of instances of the PlaylistItem class. Now would be a good time to take a look at the structure of the playlist.config file

Tracks
{
 Track
 {
   name = Iron Maiden: Paschendale
   path = Sounds\MP3\Metal\Paschendale.mp3
 }
 Track
 {
   name = Underoath: To Whom It May Concern
   path = Sounds\MP3\Metal\To Whom It May Concern.mp3
 }
}

Notice we have a root element called 'Tracks' and each entry in the playlist is encapsulated in a 'Track' block. We will see how to get these values out of the config file when we look at the BuildPlaylist method.

The other 2 properties other than the playlist property are:

  • int CurrentPlaylistIndex: This property holds the index of the currently playing track in the playlist. It is used for determining the previous/next track in the playlist
  • bool Initialized: When we initialize Bass.NET in the Initialize method, we assign the result of this initialization to the Initialized property. This way if you change between maps that have media player terminals, we do not initialize Bass.NET if it has been done previously.

We may as well look at the methods individually.

void BuildPlaylist(string playlistFile, EListBox list)

OK. This is our first taste of TextBlock. This method accepts 2 parameters

  • string playlistFile: A virtual path to the playlist file.
  • EListBox list: the gui control that will display our playlist

For thise who don't know, virtual paths start from the data directory, so where my file is stored in "Data\Sounds\MP3\playlist.config", I would pass in "Sounds\MP3\playlist.config" as the playlistFile parameter. This file is then fed into this line of code

List<TextBlock> children =
               TextBlockUtils.LoadFromVirtualFile(playlistFile).FindChild("Tracks").Children;

You can see that we are loading from a virtual file and then looking for the child "Tracks". This translates to the root block of the file (see above) and then from that block we get all the children and assign them to a list. Pretty easy, but doesn't include error checking for existsnce of the file etc. I am pretty lazy that way, preferring instead to just make sure the file exists. You can look into the docs and create whatever error checking you feel necessary.

Next up is this bit of code

foreach (TextBlock block in children)
{
    PlaylistItem item = new PlaylistItem();
    item.Name = block.GetAttribute("name");
    item.Path = block.GetAttribute("path");
    playlist.Add(item);
}

we extract the name and path attributes from each child block and create a new playlist entry. Again, no error checking (implement at your leisure). Finally we then loop through the playlist and add the name of each entry as an item in the EListBox we passed in as the second parameter, and that's how we get a visual of our playlist.

PlaybackState Play(int playlistIndex)

This method now accepts a single parameter

  • int playlistIndex: this is the index in the playlist to play

It has taken some heavy modification since we last saw it. Mostly all we are doing is implementing a switch of all the available states of the stream and doing something in each case. Doing so has allowed us to implement track pausing. When the track is playing, if we call this method, the track will be paused, and vice versa. If playback has stalled and we call Play, it will stop the track and return a failed message. If it has stopped, then we play whatever track the playlistIndex value applies to in the playlist. When we start playing a new song, the value of CurrentPlaylistIndex is updated to reflect what we are playing. The switch cases are pretty self explanatory, so i don't think i need to say more.

PlaybackState PlayNext()

All this method does is get the value of CurrentPlaylistIndex, increment it, and pass it into a Play call. If that incremented value is outside the range of the list, we loop around and go back to the start.

PlaybackState PlayPrevious()

This method does basically the same as PlayNext, only decrementing CurrentPlaylistIndex. If decrementing results in a value less than 0 we set the value to 0 and pass it into Play. We don't loop back to the last song in the list here, cause it would be annoying to start at the first song, go back to the last song then have it go again to the first song when it finished (if any of that makes sense).

void Stop()

Stop does exactly what stop is meant to do. We make a call to Bass.BASS_StreamFree which frees all resources associated with the stream we pass in as a parameter.

You may notice that when calling PlayNext and PlayPrevious that we make a call to Stop before a call to Play. This ensures that the stream is freed and that the stream is in a stopped state so when we iterate through the switch, we go into the BASSActive.BASS_ACTIVE_STOPPED case and start the new song. If using this code, you should always make a call to Stop before trying to change the track.

And that's it for the new code. Still pretty simple, but i hope to change that in the next section. Now we should move onto the gui

Step 2: The Terminal GUI

OK, by now you are familiar with how to add gui controls and edit their properties, so below is a screen shot of what i have and the names i gave to the controls. Using the same names will make it easier in the future

The layout of my terminal gui. Do what you like, just make sure you have all the controls

Well a picture tells a thousand words, so lets move onto the logic editor

Step 3: The Logic Editor

Firstly lets take a look at the PostCreated method

//assign to the control variables
btnPlay = Owner.MainControl.Controls["btnPlay"] as EButton;
btnStop = Owner.MainControl.Controls["btnStop"] as EButton;
btnNext = Owner.MainControl.Controls["btnNext"] as EButton;
btnPrevious = Owner.MainControl.Controls["btnPrevious"] as EButton;

lbxPlaylist = Owner.MainControl.Controls["lbxPlaylist"] as EListBox;

//subscribe to necessary event handlers
btnPlay.Click += btnPlay_Click;
btnStop.Click += btnStop_Click;
btnNext.Click += btnNext_Click;
btnPrevious.Click += btnPrevious_Click;
lbxPlaylist.ItemMouseDoubleClick += lbxPlaylist_ItemMouseDoubleClick;

//check if the Bass.NET is initialized and do it if it isn't
if (!MediaPlayer.Initialized)
	MediaPlayer.Initialize();

MediaPlayer.BuildPlaylist("Sounds\\MP3\\playlist.config", lbxPlaylist);

//set the selected index of the playlist to 0
lbxPlaylist.SelectedIndex = 0;

A few changes from last time, but if you understood what we did on the last page this should be OK. All we have done is declared the other buttons (stop, previous and next) and declared the playlist control, but with one difference. Have you noticed yet that we don't declare the gui control when we assign to it. If you have good on you, go get a cookie. We are going to move the declaration of these controls to local variables, so we can get to them from anywhere in the class. Creating a variable is easy, just select "Create Variable" from the context menu. In the next dialog give it a name and select the type from the drop down. Create 5 for the controls we currently have

  • EButton btnPlay
  • EButton btnStop
  • EButton btnPrevious
  • EButton btnNext
  • EButton lbxPlaylist

We have assigned event handlers for the click event to all the buttons. I showed you how to do that on the last page so do that now and come back.

Well, that was quick. Next we need to create an event handler for the playlist control when an item in the list is double clicked. We don't create the event handler in the same way as the buttons due to a bug i reported in this forum thread. Good news for us though is that we have a solution. What you need to do is create a 'Custom Script Code' file. Just select "Create Custom Script Code" from the context menu.

Create the event handler in the custom script code like so

public void lbxPlaylist_ItemMouseDoubleClick(object sender, EListBox.ItemMouseEventArgs e)
{
	MediaPlayer.Stop();
	MediaPlayer.Play(e.ItemIndex);
}

That method will stop the currently playing track and start whatever track you clicked on by getting the index of the item you clicked.

We will use the Custom Script code one more time which i will get to.

The button Click event handlers are very similar, so I think we will just take a look at the code for all of them and then talk about it briefly (because it is rather simple code) at the end

btnPlay_Click

UpdatePlayButtonText(MediaPlayer.Play(lbxPlaylist.SelectedIndex));

btnNext_Click

UpdatePlayButtonText(MediaPlayer.PlayNext());
lbxPlaylist.SelectedIndex = MediaPlayer.CurrentPlaylistIndex;

btnPrevious_Click

UpdatePlayButtonText(MediaPlayer.PlayPrevious());
lbxPlaylist.SelectedIndex = MediaPlayer.CurrentPlaylistIndex;

btnStop_Click

MediaPlayer.Stop();
btnPlay.Text = "Play";

You will notice the reoccurance of this UpdatePlayButtonText method, so we may as well have a look at that now as well. Place this method in your custom script code along with the lbxPlaylist_ItemMouseDoubleClick event handler

public void UpdatePlayButtonText(MediaPlayer.PlaybackState state)
{
         if (state == MediaPlayer.PlaybackState.Playing)
                 btnPlay.Text = "Pause";
         else if (state == MediaPlayer.PlaybackState.Paused)
                 btnPlay.Text = "Play";
}

Now we have all the code out we get a clearer picture. The UpdatePlayButtonText method accepts a MediaPlayer.PlaybackState parameter, that same that is returned from the Play, PlayNext and PlayPrevious methods in our code. I told you we would look later at what the return type did, well sorry for the anticlimax, but it just updates the text on the play button. in the next and previous handlers we also update the selected index of the playlist, so the highlighted item is always the track that is playing. All the btnStop_Click handler does is reset everything. Pretty simple, but that is the idea, we want to use as little code in the logic editor as possible.

Conclusion

Well it has been fun so far. We now have a playlist which we can go through with previous and next buttons and we can now pause tracks whenever we like. Still pretty far from a fully fledged media player though. One thing that is bugging me though is the playlist.config, for 2 reasons

  • Currently we can only have one playlist file
  • typing a playlist.config file manually will take alot of time if you have a significant amount of music

Well the solution to the first issue is in your hands. There is nothing to stop a savvy coder from implementing another list like the playlist property, but holding a list of playlists, then organize those in a master playlists.config file or something. I think we might (after we have finished with the main scope of the tutorial) to create a windows forms app, which can build a playlist automatically from files and folders you select on your hard drive. Clicking buttons sounds much less laborious than typing file paths of 200 mp3 files.

In te next section we will go further with our player. I think in the next one we will get a few things out of the way

  • Volume control
  • Time display
  • Seek bar
  • Graphic equalizer

So by the end of the next page we will start to have something with a fair bit of the functionality you would expect from a media player.

Don't expect it until the end of next week at the latest though. I am currently putting off a lot of other work i have to do and it is about to come back and bite me, so i need to get on top of it

Cheers

angrywasp