To display music from the music library you use the XNA MediaLibrary and related classes. To actually play that music you use the static XNA MediaPlayer class.
The MediaPlayer class plays either a Song object, or all the songs in a SongCollection, or all the songs in a SongCollection beginning at a particular index. Those are the three variations of the static MediaPlayer.Play method.
You cannot create a SongCollection object yourself. You must always obtain an immutable SongCollection from one of the other classes (such as Album).
This means that it’s not a simple matter to let the user select a
particular subset of an album, or to rearrange the tracks in some way.
That would require the program to maintain its own list of Song
objects, and to play them sequentially. I chose not to implement
anything like that for this relatively simple demonstration program.
Besides Play, MediaPlayer also defines Pause, Resume, and Stop methods, as well as MovePrevious and MoveNext to move to the previous or next item in a SongCollection.
The crucial properties of MediaPlayer are all get-only:
State, which returns a member of the MediaState enumeration: Playing, Paused, or Stopped.
PlayPosition, a TimeSpan object indicating the position within the currently playing song.
Queue, a MediaQueue object that contains a collection of the Song objects in the currently-playing collection as well as an ActiveSong property.
From the ActiveSong property, you can obtain the Album object and other information associated with that song.
MediaPlayer also defines two events:
MediaStateChanged
ActiveSongChanged
The code-behind file for AlbumPage
is responsible for actually playing the album. But first take a look at
the parts of the class that perform what might be considered the
“housekeeping” chores:
Example 1. Silverlight Project: MusicByComposer File: AlbumPage.xaml.cs (excerpt)
public partial class AlbumPage : PhoneApplicationPage { // Used for switching play and pause icons static Uri playButtonIconUri = new Uri("/Images/appbar.transport.play.rest.png", UriKind.Relative); static Uri pauseButtonIconUri = new Uri("/Images/appbar.transport.pause.rest.png", UriKind.Relative);
int composerInfoIndex; int albumInfoIndex;
public AlbumPage() { InitializeComponent(); appbarPlayPauseButton = this.ApplicationBar.Buttons[1] as ApplicationBarIconButton; }
protected override void OnNavigatedFrom(NavigationEventArgs args) { PhoneApplicationService.Current.State["ComposerInfoIndex"] = composerInfoIndex; PhoneApplicationService.Current.State["AlbumInfoIndex"] = albumInfoIndex;
base.OnNavigatedFrom(args); }
protected override void OnNavigatedTo(NavigationEventArgs args) { // Navigating from MainPage if (this.NavigationContext.QueryString.ContainsKey("ComposerInfoIndex")) { composerInfoIndex = Int32.Parse(this.NavigationContext.QueryString["ComposerInfoIndex"]); albumInfoIndex = Int32.Parse(this.NavigationContext.QueryString["AlbumInfoIndex"]); }
// Reactivating from tombstoning else if (PhoneApplicationService.Current.State.ContainsKey("ComposerInfoInd ex")) { composerInfoIndex = (int)PhoneApplicationService.Current.State["ComposerInfoIndex"]; albumInfoIndex = (int)PhoneApplicationService.Current.State["AlbumInfoIndex"]; }
ComposerInfo composerInfo = MusicPresenter.Current. Composers[composerInfoIndex]; AlbumInfo albumInfo = composerInfo.Albums[albumInfoIndex];
// Set page title and DataContext PageTitle.Text = composerInfo.Composer; this.DataContext = albumInfo;
// Get the media state when it changes and also right now MediaPlayer.MediaStateChanged += OnMediaPlayerMediaStateChanged; OnMediaPlayerMediaStateChanged(null, EventArgs.Empty);
base.OnNavigatedTo(args); } . . . }
|
When being tombstoned, the OnNavigatedFrom method saves the two fields named composerInfoIndex and albumInfoIndex. These are the same two values that MainPage passes to AlbumPage in the navigation query string. The OnNavigatedTo method obtains those values either from the query string or the State property of the PhoneApplicationService to set the text of the PageTitle element (to display the name of the composer) and the DataContext of the page (so the bindings in AlbumPage.xaml work).
The OnNavigatedTo method also sets a handler for the MediaPlayer.MediaStateChanged event to maintain the correct icon image for the button that combines the functions of Play and Pause.
The event handler for that button turned out to be one of the trickier aspects of this class:
Example 2. Silverlight Project: MusicByComposer File: AlbumPage.xaml.cs (excerpt)
void OnAppbarPlayButtonClick(object sender, EventArgs args) { Album thisPagesAlbum = (this.DataContext as AlbumInfo).Album;
switch (MediaPlayer.State) { // The MediaPlayer is currently playing so pause it. case MediaState.Playing: MediaPlayer.Pause(); break;
// The MediaPlayer is currently paused. . . case MediaState.Paused: MediaQueue queue = MediaPlayer.Queue;
// so if we're on the same page as the paused song, resume it. if (queue.ActiveSong != null && queue.ActiveSong.Album == thisPagesAlbum) { MediaPlayer.Resume(); } // Otherwise, start playing this page's album. else { goto case MediaState.Stopped; } break;
// The MediaPlayer is stopped, so play this page's album. case MediaState.Stopped: MediaPlayer.Play(thisPagesAlbum.Songs); break; } }
void OnAppbarPreviousButtonClick(object sender, EventArgs args) { MediaPlayer.MovePrevious(); }
void OnAppbarNextButtonClick(object sender, EventArgs args) { MediaPlayer.MoveNext(); }
|
Once a program calls MediaPlayer.Play on a Song or SongCollection object, the music
keeps going even if the user exits that program or the phone shuts off
the screen and locks the display. This is how it should be. The user
wants to listen to the music regardless—even to the point where the
battery completely runs down.
For that reason, a program should be very cautious about calling MediaPlayer.Stop, because calling that method will stop the music without allowing it to be resumed. I found no reason to call MediaPlayer.Stop at all in my program.
The user can also exit a
program such as MusicByComposer and then return to it, and the user
should also be allowed to navigate to different album pages without
interfering with the playing music. Yet, the user should also have the
option of switching from the music currently playing to the album
currently in view. It seemed to me that these choices implied four
different cases when the user presses the play/pause button:
If music is currently
playing, then the play/pause button displays the pause icon, and the
currently playing music should be paused.
If the player is stopped, then the play/pause button displays the play icon, and the album in view should be played.
If
the music is paused, then the play/pause button also displays the play
icon. If the user is on the album page that’s currently active, then the
play button should just resume whatever was playing.
However, if the music is paused but the user is on a different album page, then the play button should start playing the album on the current page.
In actual use, that logic seems to work well.
The only class you haven’t seen yet is SongTitleControl, an instance of which is used to display each individual song on the album. SongTitleControl
is also responsible for highlighting the currently playing song and
displaying the elapsed time and total duration of that song.
SongTitleControl just derives from UserControl and has a simple visual tree:
Example 3. Silverlight Project: MusicByComposer File: SongTitleControl.xaml (excerpt)
<Grid x:Name="LayoutRoot"> <StackPanel Margin="0 3"> <TextBlock Name="txtblkTitle" Text="{Binding Name}" TextWrapping="Wrap" />
<TextBlock Name="txtblkTime" Margin="24 6" Visibility="Collapsed" /> </StackPanel> </Grid>
|
In AlbumPage.xaml, the SongTitleControl contains a binding on its Song property, which means that SongTitleControl must define a dependency property named Song of the XNA type Song. Here’s the definition of the Song property and the property-changed handlers:
Example 4. Silverlight Project: MusicByComposer File: SongTitleControl.xaml.cs (excerpt)
public static readonly DependencyProperty SongProperty = DependencyProperty.Register("Song", typeof(Song), typeof(SongTitleControl), new PropertyMetadata(OnSongChanged));
. . .
public Song Song { set { SetValue(SongProperty, value); } get { return (Song)GetValue(SongProperty); } }
static void OnSongChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { (obj as SongTitleControl).OnSongChanged(args); }
void OnSongChanged(DependencyPropertyChangedEventArgs args) { if (Song != null) MediaPlayer.ActiveSongChanged += OnMediaPlayerActiveSongChanged; else MediaPlayer.ActiveSongChanged -= OnMediaPlayerActiveSongChanged;
OnMediaPlayerActiveSongChanged(null, EventArgs.Empty); }
|
If Song is set to a non-null value, then an event handler is set for the MediaPlayer.ActiveSongChanged event. That event is handled here:
Example 5. Silverlight Project: MusicByComposer File: SongTitleControl.xaml.cs (excerpt)
void OnMediaPlayerActiveSongChanged(object sender, EventArgs args) { if (this.Song == MediaPlayer.Queue.ActiveSong) { txtblkTitle.FontWeight = FontWeights.Bold; txtblkTitle.Foreground = this.Resources["PhoneAccentBrush"] as Brush; txtblkTime.Visibility = Visibility.Visible; timer.Start(); } else { txtblkTitle.FontWeight = FontWeights.Normal; txtblkTitle.Foreground = this.Resources["PhoneForegroundBrush"] as Brush; txtblkTime.Visibility = Visibility.Collapsed; timer.Stop(); } }
|
The Text property of txtblkTitle is handled with a binding in the XAML file. If the active song is the Song associated with this instance of SongTitleControl, then this TextBlock is highlighted with the accent color, the other TextBlock with the time information is made visible, and a DispatcherTimer is started:
Example 6. Silverlight Project: MusicByComposer File: SongTitleControl.xaml.cs (excerpt)
public partial class SongTitleControl : UserControl { DispatcherTimer timer = new DispatcherTimer(); . . . public SongTitleControl() { InitializeComponent(); timer.Interval = TimeSpan.FromSeconds(0.25); timer.Tick += OnTimerTick; } . . . void OnTimerTick(object sender, EventArgs args) { TimeSpan dur = this.Song.Duration; TimeSpan pos = MediaPlayer.PlayPosition;
txtblkTime.Text = String.Format("{0}:{1:D2} / {2}:{3:D2}", (int)pos.TotalMinutes, pos.Seconds, (int)dur.TotalMinutes, dur.Seconds); } }
|
That Tick handler simply formats the duration of the song and the current position for display purposes.
I thought about shifting some
of this code to XAML, which would require defining a property for the
elapsed time, as well as using the Visual State Manager for ActiveSong
and NotActiveSong states, and then bringing in the StringFormatterConverter for formatting the two TimeSpan objects. But for this particular application the code file seemed the simpler of the two approaches.
Although you’ve seen many ways in which XAML is very powerful, sometimes code is really the right solution.