1. Problem
You need to retrieve songs from the media library of your Windows Phone 7 device and play them in shuffle mode.
2. Solution
You have to use the MediaLibrary class provided by the XNA Framework and use its property collections such as Songs and Albums. Moreover, you can play a song by using the Play static method defined in the MediaPlayer class.
3. How It Works
The XNA Framework provides the MediaLibrary class, in the Microsoft.XNA.Framework.dll assembly, which enables us to retrieve information from the media library of your phone. The MediaLibrary class provides a lot of useful media collections such as Songs, Pictures, and Albums. After you have a MediaLibrary object instance, you automatically have those collections filled with related information.
In this recipe, you are going to reproduce a song, so you are interested to the Songs collection. By using the Songs property from the MediaLibrary class, you can obtain a Song object pointing to a specified collection index.
The Song class contains everything concerning a song in the media library. Indeed, you can find the Album property to retrieve information on the album containing the song. The Artist property retrieves information on the song's author. The Duration property indicates the song's duration. The Genre property contains information on the song genre. The Name property is the song name. The TrackNumber property represents the song number within the album. Finally, the PlayCount and RatingFigure 1 shows the pertinent class diagrams. properties return the number of times the song has been played and the song's rating (if you have rated it), respectively.
4. The Code
To demonstrate this recipe, we have created the ShuffleMe application by using the Silverlight Windows Phone 7 template from Visual Studio 2010.
Because the Silverlight
application for Windows Phone 7 doesn't have support for accessing the
media library, the first thing we did was to reference the Microsoft.Xna.Framework.dll assembly. Indeed, the XNA Framework has everything necessary to query the media library for pictures and songs.
After shuffling the songs and retrieving a random one, you are going to play the selected song by using the Play method provided by the MediaPlayer class. But because we have a Silverlight application calling the Play method, you need to do an extra step: you need to call the Update static method from the FrameworkDispatcher
class. This call should be done periodically, so the Microsoft official
documentation suggests the creation of a class implementing the IApplicationService
interface. This interface has two method definitions, to start and to
stop the service. In the related methods, you are going to start and
stop a DispatcherTimer timer object. This timer has an interval set to 30 times per second in which it raises the Tick event. By defining the Tick event handler, you can call the Update static method periodically.
public class XNADispatcherService : IApplicationService
{
private DispatcherTimer frameworkDispatcherTimer;
public void StartService(ApplicationServiceContext context)
{
this.frameworkDispatcherTimer.Start();
}
public void StopService()
{
this.frameworkDispatcherTimer.Stop();
}
public XNADispatcherService()
{
this.frameworkDispatcherTimer = new DispatcherTimer();
this.frameworkDispatcherTimer.Interval = TimeSpan.FromTicks(333333);
this.frameworkDispatcherTimer.Tick += frameworkDispatcherTimer_Tick;
FrameworkDispatcher.Update();
}
void frameworkDispatcherTimer_Tick(object sender, EventArgs e) {
FrameworkDispatcher.Update(); }
}
In the App.xaml file, you can add the namespace that contains the XnaDispatcherService class and include a tag so that the application itself will start and stop the timer, automatically:
<Application
x:Class="ShuffleMe.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:local="clr-namespace:ShuffleMe">
<!--Application Resources-->
<Application.Resources>
</Application.Resources>
<Application.ApplicationLifetimeObjects>
<!--Required object that handles lifetime events for the application-->
<shell:PhoneApplicationService
Launching="Application_Launching" Closing="Application_Closing"
Activated="Application_Activated" Deactivated="Application_Deactivated"/>
<local:XNADispatcherService />
</Application.ApplicationLifetimeObjects>
</Application>
The ShuffleMe application has some requirements, which can be summarized as follows:
It must continue to play and shuffle songs even when the screen is locked.
When one song is over, a new one must be played, and it shouldn't be the same.
When the application is tombstoned, it must save the song's properties such as its title and album cover.
When
the application is reactivated from a tombstone, it must not stop the
current played song and must again display the song's properties.
When the hardware Back button is pressed, the application must stop playing the song.
Let's examine the code and the solutions found to address those points.
You can set the ApplicationIdleDetectionMode property to IdleDetectionMode.Disabled so that your application doesn't stop working when the screen is locked:
// The shuffle routine has to work even when the screen is locked
PhoneApplicationService.Current.ApplicationIdleDetectionMode = IdleDetectionMode.Disabled;
Sadly, the MediaLibrary doesn't provide either a property or a method to retrieve when the song is over. Our solution has been to create a DispatcherTimer object set to 1 second more than the song duration. Indeed, when the song is over (plus 1 second), the timer raises the Tick event that is hooked by the related event handler, and the private PlaySong method is called:
public partial class MainPage : PhoneApplicationPage
{
. . .
DispatcherTimer timer = null;
// Constructor
public MainPage()
{
. . .
timer = new DispatcherTimer();
timer.Tick += new EventHandler(timer_Tick);
// The shuffle routine has to work even when the screen is locked
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
void timer_Tick(object sender, EventArgs e)
{
PlaySong();
}
. . .
When the application is
tombstoned—for example, by the user pressing the hardware Start
button—the application must store important data such the album cover
and the song's author and title. Sadly, the Song class is not serializable, and the same is true for the BitmapImage class for the album cover. You need to create the AppSettings serializable class with specific information such as the Title property to store the application's title and the AlbumImage bytes array to store the image album. The latter is the way we found to serialize an image. In the IsTombstoned property, you set when the application is tombstoned. Finally, in the SongNumber property, you store the song's number generated by the shuffle routine.
public class AppSettings
{
public bool IsTombstoned { get; set; }
public string Title { get; set; }
public byte[] AlbumImage { get; set; }
public int SongNumber { get; set; }
}
NOTE
You could store only the SongNumber
value during the tombstoning and use it to retrieve the song when the
application is reactivated. However, in this way you can learn different
techniques to manage images' serialization.
An object from the AppSettings class is stored in the App application class, and you can retrieve it by using the related settings property:
public partial class App : Application
{
/// <summary>
/// Provides easy access to the root frame of the Phone Application.
/// </summary>
/// <returns>The root frame of the Phone Application.</returns>
public PhoneApplicationFrame RootFrame { get; private set; }
public AppSettings settings { get; set; }
. . .
Moreover, in the App class, you define the event handlers to manage the tombstoning:
// Code to execute when the application is launching (e.g., from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
settings = new AppSettings();
settings.IsTombstoned = false;
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
if (PhoneApplicationService.Current.State.ContainsKey("settings"))
{
settings = PhoneApplicationService.Current.State["settings"] as AppSettings;
settings.IsTombstoned = true;
}
}
// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
PhoneApplicationService.Current.State["settings"] = settings;
}
The other properties provided by the AppSettings class are set in the PlaySong method. The first operation in this method calls the DoShuffle method, which returns a Song object. This object is compared to a previous Song object stored as _lastSong variable. If the objects are equal, the PlaySong method is called again until a new Song
object is retrieved. If the user has only one song in her library, the
song is repeated each time until the application is closed.
NOTE
You could use the SongNumber property to check whether the DoShuffle method picked the same song, but we used a Song object to demonstrate that this class supports object comparison.
The PlaySong method contains a couple of interesting code snippets. The HasArt property is checked to see whether the song has an associated album cover. If it does, the GetAlbumArt method is used to retrieve the stream data of the image. One way to use the Stream object with the Image control—and specifically its Source property—is to create a WriteableBitmap object with the static DecodeJpeg method from the PictureDecoder class defined in the Microsoft.Phone namespace. If the song does not have an associated album cover, a No Cover image is retrieved from the image folder within the ShuffleMe project.
The AlbumImage bytes array defined in the settings is filled with the GetAlbumArt data thanks to the BinaryReader class and its ReadBytes method.
Finally, the Duration property from the Song class is used to set the Interval property of the timer, and then the timer is started.
private void PlaySong()
{
Song s = DoShuffle();
if ((s != null && s != _lastSong)||(library.Songs.Count == 1))
{
App app = Application.Current as App;
_lastSong = s;
tbAuthor.Text = s.Artist.Name + ": " + s.Name;
app.settings.Title = tbAuthor.Text;
if (s.Album.HasArt)
{
WriteableBitmap wbimg =
PictureDecoder.DecodeJpeg(s.Album.GetAlbumArt());
imgCover.Source = wbimg;
using (var br = new BinaryReader(s.Album.GetAlbumArt()))
app.settings.AlbumImage =
br.ReadBytes((int)s.Album.GetAlbumArt().Length);
}
else
{
app.settings.AlbumImage = null;
imgCover.Source = new BitmapImage(new Uri("/images/nocover.jpg",
UriKind.Relative));
}
MediaPlayer.Play(s);
timer.Interval = s.Duration + TimeSpan.FromSeconds(1);
timer.Start();
}
else
PlaySong();
}
When the application either starts or is tombstoned, the Loaded event for the MainPage page is raised and the private PlaySong
method is called. But before calling this method, you check whether the
application has been tombstoned or has been launched for the first
time. Only in the latter case do you call the PlaySong
method, because you want to avoid having a new song played when the
application is reactivated by tombstoning. Indeed, in the tombstoning
case, you simply rewrite the title and reset the album cover.
In the PhoneApplicationPage_Loaded
event handler, there is an interesting thing that is worth noting.
After tombstoning occurs, the timer is not working anymore, and so you
need to set its interval again. But this time you can't use the song
duration because some time has passed. So the PlayPosition property from the MediaPlayerTimeSpan object representing the song reproduction time. By subtracting this value from the song's duration, you obtain the new Interval class is used to retrieve a value of the timer.
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
App app = Application.Current as App;
if (!app.settings.IsTombstoned)
PlaySong();
else
{
tbAuthor.Text = app.settings.Title;
if (app.settings.AlbumImage != null)
{
MemoryStream ms = new MemoryStream(app.settings.AlbumImage);
WriteableBitmap wbimg = PictureDecoder.DecodeJpeg(ms);
imgCover.Source = wbimg;
}
else
imgCover.Source = new BitmapImage(new Uri("/images/nocover.jpg",
UriKind.Relative));
TimeSpan remainTime = library.Songs[app.settings.SongNumber].Duration –
MediaPlayer.PlayPosition;
timer.Interval = remainTime + TimeSpan.FromSeconds(1);
timer.Start();
}
}
In the DoShuffle method code, you use the Random class to generate a number ranging from zero to the value of the Count property of the Songs collection, which returns the number of songs in the media library. Finally, this number is saved in the SongNumber property of the settings object, and the song at songIndex is returned from the Songs collection.
private Song DoShuffle()
{
App app = Application.Current as App;
int count = library.Songs.Count;
Random rand = new Random();
int songIndex = rand.Next(0, count);
app.settings.SongNumber = songIndex;
return library.Songs[songIndex];
}
The last requirement we
have imposed on ourselves is that the application must end when the
hardware Back button is pressed. In the PhoneApplicationPage class, you define the OnBackKeyPress method, which you can override so as to add your code before the back functionality is accomplished.
In this case, you stop the song that is playing and you stop the timer.
protected override void OnBackKeyPress(System.ComponentModel.CancelEventArgs e)
{
MediaPlayer.Stop();
timer.Stop();
base.OnBackKeyPress(e);
}
NOTE
Actually, at writing time, the MediaLibrary has an initialization bug that can be resolved with a workaround. In the MainPage class constructor, you can add the MediaPlayer.Queue.ToString(); call so that you force the library initialization.
5. Usage
From Visual Studio 2010,
select the output target as Windows Phone 7 Emulator and press Ctrl+F5.
The emulator starts, briefly showing the application, as in Figure 2. The song you play could be different from the one shown in Figure 7-8,
because of the shuffle mode. The emulator provides three songs with no
covers, so if you want to see the application with album covers as well,
you should run it on a physical device.
Press the hardware Back
button to terminate the application and its song. Or press the hardware
Start button and then the hardware Back button to simulate the
application tombstoning. You should hear the song continuing playing
without any interruption and see the title, as before the tombstoning.
Now you can wait until the song ends in order to see that another (and different) song is played.