Working with Music Libraries - iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)

iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)

Chapter 6. Working with Music Libraries

When Steve Jobs first introduced the iPhone onstage at Macworld in 2007, it was touted as a phone, an iPod, and revolutionary Internet communicator. Several years later, and partially due to a lot of hard work by third-party developers, the iPhone has grown into something so much more than those three core concepts. That original marketing message has not changed, however; the iPhone itself remains primarily a phone, an iPod, and an Internet communication device. Users did not add an iPhone to their collection of devices they already carried every day; they replaced their existing phones and iPods with a single device.

Music is what gave the iPhone its humble start when Apple begun planning the device in 2004; the iPhone was always an extension of an iPod. Music inspired the iPod, which, it could be argued, brought the company back from the brink. Music is universally loved by people. It brings them together and it allows them to express themselves. Although day-to-day iPhone users might not even think of their iPhone as a music playback device, most of them will use it almost absent-mindedly to listen to their favorite songs.

This chapter discusses how to add access to the user’s music library inside of an iOS app. Whether building a full-featured music player or allowing users to play their music as the soundtrack to a game, this chapter demonstrates how to provide music playback from the user’s own library.

Introduction to the Sample App

The sample app for this chapter is simply called Player (see Figure 6.1). The sample app is a full-featured music player for the iPhone. It will allow the user to pick songs to be played via the Media Picker, play random songs, or play artist-specific songs. In addition, it features functionality for pause, resume, previous, next, volume, playback counter, and plus or minus 30 seconds to the playhead. In addition, the app will display the album art for the current track being played if it is available.

Image

Figure 6.1 A first look at the sample app, Player, a fully functional music player running on an iPod.

Since the iOS Simulator that comes bundled with Xcode does not include a copy of the Music.app, nor does it have an easy method of transferring music into its file system, the app can be run only on actual devices. When the app is launched on a simulator, a number of errors will appear.

2013-03-02 16:04:13.392 player[80633:c07] MPMusicPlayer: Unable to launch iPod music player server: application not found
2013-03-02 16:04:13.893 player[80633:c07] MPMusicPlayer: Unable to launch iPod music player server: application not found
2013-03-02 16:04:14.395 player[80633:c07] MPMusicPlayer: Unable to launch iPod music player server: application not found

Any attempt to access the media library will result in a crash on the simulator with the following error:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Unable to load iPodUI.framework'

Building a Playback Engine

Before it makes sense to pull in any audio data, an in-depth understanding of the playback controls is required. To play music from within an app, a new instance of MPMusicPlayerController needs to be created. This is done in the header file ICFViewController.h, and the new object is called player. The MPMusicPlayerController will be referenced throughout this chapter to control the playback as well as retrieve information about the items being played.

@interface ICFViewController : UIViewController
{
MPMusicPlayerController *player;
}

@property (nonatomic, retain) MPMusicPlayerController *player;

Inside the viewDidLoad method, the MPMusicPlayerController player can be initialized using a MPMusicPlayerController class method. There are two possible options when creating a new MPMusicPlayerController. In the first option, anapplicationMusicPlayer will play music within an app; it will not affect the iPod state and will end playback when the app is exited. The second option, iPodMusicPlayer, will control the iPod app itself. It will pick up where the user has left the iPod playhead and track selection, and will continue to play after the app has entered the background. The sample app uses applicationMusicPlayer; however, this can easily be changed without the need to change any other code or behavior.

- (void)viewDidLoad
{
[super viewDidLoad];

player = [MPMusicPlayerController applicationMusicPlayer];
}

Registering for Playback Notifications

To efficiently work with music playback, it is important to be aware of the state of the music player. When you are dealing with the music player, there are three notifications to watch. The “now playing” item has changed, the volume has changed, and the playback state has changed. These states can be monitored by using NSNotificationCenter to subscribe to the afore-mentioned events. The sample app uses a new convenience method, registerMediaPlayerNotifications, to keep the sample app’s code clean and readable. After the new observers have been added to NSNotificationCenter, the beginGeneratingPlaybackNotifications needs to be invoked on the player object.

