Search This Blog

Wednesday, April 11, 2012

EyeMusix Player using AVFoundation


Creating a music player in iphone sdk 3.0 using AVFoundationFramework

This was a very tough task since I am new to iphone application development and as for objective c (which is quite a different programming language to me in the beginning). But I took the challenge to develop the Mobile Application Development pair project which is based on an MP3 player for the blind or visually impaired. The task was to develop a general usage application but that can be used by visually impaired wilt sufficient aids to fulfill their requirements as well.


When I first did the literature research on dealing with audio in iphone development, it had several choices to go but I chose AVFoundation Framework which is one of several frameworks that we can use to play and create time-based audiovisual media. It provides an Objective –C interface we use to work on a detailed level with time-based audiovisual data. For an example, we can use it to examine, create and edit media files. We can also get input streams from devices and manipulate video during real-time capture and playback.

My first attempt was to create a simple iphone application using View Based applications. This was done basically to understand the environment that I need to deal with throughout the semester. In every sample code I came across, they used to playback audio files which was added to our project itself. It worked fine for me. This is all I did,

·         Created a view based application with a basic design to play, pause, stop, fast forward, rewind, volume increase/decrease and timeline handling.

·         Added AVFoundation framework to my project’s frameworks directory

·         Added a sample audio file to the project’s resources directory

·         Created an NSUrl to the audio file in the resources directory and created an instance of AVAudioPlayer

From there, the rest of the tasks could be handled without many problems at all. Then the problem rose when I needed to playback a collection of songs. to try this out, I added some more audio files to the project itself and selected them via a data picker controller and used them as the same as above. But still, is this the real scenario that I needed? My requirement is to have a player to control all the playback tasks and a playlist to select songs by the user. And also another welcome screen to give the first view for the user to choose what they wish to do with the player because it is not going to be an ordinary player. I had some other problems in retrieving metadata of the songs as well.
The big story behind this is that did all these testing of the app completely on the iOS Simulator 
There should be a way to add audio files to the simulator and access them via my application or add them to my play list. There was no way for me to do it since it wasn’t easy as dragging and dropping an image to the simulator from the ios desktop and save it in the photo gallery of the simulator. I searched for it in every possible website but failed because as far as I realized that is impossible.
Alternatively I found some solutions for this. That is to use Media Player framework and access the ipod library which holds a collection of audio files in the system. I tried out the coding by doing my project from the scratch but finally when I run the application in the simulator, all what I got was…

Unable to launch iPod music player server: application not found

Message playbackState timed out.

Unable to access iPod library.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present a nil modal view controller on target <MusicPlayerViewController: 0x5758e90>.'

Then I wanted to know why this happened and I got the answer within few seconds by reading the exception itself. That is we cannot access the ipod library via the ios simulator because it does not exist. Since the progress evaluation presentation days got closer, I had no way to continue with my project because of many reasons. First is, I must test my app in a real device to access ht ipod library to get playlist items for my player. Second is, I had no device to test it. Third is, even though I managed to borrow a device somehow to test it on my own device I will need to pay the $100 to grab the certificate, unless my device would be jail broken. Finally, I could not even think of this in a hurry for getting ready for my evaluation.
I was thinking for days on how to solve the problem because it is a real issue for students or beginners who are willing to work with iphone development but it is not being open source. I contacted my friends and lecturers as well in order to find a solution but all what I had to do was to wait until I get an account somehow. Anyways, I had to find a solution for my issue.  
I decided to continue my project with a limitation where I created my own song library and make use of it. I don’t know that I actually call it a library because it is just a simple concept to carry on my work. I describe how I created my EyeMusixPlayer (that is the name of the projectJ) application step by step. 


Following are the three views that I have designed so far,
the welcome screen design


player screen design


The playlist’s design view  (just a UItableView with the black navigation bar on top)


This is how each view looks like in running mode,
How the icon appears in the home screen

the welcome screen while running (four buttons and an image view in the background)

when clicked on the Playlist from the welcome screen, the playlist appears (titles contains the name of the song, song duration as the subtitle, album art ( for the moment the pictures are the same since I still cannot find a way to get  the album art of each song from the AVAudioPlayer instance) and the accessory type is UITableViewCellAccessoryDetailDisclosureButton)



On selecting the song as above, the player view appears and the song starts to play. Following image shows the playing mode of the song. Play button icon changes to pause button icon, the status of the song, shows in the timeline along with the current time and remaining time in both the sides. The current volume will be shown near the volume controller

                                                                                
When the song is paused, the label on top, mentions so and also the pause button icon changes in to play button icon. User can click on play button to resume from where he stopped

                   
When the song stopped, the label on top mentions so and also the pause button changes to play button icon again where the user can click play button to play the song from the beginning


