How to make a music player inside an app featuring: Bora Bora App – Tahiti and her islands

Wondering how to implement a music player inside an app? The interesting part of this short tutorial is the fact that our player also reacts and syncs when the user presses the volume control of his device. That is, we take the current “hardware volume” of his device and enable the user to manipulate the same volume inside the app, too, compatible for iPhone and iPad. Sounds easy, right?

Here’s a screenshot showing what we’re going to do:

Advertisement:

white,pages,yellow,tracker,search,find,gps,mobile,411,ringtone,business,911,likes,follower,locator

The first iPhone App ever that finds out who’s calling you before you answer the phone.


 

Our Jukebox – play, pause, nex, previous, fade in, fade out methods

We created a singleton JukeBox class with following features:

  • set up app player
  • stop
  • next
  • previous
  • stop for video (if we play a video inside the app, we want our background music to stop and continue once the video playback is finished)
  • do volume fade (fade in and fade out nicely :))

So let’s dig right into the code! Here’s the JukeBox.h file:

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

/*
 JukeBox is responsible to playback desired sounds and backgroundmusic
 */

@interface JukeBox : NSObject  {

	AVAudioPlayer *appPlayer;
}

@property (nonatomic, retain) AVAudioPlayer *appPlayer;

-(void)setUpAppPlayer;
-(void)stop;
-(void)next;
-(void)previous;

-(void)stopForVideo;
-(void)playForVideoIfItWasPlaying;

-(void)doVolumeFade;

+(JukeBox *)sharedInstance;

@end

As you can see we need the two frameworks Audio Toolbox and AVFoundation. The Audio Toolbox framework provides interfaces for recording, playback, etc. and the AVFoundation provides an Objective-C interface for managing and playing audio-visual media. That’s all you have to know for now, it’s just theory 😉

There are two things I have to point out: The method setUpAppPlayer and the static method sharedInstance. setUpAppPlayer makes the initialization of our appPlayer object that is An instance of the AVAudioPlayer class, which provides playback of audio data from a file or memory (wav or mp3 or whatever you may have :))
The sharedInstance is a method that enables us to interact with our JukeBox singleton. Whenever you have a class from which you are going to create only one instance throughout your whole app, it makes sense to have such one. For example if you’re inside your Viewcontroller and whant to play the next track you can make this call:

[[JukeBox sharedInstance] next];   // play next track in queue

That’s it!

Here’s the JukeBox.m file:

#import "JukeBox.h"
#import "ToolBarVC.h"
#import "Constants.h"

static JukeBox *staticJukeBox	= nil;
static int currentTrackIndex	= 0;
static BOOL alreadyPlayedIntro	= NO;
static BOOL isPlaying           = NO;
static BOOL isFadingOut         = NO;
static BOOL isFadingIn          = NO;
static BOOL firstTimeStarting   = YES;
static float maxTrackVolume     = 1.0f;

@implementation JukeBox

@synthesize appPlayer;

- (id) init {

	if ([super init]) {

		staticJukeBox = self;
        maxTrackVolume = 1.0f;
        firstTimeStarting = YES;

        [self setUpAppPlayer];
	}

	return self;
}

-(void)setUpAppPlayer {

    NSString *path;
    NSError *err;

    if (currentTrackIndex >= [TRACKQUEUE count])
        currentTrackIndex = 0;
    if (currentTrackIndex < 0)         currentTrackIndex = ([TRACKQUEUE count] - 1); 			     path = [[NSBundle mainBundle] pathForResource:[TRACKQUEUE objectAtIndex:currentTrackIndex] ofType:@"mp3"];     NSURL *url = [[[NSURL alloc] initFileURLWithPath:path] autorelease]; 	     [[AVAudioSession sharedInstance] setDelegate:self];     [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:&err];     [[AVAudioSession sharedInstance] setActive:YES error:&err]; 	 	if(nil != appPlayer)  		[appPlayer release]; 	     appPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&err]; 		 	if (nil != err) { 		 		NSLog(@"error: %@", [err description]); 	} 	     if(!firstTimeStarting) {         [appPlayer setVolume:maxTrackVolume];     }     else {         [appPlayer setVolume:0.0f];     }          [appPlayer setDelegate:self];	 	     if(nil != [ToolBarVC sharedInstance]) 			[[[ToolBarVC sharedInstance] trackLabel] setText:[TRACKNAMES objectAtIndex:currentTrackIndex]]; 	if([appPlayer prepareToPlay]) { 	 		[appPlayer play]; 	}          isPlaying = YES;          if(firstTimeStarting) {              isFadingIn = YES;         [self doVolumeFade];         firstTimeStarting = NO;     } } -(void)doVolumeFade {                if (appPlayer.volume > 0.0 && isFadingOut && [appPlayer isPlaying]) {

        appPlayer.volume = appPlayer.volume - 0.1;

        if(appPlayer.volume > 0.0) {
            [self performSelector:@selector(doVolumeFade) withObject:nil afterDelay:0.1]; 
        }
        else {

            appPlayer.volume = 0.0f;
            isFadingOut = NO;
        }
    } 
    else {
        isFadingOut = NO;
    }

    if(appPlayer.volume < maxTrackVolume && isFadingIn && [appPlayer isPlaying]){

        appPlayer.volume = appPlayer.volume + 0.1;

        if(appPlayer.volume < maxTrackVolume) {
            [self performSelector:@selector(doVolumeFade) withObject:nil afterDelay:0.1];  
        }
        else {

            appPlayer.volume = maxTrackVolume;
            isFadingIn = NO;
        }
    }
    else {
        isFadingIn = NO;
    }
}