- (void)registerMediaPlayerNotifications
{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

[notificationCenter addObserver: self
selector: @selector
(nowPlayingItemChanged:)
name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object: player];

[notificationCenter addObserver: self
selector: @selector
(playbackStateChanged:)
name: MPMusicPlayerControllerPlaybackStateDidChangeNotification
object: player];

[notificationCenter addObserver: self
selector: @selector (volumeChanged:)
name: MPMusicPlayerControllerVolumeDidChangeNotification
object: player];

[player beginGeneratingPlaybackNotifications];
}

When registering for notifications, it is important to make sure that they are properly deregistered during memory and view cleanup; failing to do so can cause crashes and other unexpected behavior. A call to endGeneratingPlaybackNotifications is also performed during theviewDidUnload routine.

-(void)viewWillDisappear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] removeObserver: self
name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object: player];

[[NSNotificationCenter defaultCenter] removeObserver: self
name: MPMusicPlayerControllerPlaybackStateDidChangeNotification
object: player];

[[NSNotificationCenter defaultCenter] removeObserver: self
name: MPMusicPlayerControllerVolumeDidChangeNotification
object: player];

[player endGeneratingPlaybackNotifications];



[super viewWillDisappear: animated];
}

In addition to registering for callbacks from the music player, a new NSTimer will be created to handle updating the playback progress and playhead time label. In the sample app, the NSTimer is simply called playbackTimer. For the time being, the notification callback selectors and the NSTimer behavior will be left uncompleted. These are discussed later, in the section “Handling State Changes.”

User Controls

The sample app provides the user with several buttons designed to allow them to interact with the music, such as play, pause, skip, and previous, as well as jumping forward and backward by 30 seconds. The first action that needs to be implemented is the play and pause method. The button functions as a simple toggle: If the music is already playing, it is paused; if it is paused or stopped, it resumes playing. The code to update the text of the button from play to pause and vice versa is discussed as part of the state change notification callback in the section “Handling State Changes.”

- (IBAction)playButtonAction:(id)sender
{
if ([player playbackState] == MPMusicPlaybackStatePlaying)
{
[player pause];
}

else
{
[player play];
}
}

The user should also have the ability to skip to the previous or next track while listening to music. This is done through two additional calls on the player object.

- (IBAction)previousButtonAction:(id)sender
{
[player skipToPreviousItem];
}

- (IBAction)nextButtonAction:(id)sender
{
[player skipToNextItem];
}

Users can also be provided with actions that allow them to skip 30 seconds forward or backward in a song. If the user hits the end of the track, the following code will skip to the next track; likewise, if they hit further than the start of a song, the audio track will start over. Both of these methods make use of the currentPlaybackTime property of the player object. This property can be used to change the current playhead, as well as determine what the current playback time is.

- (IBAction)skipBack30Seconds:(id)sender
{
int newPlayHead = player.currentPlaybackTime - 30;

if(newPlayHead < 0)
{
newPlayHead = 0;
}

player.currentPlaybackTime = newPlayHead;
}

- (IBAction)skipForward30Seconds:(id)sender
{
int newPlayHead = player.currentPlaybackTime + 30;

if(newPlayHead > currentSongDuration)
{
[player skipToNextItem];
}

else
{
player.currentPlaybackTime = newPlayHead;
}
}

In addition to these standard controls to give the user control over the item playing, the sample app enables the user to change the volume of the audio. The player object takes a float value of 0.0 for muted to 1.0 for full volume. Since the sample uses a UISlider, the value can directly be passed into the player.

- (IBAction)volumeSliderChanged:(id)sender
{
[player setVolume:[volumeSlider value]];
}

Handling State Changes

Earlier in this section, three notifications were registered to receive callbacks. These notifications allow the app to determine the current state and behavior of the MPMusicPlayerController. The first method that is being watched will be called whenever the currently playing item changes. This method contains two parts. The first part updates the album artwork, and the second updates the labels that indicate the artist, song title, and album being played.