If the playlist icon in the bottom tool bar of the player view is clicked, the playlist appears and another song can be selected to play. Tool bar items are used to view playlist, shuffle the song list (play songs in random order), replay the song once and enable/disable narrator feature (note that it is not implemented yet) respectively


Above image shows when the user navigates to playlist from the player view how it looks like. Note the navigation bar item lets the user to go back to the player view if needed. This is the advantage in using the navigation-based-application instead of view-based-application.
The codes that I thought would be important are shown below.
First of all, it is good to have a look at the header files of player view (PlayerView.h) and playlist view (RootViewController.h)

// PlayerView.h
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>  //do not forget to add this framework

@interface PlayerView : UIViewController
<AVAudioPlayerDelegate>
{
    NSMutableArray            *songs;
    AVAudioPlayer            *audioPlayer;
    NSString                  *selectedSong;
    NSInteger                  selectedIndex;
    NSString                  *duration;
    NSString                  *currentVolume;
    NSTimer                   *updateTimer;
   
    IBOutlet UIButton         *playPauseButton;
    IBOutlet UIButton         *stopButton;
    IBOutlet UILabel          *modeLabel;
    IBOutlet UIImageView      *albumArt;
    IBOutlet UIButton         *nextButton;
    IBOutlet UIButton         *previousButton;
    IBOutlet UILabel          *durationLabel;
    IBOutlet UILabel          *playmodeLabel;
    IBOutlet UISlider         *volumeControl;
    IBOutlet UIButton         *volumeIncreaseButton;
    IBOutlet UIButton         *volumeDecreaseButton;
    IBOutlet UILabel          *audioVolume;
    IBOutlet UILabel          *audioCurrentTime;
    IBOutlet UILabel          *remainingTime;
    IBOutlet UISlider         *progressBar;
}

@property (nonatomic,retain)NSMutableArray            *songs;
@property (nonatomic,retain)AVAudioPlayer             *audioPlayer;
@property(nonatomic,retain)NSString                  *selectedSong;
@property(assign,nonatomic)NSInteger                  selectedIndex;
@property(nonatomic,retain)NSString                   *duration;
@property(nonatomic,retain)IBOutlet UIButton          *playPauseButton;
@property(nonatomic,retain)IBOutlet UIButton          *stopButton;
@property(nonatomic,retain)IBOutlet UILabel           *modeLabel;
@property(nonatomic,retain)IBOutlet UIImageView       *albumArt;
@property(nonatomic,retain)IBOutlet UISlider          *volumeControl;
@property(nonatomic,retain)IBOutlet UIButton          *volumeIncreaseButton;
@property(nonatomic,retain)IBOutlet UIButton          *volumeDecreaseButton;
@property(nonatomic,retain)IBOutlet UILabel           *audioVolume;
@property(nonatomic,retain)IBOutlet UILabel           *audioCurrentTime;
@property(nonatomic,retain)IBOutlet UILabel           *remainingTime;
@property (nonatomic, retain)UISlider                 *progressBar;
@property (nonatomic, retain)NSTimer                  *updateTimer;


-(IBAction)playPause:(id)sender;
-(IBAction)stop:(id)sender;
-(IBAction)next:(id)sender;
-(IBAction)previous:(id)sender;
-(IBAction)adjustVolume:(id)sender;
-(IBAction)volumeIncrease:(id)sender;
-(IBAction)volumeDecrease:(id)sender;
-(IBAction)toolBarcSelected:(id)sender;
-(IBAction)progressSliderMoved:(id)sender;

@end
//have to set @synthesize property for each identifiers that we set @property


//  RootViewController.h

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h> 

@interface RootViewController : UITableViewController
{
    IBOutlet UITableView      *Songtable;
    NSMutableArray            *songs;
    AVAudioPlayer             *audioPlayer;
    NSString                  *selectedSong;
    NSInteger                  selectedIndex;
    UIImageView               *albumArt;
}
@property(nonatomic,retain)IBOutlet UITableView *Songtable;
@property(nonatomic,retain)NSMutableArray       *songs;
@property(nonatomic,retain)AVAudioPlayer        *audioPlayer;
@property(nonatomic,retain)NSString             *selectedSong;
@property(assign,nonatomic)NSInteger            selectedIndex;
@property(nonatomic,retain)UIImageView          *albumArt;

