Introduction
The iPod application on the iPhone/iPod is a decent player for listening to music or viewing video; however, when it comes to listening to audiobooks, it falls short:
- The play/pause button is small and easy to miss. It's also co-located with the Prev/Next buttons which might be pressed unintentionlly (when listening to a book, skipping or rewinding accidentally tends to be quite frustrating).
- There is no way to organize the books into groups (for series, type of book, etc.).
- The timeline navigator is too small and too cumbersome to handle effectively (audiobooks tend to be large files - several hours long), and if one wishes to move a short while - seconds, it's almost impossible.
- In a multiple-files book, iPod tends to play them in an arbitrary order (can't understand the logic).
- There is no easy way to view the time listened so far and left to play in multiple-file books.
- Volume level and the shuffle attribute are player properties (as opposed to book/playlist property).
- Bookmarks are maintained only for 'audiobooks' (and not for music collections), and they are prone to unwanted changes due to ... (I don't get the logic here too, maybe the sync process spoils them).
As I tend to frequently listen to audiobooks, and as I have some programming background, I developed this 'abPlayer'.
The Application
abPlayer organizes playlists (books) in groups called 'shelves'. Each shelf can hold any number of books as well as any number of sub-shelves (no limit to the depth of shelves within shelves).
The main 'playing' form is shown below:
When playing, the playing location (bookmark) is saved in 1 second intervals, and is persistent across program activation. Persistency is achieved using a SQLite database that resides in the application's 'Documents' directory. The SQLite code is covered in the 'Code' section below.
The whole screen area is used as a play/pause alternating switch - tap the screen once to play, tap again to pause.
The information displayed includes:
- Book name
- Media title
- Entry (media number) / number of media files in book
- Location / duration within the media file being played
- Location / duration within the whole book
- Player state
In addition, the screen area is sensitive to gestures (finger swipes):
- horizontal - right to left: rewind 15 seconds
- horizontal - left to right: skip forward 15 seconds
- vertical - any direction: replace display with album artwork, or back to text - alternating
The screen below shows the same 'playing' form with artwork display:
When the 'Properties' button is pressed, the following screen is displayed:
Using the 'slot machine'-like control, the file number as well as the location in the file can be changed with a 1 second precision. The 'shuffle' state and the playing volume are saved as book attributes, and are persistent across multiple activations of the program.
The following screen shows one of the 'Library' forms. The library form is where shelves and books and their media items are defined. In the picture below, we can see a set of shelves located in the 'Series' shelf:
The 'New' button is used to create a new book/shelf. 'Add media' is used to add media items to a book. 'Del' and 'Edit' are used to delete, move, and rename an item. When adding media to a shelf, a book is automatically created. The name of the book is taken from the 'album' tag of the first media item. Selecting an entry will make it the active book (currently being played). Hitting the accessory button (the horizontal arrow like button) will navigate to a lower level - shelf: to its sub-shelves and books, book: to its media items. If a book is selected, playing starts at the playing location last played. If a media item is selected, playing starts with the selected media.
When 'New' is pressed, the 'browser' form is displayed. The browser form(s) navigate through existing media items (by category), and allows selection of media items to add to a book. Following is the first 'browser' form:
Each media item displayed in the browsed screen can be selected. 'All' selects/de-selects all (alternating). The 'browser' form with the media items is shown here:
When 'Graphics' is pressed in a 'library' form, the books artwork is displayed. The display includes all books in the active shelf (recursively). Here is the 'Graphics' form:
To change to the Prev/Next artwork, horizontal gestures are used: right-to-left and left-to-right, respectively. That's basically it. There's more that can be described and shown, but that would spoil all the fun.
Compile/build it, install it, play with it - hope you'll enjoy it - I do.
Using the Code
There are several classes/forms in the project. I will highlight some pieces of code relating to the two classes:
cPersistant
- is a SQLite function wrapper cPlayer
- is a media player encapsulating class
cPersistant
can be used as the application template for a SQLite database access. cPlayer
can be used as-is in any application that needs to play a media item. Both classes can be used as code samples for their respective subject.
cPersistant
SQLite routines are C based (not Obj-C), which are a nuisance when your code, as a whole, is Obj-C. More disturbing is the fact that each call involves several steps:
- Prepare the SQL statement
- Format it
- Execute it
- Release resources
I decided to write an Obj-C wrapper around the SQLite calls and make them more straightforward, readable, and short. The following code describes what I did:
Define these globals:
char* errorMsg;
sqlite3* DB;
These are the error description texts when an operation fails or the SQLite database object.
Whenever an operation fails, SQLite returns (optionally) a description text in the global errorMsg
above. The following routine formats this message and displays it in an alert box:
-(void)showErr {
if(errorMsg == nil) return; UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"DB Error"
message:[NSString stringWithCString:errorMsg
encoding:NSUTF8StringEncoding]
delegate:nil cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
formatSQL
is a variadic function (function that takes a variable number of arguments). It takes a SQL statement as its first parameter and any number of additional parameters. Embedded in the SQL statement are '?' characters (as many as there are parameters). The function replaces each '?' with the corresponding parameter while formatting it to a string. In addition, string parameters are scanned for a single quote character that are replaced with two consecutive single quote characters so as not to confuse the SQLite syntax parser which takes string
values in single quote enveloping.
-(NSString*)formatSQL:(NSString*)sql argumentList:(va_list)argumentList
{
NSArray* splitCmd = [sql componentsSeparatedByString:@"?"];
NSString* result = @"";
int n = 0;
NSObject* temp;
NSString* str;
while(temp = va_arg(argumentList, id))
{
if([temp isKindOfClass:[NSString class]])
str = (NSString*)temp;
else
str = [(NSNumber*)temp stringValue];
result = [result stringByAppendingString:[splitCmd objectAtIndex:n]];
n++;
result = [result stringByAppendingString:[str stringByReplacingOccurrencesOfString:
@"'" withString:@"''"]];
}
return [result stringByAppendingString:[splitCmd objectAtIndex:n]];
}
The executeSQL
function is again a variadic function (which uses the above formatSQL
function) and executes a non result-set SQL command returning a success/failure Boolean result code.
-(BOOL)executeSQL:(NSString*) sql, ...
{
va_list argumentList;
va_start(argumentList, sql);
NSString* str = [self formatSQL:sql argumentList:argumentList];
va_end(argumentList);
if(sqlite3_exec(DB, [str UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK)
{
[self showErr];
return NO;
}
return YES;
}
The createDataSet
executes a SQL statement with a result-set (usually a 'select
') and formats the result-set into an array of dictionary items. Each dictionary item corresponds to a row of the result-set, and each key-value set corresponds to a column where the key is the column name:
-(NSArray*)createDataSet:(NSString*) sql, ...;
{
va_list argumentList;
va_start(argumentList, sql);
NSString* str = [self formatSQL:sql argumentList:argumentList];
va_end(argumentList);
sqlite3_stmt* compiledSQL;
NSMutableArray* array = [NSMutableArray arrayWithCapacity:10];
if(sqlite3_prepare_v2(DB, [str UTF8String], -1, &compiledSQL, NULL) == SQLITE_OK)
{
while(sqlite3_step(compiledSQL) == SQLITE_ROW)
{
NSMutableDictionary* dic = [NSMutableDictionary dictionaryWithCapacity:10];
int count = sqlite3_column_count(compiledSQL);
for(int n=0; n<count; n++)
{
NSString* name = [NSString stringWithUTF8String:(char*)sqlite3_column_name
compiledSQL, n)];
id value = @"";
int columnType = sqlite3_column_type(compiledSQL, n);
switch (columnType)
{
case SQLITE_INTEGER:
value = [NSNumber numberWithInt:sqlite3_column_int(compiledSQL, n)];
break;
case SQLITE_FLOAT:
value = [NSNumber numberWithFloat:sqlite3_column_double
(compiledSQL, n)];
break;
case SQLITE_TEXT:
value = [NSString stringWithUTF8String:
(char*)sqlite3_column_text(compiledSQL, n)];
break;
}
[dic setValue:value forKey:name];
}
[array addObject:dic];
}
}
sqlite3_finalize(compiledSQL);
return array;
}
From here on, accessing the SQLite database is free of SQLite syntax and complexities, and is straightforward. Moreover, it allows the bulk of the code to use terms and lingo that's consistent with the application subject matter (in my case: shelves, books, etc.). The following code is an example of the use of the wrapper functions:
-(NSMutableArray*)books:(NSString*)inShelf
{
NSMutableArray* array = [NSMutableArray arrayWithCapacity:20];
NSString* const sql = @"SELECT BOOK FROM BOOKS WHERE SHELF=? ORDER BY BOOK";
NSArray* ds = [self createDataSet:sql, inShelf, nil];
for(NSDictionary* dsDic in ds)
[array addObject:[dsDic objectForKey:@"BOOK"]];
return array;
}
The above can be enhanced to support more data types etc., but it can serve as a decent start.
cPlayer
MPMediaPlayerController
is the class that manages audio files playback. In order to play an audio media (or a playlist), three stages are involved:
- Instantiate a
MPMediaPlayerController
object. - Perform a media query to get an array of
MPMediaItem
(s). - Set the
MPMediaPlayerController
queue with the array obtained in step 2.
Once these steps are followed, media commands can be issued (play, pause, next ...). There is one more optional step: subscribe to notification messages so we can be alerted when certain events take place - play state has changed, play item has changed, and external volume control has changed.
As stated above, the order in which a queue's media items is played is unpredictable (to me, it seams so) - especially when the media items are MP3 files, so in the abPlayer application, I play one media item at a time and manage the transition from one item to the next, as well as skip commands (prev, next) manually.
For each book, the application maintains an array of dictionary items, each item holding attributes that identify the media item. These attributes are: the media title, its artist, and its album - it is presumed that these three attributes identify a unique media item.
The code below demonstrate the steps discussed above:
Step 1 (and the optional step)
[notificationCenter addObserver:self
selector:@selector(handleExternalVolumeChanged:)
name:MPMusicPlayerControllerVolumeDidChangeNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(handlePlayItemChanged:)
name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object:nil];
audioPlayer = [[MPMusicPlayerController iPodMusicPlayer] retain];
[audioPlayer setRepeatMode:MPMusicRepeatModeNone];
[audioPlayer beginGeneratingPlaybackNotifications];
- Get the
MPMediaItem
- Create three predicates with the three identifying attributes
- Create an NSSet with these predicates
- Perform a
MPMediaQuery
Step 2
-(NSArray*)mediaItemsArray:(int)index
{
NSDictionary* dic = [nowPlayingItems objectAtIndex:index];
MPMediaPropertyPredicate* albumP = [MPMediaPropertyPredicate
predicateWithValue:[dic objectForKey:@"ALBUM"]
forProperty: MPMediaItemPropertyAlbumTitle];
MPMediaPropertyPredicate* titleP = [MPMediaPropertyPredicate
predicateWithValue:[dic objectForKey:@"TITLE"]
forProperty: MPMediaItemPropertyTitle];
MPMediaPropertyPredicate* artistP = [MPMediaPropertyPredicate
predicateWithValue:[dic objectForKey:@"ARTIST"]
forProperty: MPMediaItemPropertyArtist];
NSSet* set = [NSSet setWithObjects:albumP, artistP, titleP, nil];
MPMediaQuery* query = [[[MPMediaQuery alloc]
initWithFilterPredicates:set] autorelease];
return [query items];
}
Load the playlist (array of MPMediaitem
s - in our case, one item should be found in the query).
Step 3
-(void)prepareToPlay:(float)location play:(BOOL)play
{
[audioPlayer pause];
NSDictionary* dic = [nowPlayingItems objectAtIndex:nowPlayingIndex];
NSArray* array = [self mediaItemsArray:nowPlayingIndex];
if([array count])
[audioPlayer setQueueWithItemCollection:[MPMediaItemCollection
collectionWithItems:array]];
else {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Error - zero count query"
message:[NSString stringWithFormat:@"TITLE:%@, ALBUM:%@, ARTIST:%@",
[dic objectForKey:@"TITLE"],
[dic objectForKey:@"ALBUM"],
[dic objectForKey:@"ARTIST"]]
delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil];
[alert show];
[alert release];
return;
}
[audioPlayer setCurrentPlaybackTime:((location)? location: 0.3)];
[audioPlayer setVolume:volume / 100];
if(play)
[audioPlayer play];
}
That's all folks.
Now the audio player is loaded, ready (and playing). We can traverse the queue (if there is more than one item in the queue), pause, play again ...
[audioPlayer next];
History
The current version is 1.32. This is actually the first version released. The changes from 1.00 to 1.32 represent small enhancements and bug fixes discovered by me since I started actually using the application, and suggestion/bugs discovered by a small group of friendly users.
Once this app is used by more people, I will finalize the 2.00 version with whatever is found or suggested that makes sense to me.
A full change log has been included in the source zip download.
Revisions
I have made some changes to the application. Mostly bug fixes and some suggested enhancements.
The download now is version 2.00. Following is the list of changes:
- (Player) Added warning message when trying to create a media queue with more than 1 file.
- (Library form) Added support for manual reordering of media files.
- (Browser form) Added 'Composers' and 'Genre' to the initial list of collections/predicates.
- (Library form) Fix: Corrected section title of table view in edit and delete modes.
- (Library form) Fix: Stay in edit mode after edit action performed.
- (Library form) Replaced 'del' button functionality with cell swipe. Removed button.
- (Edit form) On entry, make edit control the first responder so it's focused and keyboard is showing.
- (Edit form) Rev: Removed erasing of Back button so Cancel can be achieved by rolling back.
- (All over) Prevents screens from tilting when device is (some screens don't look good in landscape orientation).
- (Library form) In auto-selection routine, also select active shelf.
- (Browser form) Added images to all table view cells.
- (Browser from) Rev: Used different images to indicate selection.