Every audio or video item being played through the MPMusicPlayerController is represented by an MPMediaItem object. This object can be retrieved by invoking the method nowPlayingItem on an instance of MPMusicPlayerController.

A new UIImage is created to represent the album artwork and is initially set to a placeholder that will be used in the event that the user does not have album artwork for the MPMediaItem. MPMediaItem uses key value properties for stored data; a full list is shown in Table 6.1. A newMPMediaItemArtwork is created and set with the artwork data. Although the documentation specifies that if no artwork is available this will return nil, in practice this is not the case. A workaround is to load the artwork into a UIImage and check the resulting value. If it is nil, the assumption is that there is no album artwork and the placeholder is loaded. The code used in the sample app will continue to function in the event that MPMediaItemArtwork begins returning nil when no album artwork is available.

Image

Image

Table 6.1 Available Keys When Working with MPMediaItem

The second part of the nowPlayingItemChanged: method handles updating the song title, artist info, and album name, as shown earlier in Figure 6.1. In the event that any of these properties returns nil, a placeholder string is set. A complete list of accessible properties on aMPMediaItem can be found in Table 6.1. When referencing the table, note that if the media item is a podcast, additional keys will be available, which are available in Apple’s documentation for MPMediaItem. Also indicated is whether the key can be used for predicate searching when programmatically finding MPMediaItems.

- (void) nowPlayingItemChanged: (id) notification
{
MPMediaItem *currentItem = [player nowPlayingItem];

UIImage *artworkImage = [UIImage imageNamed:@"noArt.png"];

MPMediaItemArtwork *artwork = [currentItem valueForProperty: MPMediaItemPropertyArtwork];

if (artwork)
{
artworkImage = [artwork imageWithSize: CGSizeMake (120,120)];

if(artworkImage == nil)
{
artworkImage = [UIImage imageNamed:@"noArt.png"];
}
}

[albumImageView setImage:artworkImage];

NSString *titleString = [currentItem valueForProperty:MPMediaItemPropertyTitle];

if (titleString)
{
songLabel.text = titleString;
}

else
{
songLabel.text = @"Unknown Song";
}

NSString *artistString = [currentItem valueForProperty:MPMediaItemPropertyArtist];

if (artistString)
{
artistLabel.text = artistString;

}

else
{
artistLabel.text = @"Unknown artist";
}

NSString *albumString = [currentItem valueForProperty:MPMediaItemPropertyAlbumTitle];

if (albumString)
{
recordLabel.text = albumString;
}

else
{
recordLabel.text = @"Unknown Record";
}
}

Monitoring the state of the music player is a crucial step, especially since this value can be affected by input outside the control of the app. In the event that the state is updated, the playbackStateChanged: method is fired. A new variable playbackState is created to hold onto the current state of the player. This method performs several important tasks, the first of which is updating the text on the play/pause button to reflect the current state. In addition, the NSTimer that was mentioned in the “Registering for Playback Notifications” section is both created and torn down. While the app is playing audio, the timer is set to fire every 0.3 seconds; this is used to update the playback duration labels as well as the UIProgressIndicator that informs the user of the placement of the playhead. The method that the timer fires,updateCurrentPlaybackTimer, is discussed in the next subsection.

In addition to the states that are shown in the sample app, there are three additional states. The first, MPMusicPlaybackStateInterrupted, is used whenever the audio is being interrupted, such as by an incoming phone call. The other two states,MPMusicPlaybackStateSeekingForward and MPMusicPlaybackStateSeekingBackward, are used to indicate that the music player is seeking either forward or backward.