-(void)stop {

	if(nil != appPlayer && [appPlayer isPlaying])
		[appPlayer stop];

    isPlaying = NO;
}

-(void)stopForVideo {

    if([appPlayer isPlaying]) {

        isFadingOut = YES;
        [self doVolumeFade];
    }
}

-(void)playForVideoIfItWasPlaying {

    if([appPlayer isPlaying]) {

            isFadingIn = YES;
            [self doVolumeFade];
    }
}

-(void)next {

	[self stop];
	currentTrackIndex = currentTrackIndex + 1;
	[self setUpAppPlayer];
}

-(void)previous {

	[self stop];

	if(currentTrackIndex == 0) {

		currentTrackIndex = [TRACKQUEUE count]-1;
	}
	else {

		currentTrackIndex = currentTrackIndex - 1;
	}

	[self setUpAppPlayer];
}

-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL) flag { 

	currentTrackIndex = currentTrackIndex + 1;
	[self setUpAppPlayer];
}

- (void) dealloc {

	[appPlayer release];

	currentTrackIndex	= 0;
	alreadyPlayedIntro  = NO;

	[super dealloc];
}

+(JukeBox *)sharedInstance {

    if(nil == staticJukeBox)
        staticJukeBox = [[JukeBox alloc] init];

	return staticJukeBox;
}

@end

 

OK I know, it’s a bunch of lines, but hey, you can grab it and edit and do whatever you want with it!
As you might have noticed, we imported two other header files on top of this file: ToolbarVC and Constants. ToolbarVC we need so we can tell the toolbar when we changed track so the toolbar can update it’s track labels. Inside the Constants file we have an array with all the names of our tracks we imported into our project:

#define TRACKQUEUE [NSArray arrayWithObjects:@"Monoi Tupuna", @"Virtual Trip - Tahiti 1", @"Tamariki Poerani - Mono'i Tupuna", @"Theo - Ute", @"Virtual Trip - Tahiti 2", nil]

Finally we need the GUI part, our player:

We initialize a MPVolumeView object, modify it’s appearance and add it as a subview of our toolbar like this:

    // Create MPVolumeView obj
    volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(84.0f, 0.0f, 100.0f, 45.0f)];

    // Accessing the UISlider
    for (UIView *view in [volumeView subviews]){
        if ([[[view class] description] isEqualToString:@"MPVolumeSlider"]) {

            volumeSlider = (UISlider *)view;
            [volumeSlider retain];
        }
    }

    // Customize our slider
    [volumeSlider setFrame:CGRectMake(93.0f, 0.0f, 100.0f, 45.0f)];
    [volumeSlider setMaximumValue:1.0f];
    [volumeSlider setMinimumValue:0.0f];
    [volumeSlider setBackgroundColor:[UIColor clearColor]];
    [volumeSlider setThumbImage:[UIImage imageNamed:@"volumeSliderButton.png"] forState:UIControlStateNormal];
    [volumeSlider setThumbImage:[UIImage imageNamed:@"volumeSliderButton.png"] forState:UIControlStateHighlighted];
    [volumeSlider setMinimumTrackImage:[[UIImage imageNamed:@"sliderBackgroundActive.png"] stretchableImageWithLeftCapWidth:10.0f topCapHeight:0.0f] forState:UIControlStateNormal];
    [volumeSlider setMaximumTrackImage:[[UIImage imageNamed:@"sliderBackgroundInactive.png"] stretchableImageWithLeftCapWidth:10.0f topCapHeight:0.0f] forState:UIControlStateNormal];
    [volumeSlider setMinimumValueImage:nil];
    [volumeSlider setMaximumValueImage:nil];

    [self.view addSubview:volumeSlider];

 

Now we need some Magic – sync with volume button signal of device

In your Toolbar init method you could add an observer and a method like this to manipulate our player:

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(volumeChanged)
                                                 name:@"AVSystemController_SystemVolumeDidChangeNotification"
                                               object:nil];
...
-(void)volumeChanged {
[[[JukeBox sharedInstance] appPlayer] setVolume:[volumeSlider value]];
}
...

Like this, whenever the user clicks on the volume button of his device, we can adapt our slider and set the volume accordingly.

 

Here’s the final result for those who are interested:

iTunes link: http://itunes.apple.com/us/app/bora-bora-tahiti-und-ihre/id526722265?mt=8

 

If you don’t understand the steps, don’t hesitate to comment and ask!

 

Cheers!

Gabe

2 comments

  1. Sham says:

    Good day I am so glad I found your site, I really found you by error, while I was brswiong on Yahoo for something else, Anyhow I am here now and would just like to say thank you for a fantastic post and a all round exciting blog (I also love the theme/design), I don’t have time to browse it all at the moment but I have book-marked it and also added in your RSS feeds, so when I have time I will be back to read much more, Please do keep up the awesome job.

Leave a Reply

Your email address will not be published. Required fields are marked *

*