@end

//have to set @synthesize property for each identifiers that we set @property

How to get the audio files from the resource directory and create an NSMutableArray which contains the name if all the songs in the directory?

 Another couple of class files (ClassEyeMusix.h and ClassEyeMusix.m ) contain code for this, which can be globally accessed by the player and the playlist classes
//  ClassEyeMusix.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>  

@interface ClassEyeMusix : UIViewController
{
    NSMutableArray *songsArray;
    AVAudioPlayer *audioPlayer;
    NSString *selectedSong;
    NSInteger selectedIndex;
}

@property(nonatomic,retain) NSMutableArray *songsArray;
@property(nonatomic,retain) NSString *selectedSong;
@property(assign,nonatomic)NSInteger selectedIndex;

-(NSMutableArray *)createSongArray;
-(AVAudioPlayer *)createAudioPlayer:(NSString *)selectedSong;

@end

//  ClassEyeMusix.m

#import "ClassEyeMusix.h"
#import <AVFoundation/AVFoundation.h> 

@implementation ClassEyeMusix

@synthesize songsArray;
@synthesize selectedSong;
@synthesize selectedIndex;

-(NSMutableArray *)createSongArray
{
     //gets the path to the resources folder to an NSString
    NSString * resourcePath = [[NSBundle mainBundle] resourcePath];

    NSError * error;

     //gets the directory contents(all files) to an NSArray
     NSArray * directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath: resourcePath error:&error];

     //creating an NSMutableAray named songsArray to hold all the mp3 files
    NSString *song;
    int songArrayIndex=0;
    songsArray=[NSMutableArray new];


 for (int i=0; i<directoryContents.count; i++)
 {   
     song=[directoryContents objectAtIndex:i];
    
     if([song hasSuffix:@".mp3"])
     {
         [songsArray insertObject:song atIndex:(NSUInteger)songArrayIndex];
         songArrayIndex++;
     }
 }    
    return songsArray;
   
}

How to create an instance of the AVAudioPlayer using a selected song?

-(AVAudioPlayer *)createAudioPlayer:(NSString *)pselectedSong
{
  self.selectedSong=pselectedSong;

   //self.selectedIndex=[songsArray indexOfObject:selectedSong];
   //creates a url to get the song from its path
  NSURL *songUrl = [NSURL fileURLWithPath:
                  [NSString stringWithFormat:@"%@/%@",
                  [[NSBundle mainBundle] resourcePath],
                  pselectedSong]];                     
   
  //error object to ditect errors
  NSError *error;                                       
   
 //creating the audioPlayer Object
 audioPlayer = [[AVAudioPlayer alloc]
                  initWithContentsOfURL:songUrl
                  error:&error];
       
return audioPlayer;
   
}

How to add the song list to the table view controller?

// Customize the number of sections in the table view.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return songs.count;

}

// Customize the appearance of table view cells.
//clsEye is the instance of ClassEyeMusix
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   
audioPlayer=[clsEye createAudioPlayer:[songs  objectAtIndex:indexPath.row]];

    NSString *songDuration =[self getSongDuration] ;
   
    UIImage *image=[UIImage imageNamed:@"no_artwork.png"];
       
    static NSString *CellIdentifier = @"Cell";
   
 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil)
     {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
     }

    cell.textLabel.text=[songs objectAtIndex:indexPath.row];
    cell.detailTextLabel.text=songDuration;   
    cell.accessoryType=UITableViewCellAccessoryDetailDisclosureButton;
    cell.imageView.image=image;
   
    return cell;
}
//implementation of getSongDuration method
-(NSString *)getSongDuration
{
    return[NSString stringWithFormat:@"%d:%02d",
     (int)audioPlayer.duration / 60,
     (int)audioPlayer.duration % 60, nil];
}

How to let the song to play when selecting the song from the table cell meanwhile going to the player view?

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    selectedSong=[songs objectAtIndex:indexPath.row];
    selectedIndex=[songs indexOfObject:selectedSong];
   
    if(player.audioPlayer.playing==YES )
    {
            [player.audioPlayer stop];
             player.audioPlayer=nil;
    }
   
    if(player.audioPlayer.playing==false)
    {
    audioPlayer=[clsEye createAudioPlayer:selectedSong];
    [audioPlayer prepareToPlay];
    [audioPlayer play];

    player = [[PlayerView alloc] initWithNibName:@"PlayerView" bundle:nil];
    player.songs=self.songs;
    player.audioPlayer=self.audioPlayer;
    player.selectedSong=self.selectedSong;
    player.selectedIndex=self.selectedIndex;
    player.duration=[self getSongDuration];
   
    [self.navigationController pushViewController:player animated:YES];
   
    [player release];
    }
}
The above method hands over the control of the audioPlayer object to the player view where the player view continues with its tasks