- (void) playbackStateChanged: (id) notification
{
MPMusicPlaybackState playbackState = [player playbackState];

if (playbackState == MPMusicPlaybackStatePaused)
{
[playButton setTitle:@"Play" forState:UIControlStateNormal];

if([playbackTimer isValid])
{
[playbackTimer invalidate];
}
}

else if (playbackState == MPMusicPlaybackStatePlaying)
{
[playButton setTitle:@"Pause" forState: UIControlStateNormal];

playbackTimer = [NSTimer
scheduledTimerWithTimeInterval:0.3
target:self
selector:@selector(updateCurrentPlaybackTime)
userInfo:nil
repeats:YES];
}

else if (playbackState == MPMusicPlaybackStateStopped)
{
[playButton setTitle:@"Play" forState:UIControlStateNormal];

[player stop];

if([playbackTimer isValid])
{
[playbackTimer invalidate];
}
}
}

In the event that the volume has changed, it is also important to reflect that change on the volume slider found in the app. This is done by watching for the volumeChanged: notification callback. From inside this method the current volume of the player can be polled and thevolumeSlider can be set accordingly.

- (void) volumeChanged: (id) notification
{
[volumeSlider setValue:[player volume]];
}

Duration and Timers

Under most circumstances, users will want to have information available to them about the current status of their song, such as how much time has been played and how much time is left in the track. The sample app features two methods for generating this data. The firstupdateSongDuration is called whenever the song changes or when the app is launched. A reference to the current track being played is created, and the song duration expressed in seconds is retrieved through the key playbackDuration. The total hours, minutes, and seconds are derived from this data, and the song duration is displayed in a label next to the UIProgressIndicator.

-(void)updateSongDuration;
{
currentSongPlaybackTime = 0;

currentSongDuration = [[[player nowPlayingItem] valueForProperty: @"playbackDuration"] floatValue];

int tHours = (currentSongDuration / 3600);
int tMins = ((currentSongDuration / 60) - tHours*60);
int tSecs = (currentSongDuration) - (tMins*60) - (tHours *3600);

songDurationLabel.text = [NSString stringWithFormat:@"%i: %02d:%02d", tHours, tMins, tSecs ];

currentTimeLabel.text = @"0:00:00";
}

The second method, updateCurrentPlaybackTime, is called every 0.3 seconds via an NSTimer that is controlled from the playbackStateChanged: method discussed in the “Handling State Changes” section. The same math is used to derive the hours, minutes, and seconds as in the updateSongDuration method. A percentagePlayed is also calculated based on the previously determined song duration and is used to update the playbackProgressIndicator. Since the currentPlaybackTime is accurate only to one second, this method does not need to be called more often. However, the more regularly it is called, the better precision to the actual second it will be.

-(void)updateCurrentPlaybackTime;
{
currentSongPlaybackTime = player.currentPlaybackTime;

int tHours = (currentSongPlaybackTime / 3600);
int tMins = ((currentSongPlaybackTime / 60) - tHours*60);
int tSecs = (currentSongPlaybackTime) - (tMins*60) - (tHours*3600);

currentTimeLabel.text = [NSString stringWithFormat:@"%i: %02d:%02d", tHours, tMins, tSecs ];

float percentagePlayed = currentSongPlaybackTime/
currentSongDuration;

[playbackProgressIndicator setProgress:percentagePlayed];
}

Shuffle and Repeat

In addition to the properties and controls mentioned previously, an MPMusicPlayerController also allows the user to specify the repeat and shuffle properties. Although the sample app does not implement functionality for these two properties, they are fairly easy to implement.

player.repeatMode = MPMusicRepeatModeAll;
player.shuffleMode = MPMusicShuffleModeSongs;

The available repeat modes are MPMusicRepeatModeDefault, which is the user’s predefined preference, MPMusicRepeatModeNone, MPMusicRepeatModeOne, and MPMusicRepeatModeAll. The available modes for shuffle are MPMusicShuffleModeDefault,MPMusicShuffleModeOff, MPMusicShuffleModeSongs, and MPMusicShuffleModeAlbums, where the MPMusicShuffleModeDefault mode represents the user’s predefined preference.

Media Picker

The simplest way to allow a user to specify which song he wants to hear is to provide him access to an MPMediaPickerController, as shown in Figure 6.2. The MPMediaPickerController allows the user to browse his artists, songs, playlists, and albums to specify one or more songs that should be considered for playback. To use an MPMediaPickerController, the class first needs to specify that it handles the delegate MPMediaPickerControllerDelegate, which has two required methods. The first mediaPicker:didPickMediaItems: is called when the user has completed selecting the songs she would like to hear. Those songs are returned as an MPMediaItemCollection object, and the MPMusicPlayerController can directly take this object as a parameter of setQueueWithItemCollection:. After a new queue has been set for the MPMusicPlayerController, it can begin to play the new items. The MPMediaPickerController does not dismiss itself after completing a selection and requires explicit use of dismissViewControllerAnimated:completion:.

- (void) mediaPicker: (MPMediaPickerController *) mediaPicker didPickMediaItems: (MPMediaItemCollection *) mediaItemCollection
{
if (mediaItemCollection)
{
[player setQueueWithItemCollection: mediaItemCollection];
[player play];
}

[self dismissViewControllerAnimated:YES completion:nil];
}

Image

Figure 6.2 Selecting songs using the MPMediaPickerController. Two songs are selected by the user, indicated by the titles and info changing to light gray.

In the event that the user cancels or dismisses the MPMediaPickerController without making a selection, the delegate method mediaPickerDidCancel: is called. The developer is required to dismiss the MPMediaPickerController as part of this method.

- (void) mediaPickerDidCancel: (MPMediaPickerController *) mediaPicker
{
[self dismissViewControllerAnimated:YES completion:nil];
}

After the delegate methods have been implemented, an instance of MPMediaPickerController can be created. During allocation and initialization of a MPMediaPickerController, a parameter for supported media types is required. A full list of the available options is shown inTable 6.2. Note that each media item can be associated with multiple media types. Additional optional parameters for the MPMediaPickerController include specifying the selection of multiple items, and a prompt to be shown during selection, shown in Figure 6.2. An additional Boolean property also exists for setting whether iCloud items are shown; this is defaulted to YES.

- (IBAction)mediaPickerButtonAction:(id)sender
{
MPMediaPickerController *mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeAny];

mediaPicker.delegate = self;
mediaPicker.allowsPickingMultipleItems = YES;
mediaPicker.prompt = @"Select songs to play";

[self presentViewController:mediaPicker animated:YES completion: nil];

[mediaPicker release];
}

Image

Table 6.2 Available Constant Accepted During the Specification of Media Types When Creating a New MPMediaPickerController

These are all the steps required to allow a user to pick songs for playback using the MPMediaPickerController; however in many circumstances it might be necessary to provide a custom user interface or select songs with no interface at all. The next section, “Programmatic Picker,” covers these topics.

Programmatic Picker

Often, it might be required to provide a more customized music selection option to a user. This might include creating a custom music selection interface or automatically searching for an artist or album. In this section the steps necessary to provide programmatic music selection are discussed.

To retrieve songs without using the MPMediaPickerController, a new instance of MPMediaQuery needs to be allocated and initialized. The MPMediaQuery functions as a store that references a number of MPMediaItems, each of which represents a single song or audio track to be played.

The sample app provides two methods that implement an MPMediaQuery. The first method, playRandomSongAction:, will find a single random track from the user’s music library and play it using the existing MPMusicPlayerController. Finding music programmatically begins by allocating and initializing a new instance of MPMediaQuery.

Playing a Random Song

Without providing any predicate parameters, the MPMediaQuery will contain all the items found within the music library. A new NSArray is created to hold onto these items, which are retrieved using the item’s method on the MPMediaQuery. Each item is represented by anMPMediaItem. The random song functionality of the sample app will play a single song at a time. If no songs were found in the query a UIAlert is presented to the user; however if multiple songs are found a single one is randomly selected.