How to handle play/pause tasks?

//following method will be invoked when clicking on  the play/pause button
-(IBAction)playPause:(id)sender;
{  
   
    if(audioPlayer==nil)
    {
        audioPlayer=[clsEye createAudioPlayer:selectedSong];
        [self playAudio];
    }

    if(audioPlayer!=nil)
    {
        if(audioPlayer.playing)
        {
            [self pauseAudio];
        }
        else
        {               
            [self playAudio];
        }
   
    }
}
//implantation of playAudio method
-(void)playAudio
{
[playPauseButton setImage:pauseButtonImage forState:UIControlStateNormal];
     [audioPlayer play];
}

//implementation of pauseAudio method
-(void)pauseAudio
{
    [playPauseButton setImage:playButtonImage forState:UIControlStateNormal];
    [audioPlayer pause];
}

How to stop the now playing audio?

-(IBAction)stop:(id)sender;
{
    if(audioPlayer.playing)
    {   
        [self stopAudio];
    }       
}
-(void)stopAudio
{
    [audioPlayer stop];
    audioPlayer =nil;
    [playPauseButton setImage:playButtonImage forState:UIControlStateNormal];
}

How to play the next song?

-(IBAction)next:(id)sender
{
    if(audioPlayer.playing)
    {
        [self stopAudio];
        [self playNext];
        [self playAudio];
    }
}
-(void)playNext
{
    if(selectedIndex==songs.count-1)
    {
        selectedIndex=0;
    }
   
    else
    {
        selectedIndex++;
    }
    self.selectedSong=[songs objectAtIndex:selectedIndex];
    audioPlayer=[clsEye createAudioPlayer:selectedSong];   
}

How to play the previous song?

-(void)playPrevious
{
    if(selectedIndex==0)
    {
        selectedIndex=songs.count-1;
    }
   
    else
    {
        selectedIndex--;
    }
    self.selectedSong=[songs objectAtIndex:selectedIndex];
    audioPlayer=[clsEye createAudioPlayer:selectedSong];
}
-(IBAction)previous:(id)sender
{
    if(audioPlayer.playing)
    {
        [self stopAudio];
        [self playPrevious];
        [self playAudio];
    }      
}

How to replay the now playing song once?

        if(audioPlayer !=nil && audioPlayer.playing)
        {
            audioPlayer.numberOfLoops=1;   
        }
           

How to shuffle the playlist?

- (void)shuffle
    static BOOL seeded = NO;
    if(!seeded)
    {
        seeded = YES;
        srandom(time(NULL));
    }
   
    NSUInteger count = self.songs.count;

    for (NSUInteger i = 0; i < count; ++i)
     {
        // Select a random element between i and end of array to swap with.
        int nElements = count - i;
        int n = (random() % nElements) + i;
        [self.songs exchangeObjectAtIndex:i withObjectAtIndex:n];
    }
       
}

How to increase/decrease volume by moving the volume control slider?

//method to adjust volume of the playing audio via the horizontal slider
//can both increase and decrease
//can increase up to 2.0 and decrease to 0.0

-(IBAction)adjustVolume:(id)sender
{
    if (audioPlayer!= nil)
    {
       
        if(audioPlayer.volume<=0.0)
      {
            //if the volume is not mute, then display volume down icon
UIImage *muteImage=[UIImage imageNamed:@"ic_lock_silent_mode.png"];
[volumeDecreaseButton setImage:muteImage forState:UIControlStateNormal];
        }
       
        else
        {
            // if the volume reached mute, then show mute icon                                          
            UIImage *volumeImage=[UIImage imageNamed:@"volumeDown_.png"];
[volumeDecreaseButton setImage:volumeImage forState:UIControlStateNormal];
        }

            //show the changed volume value in the slider
audioPlayer.volume = volumeControl.value;          currentVolume=[[NSString alloc] initWithFormat:@"%2.2f",audioPlayer.volume];
           
    }
   
}

I hope to add text to speech feature to the existing application. I have already done search on it and found that there is no inbuilt library in ios to get this feature instead have to go for third party libraries.

OpenEars
flite

are some of them. I tried OpenEars but it didn't worked for me.My application gets stucked in the middle when i tried to run it using open ears. have to go for the best option which can be run in iphone sdk 3.0.