After a single (or multiple) MPMediaItem has been found, a new MPMediaItemCollection is created by passing an array of MPMediaItems into it. This collection will serve as a playlist for the MPMusicPlayerController. After the collection has been created, it is passed to the player object using setQueueWithItemCollection. At this point the player now knows which songs the user intends to listen to, and a call of play on the player object will begin playing the MPMediaItemCollection in the order of the array that was used to create theMPMediaItemCollection.

- (IBAction)playRandomSongAction:(id)sender
{
MPMediaItem *itemToPlay = nil;
MPMediaQuery *allSongQuery = [MPMediaQuery songsQuery];
NSArray *allTracks = [allSongQuery items];

if([allTracks count] == 0)
{
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"Error"
message:@"No music found!"
delegate:nil
cancelButtonTitle:@"Dismiss"
otherButtonTitles:nil];

[alert show];
[alert release];

return;
}

if ([allTracks count] < 2)
{
itemToPlay = [allTracks lastObject];
}

int trackNumber = arc4random() % [allTracks count];
itemToPlay = [allTracks objectAtIndex:trackNumber];

MPMediaItemCollection * collection = [[MPMediaItemCollection
alloc] initWithItems:[NSArray arrayWithObject:itemToPlay]];

[player setQueueWithItemCollection:collection];
[collection release];

[player play];

[self updateSongDuration];
[self updateCurrentPlaybackTime];
}


Note

arc4random() is a member of the standard C library and can be used to generate a random number in Objective-C projects. Unlike most random-number-generation functions, arc4random is automatically seeded the first time it is called.


Predicate Song Matching

Often, an app won’t just want to play random tracks and will want to perform a more advanced search. This is done using predicates. The following example uses a predicate to find music library items that have an artist property equal to "Bob Dylan", as shown in Figure 6.3. This method functions very similarly to the previous random song example, except that addFilterPredicate is used to add the filter to the MPMediaQuery. In addition, the results are not filtered down to a single item, and the player is passed an array of all the matching songs. For a complete list of available predicate constants, see the second column of Table 6.1 in the “Handling State Changes” section. Multiple predicates can be used with supplemental calls to addFilterPredicate on the MPMediaQuery.

- (IBAction)playDylan:(id)sender
{
MPMediaPropertyPredicate *artistNamePredicate =
[MPMediaPropertyPredicate predicateWithValue: @"Bob Dylan"
forProperty:
MPMediaItemPropertyArtist];

MPMediaQuery *artistQuery = [[MPMediaQuery alloc] init];

[artistQuery addFilterPredicate: artistNamePredicate];

NSArray *tracks = [artistQuery items];

if([tracks count] == 0)
{
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"Error"
message:@"No music found!"
delegate:nil
cancelButtonTitle:@"Dismiss"
otherButtonTitles:nil];

[alert show];
[alert release];

return;
}

MPMediaItemCollection * collection = [[MPMediaItemCollection alloc] initWithItems:tracks];

[player setQueueWithItemCollection:collection];
[collection release];

[player play];

[self updateSongDuration];
[self updateCurrentPlaybackTime];
}

Image

Figure 6.3 Using a predicate search to play tracks by the artist Bob Dylan.

Summary

This chapter covered accessing and working with a user’s music library. The first topic covered was building a playback engine to allow a user to interact with the song playback, such as pausing, resuming, controlling the volume, and skipping. The next two sections covered accessing and selecting songs from the library. The media picker demonstrated using the built-in GUI to allow the user to select a song or group of songs, and the “Programmatic Picker” section dealt with finding and searching for songs using predicates.

The sample app demonstrated how to build a fully functional albeit simplified music player for iOS. The knowledge demonstrated in this chapter can be applied to creating a fully featured music player or adding a user’s music library as background audio into any app.

Exercises

1. Add a scrubber to the playback indicator in the sample app to allow the user to drag the playhead back and forth, allowing for real-time seeking.

2. Add functionality to the sample app to allow the user to enter an artist or a song name and return a list of all matching results, which can then be played.