3

I'm working on a soundboard app, that has several pages of buttons to play sound effects with a stop button on every page should the user wish to manually interrupt the clip. I'm using avaudioplayer in each view to play the sound upon pressing the button for that clip. It works fine until the view is changed. If a user jumps to a new page the sound keeps playing and the stop button stops working even if they return to the original view. Pressing a sound button no longer interrupts the running sound resulting in two sounds over each other.

From googling and searching this site, I know the issue is that each view change creates a new instance of the player and the remedy is to create a singleton class. Unfortunately I have yet to find any further examples of how to actually do this. If someone could provide or point the way to a beginners guide for creating an avaudioplayer singleton I would really appreciate it. All I need to be able to do is pass the file name to the shared player and start play with a sound clip button and have the stop button stop sounds no matter what view the user is on. I am using the ios 5.1 sdk with storyboards and ARC enabled.

James D.
  • 31
  • 1
  • 2

2 Answers2

10

My solution, as used in one of my own projects, is posted beneath. Feel free to copy-and-paste, I intend to open-source this project once it's finished :)

A preview of the player can be seen on YouTube: http://www.youtube.com/watch?v=Q98DQ6iNTYM

AudioPlayer.h

@protocol AudioPlayerDelegate;

@interface AudioPlayer : NSObject

@property (nonatomic, assign, readonly) BOOL isPlaying;
@property (nonatomic, assign) id <AudioPlayerDelegate> delegate;

+ (AudioPlayer *)sharedAudioPlayer;

- (void)playAudioAtURL:(NSURL *)URL;
- (void)play;
- (void)pause;

@end



@protocol AudioPlayerDelegate <NSObject>
@optional
- (void)audioPlayerDidStartPlaying;
- (void)audioPlayerDidStartBuffering;
- (void)audioPlayerDidPause;
- (void)audioPlayerDidFinishPlaying;
@end

AudioPlayer.m

// import AVPlayer.h & AVPlayerItem.h


@interface AudioPlayer ()
- (void)playerItemDidFinishPlaying:(id)sender;
@end


@implementation AudioPlayer
{
    AVPlayer *player;
}

@synthesize isPlaying, delegate;

+ (AudioPlayer *)sharedAudioPlayer
{
    static dispatch_once_t pred;
    static AudioPlayer *sharedAudioPlayer = nil;
    dispatch_once(&pred, ^
    { 
        sharedAudioPlayer = [[self alloc] init]; 

        [[NSNotificationCenter defaultCenter] addObserver:sharedAudioPlayer selector:@selector(playerItemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    });
    return sharedAudioPlayer;
}

- (void)playAudioAtURL:(NSURL *)URL
{
    if (player)
    {
        [player removeObserver:self forKeyPath:@"status"];
        [player pause];
    }

    player = [AVPlayer playerWithURL:URL];
    [player addObserver:self forKeyPath:@"status" options:0 context:nil];

    if (delegate && [delegate respondsToSelector:@selector(audioPlayerDidStartBuffering)])
        [delegate audioPlayerDidStartBuffering];
}

- (void)play
{
    if (player) 
    {
        [player play];

        if (delegate && [delegate respondsToSelector:@selector(audioPlayerDidStartPlaying)])
            [delegate audioPlayerDidStartPlaying];
    }
}

- (void)pause
{
    if (player) 
    {
        [player pause];

        if (delegate && [delegate respondsToSelector:@selector(audioPlayerDidPause)])
            [delegate audioPlayerDidPause];
    }
}

- (BOOL)isPlaying
{
    DLog(@"%f", player.rate);

    return (player.rate > 0);
}

#pragma mark - AV player 

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{
    if (object == player && [keyPath isEqualToString:@"status"]) 
    {
        if (player.status == AVPlayerStatusReadyToPlay) 
        {
            [self play];
        }
    }
}

#pragma mark - Private methods

- (void)playerItemDidFinishPlaying:(id)sender
{
    DLog(@"%@", sender);

    if (delegate && [delegate respondsToSelector:@selector(audioPlayerDidFinishPlaying)])
        [delegate audioPlayerDidFinishPlaying];
}

@end

AudioPlayerViewController.h

extern NSString *const kAudioPlayerWillShowNotification;
extern NSString *const kAudioPlayerWillHideNotification;


@interface AudioPlayerViewController : UIViewController

@property (nonatomic, assign, readonly) BOOL isPlaying;
@property (nonatomic, assign, readonly) BOOL isPlayerVisible;

- (void)playAudioAtURL:(NSURL *)URL withTitle:(NSString *)title;
- (void)pause;

@end

AudioPlayerViewController.m

NSString *const kAudioPlayerWillShowNotification = @"kAudioPlayerWillShowNotification";
NSString *const kAudioPlayerWillHideNotification = @"kAudioPlayerWillHideNotification";


@interface AudioPlayerViewController () <AudioPlayerDelegate>

@property (nonatomic, strong) AudioPlayerView *playerView;

- (void)playButtonTouched:(id)sender;
- (void)closeButtonTouched:(id)sender;
- (void)hidePlayer;

@end


@implementation AudioPlayerViewController

@synthesize playerView, isPlaying, isPlayerVisible;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) 
    {
        playerView = [[AudioPlayerView alloc] initWithFrame:CGRectZero];

        [AudioPlayer sharedAudioPlayer].delegate = self;
    }
    return self;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - View lifecycle

// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView
{
    self.view = playerView;
}


// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
    [super viewDidLoad];

    [playerView.playButton addTarget:self action:@selector(playButtonTouched:) forControlEvents:UIControlEventTouchUpInside];
    [playerView.closeButton addTarget:self action:@selector(closeButtonTouched:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - Private methods

- (AudioPlayerView *)playerView
{
    return (AudioPlayerView *)self.view;
}

- (void)hidePlayer
{
    [[NSNotificationCenter defaultCenter] postNotificationName:kAudioPlayerWillHideNotification object:nil];
    [self.playerView hidePlayer];
}

- (void)playButtonTouched:(id)sender
{
    DLog(@"play / pause");

    if ([AudioPlayer sharedAudioPlayer].isPlaying) 
    {
        [[AudioPlayer sharedAudioPlayer] pause];
    }
    else
    {
        [[AudioPlayer sharedAudioPlayer] play];
    }

    [self.playerView showPlayer];
}

- (void)closeButtonTouched:(id)sender
{
    DLog(@"close");

    if ([AudioPlayer sharedAudioPlayer].isPlaying)
        [[AudioPlayer sharedAudioPlayer] pause];

    [self hidePlayer];
}

#pragma mark - Instance methods

- (void)playAudioAtURL:(NSURL *)URL withTitle:(NSString *)title
{
    playerView.titleLabel.text = title;
    [[AudioPlayer sharedAudioPlayer] playAudioAtURL:URL];

    [[NSNotificationCenter defaultCenter] postNotificationName:kAudioPlayerWillShowNotification object:nil];
    [playerView showPlayer];
}

- (void)pause
{
    [[AudioPlayer sharedAudioPlayer] pause];

    [[NSNotificationCenter defaultCenter] postNotificationName:kAudioPlayerWillHideNotification object:nil];
    [playerView hidePlayer];
}

#pragma mark - Audio player delegate

- (void)audioPlayerDidStartPlaying
{
    DLog(@"did start playing");

    playerView.playButtonStyle = PlayButtonStylePause;    
}

- (void)audioPlayerDidStartBuffering
{
    DLog(@"did start buffering");

    playerView.playButtonStyle = PlayButtonStyleActivity;
}

- (void)audioPlayerDidPause
{
    DLog(@"did pause");

    playerView.playButtonStyle = PlayButtonStylePlay;
}

- (void)audioPlayerDidFinishPlaying
{
    [self hidePlayer];
}

#pragma mark - Properties

- (BOOL)isPlaying
{
    return [AudioPlayer sharedAudioPlayer].isPlaying;
}

- (BOOL)isPlayerVisible
{
    return !playerView.isPlayerHidden;
}

@end

AudioPlayerView.h

typedef enum 
{
    PlayButtonStylePlay = 0,
    PlayButtonStylePause,
    PlayButtonStyleActivity,
} PlayButtonStyle;


@interface AudioPlayerView : UIView

@property (nonatomic, strong) UIButton                *playButton;
@property (nonatomic, strong) UIButton                *closeButton;
@property (nonatomic, strong) UILabel                 *titleLabel;
@property (nonatomic, strong) UIActivityIndicatorView *activityView;
@property (nonatomic, assign) PlayButtonStyle         playButtonStyle;
@property (nonatomic, assign, readonly) BOOL          isPlayerHidden;

- (void)showPlayer;
- (void)hidePlayer;

@end

AudioPlayerView.m

@implementation AudioPlayerView
{
    BOOL _isAnimating;
}

@synthesize playButton, closeButton, titleLabel, playButtonStyle, activityView, isPlayerHidden = _playerHidden;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) 
    {
        self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"musicplayer_background.png"]];

        _playerHidden = YES;

        activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];        
        activityView.frame = CGRectMake(0.0f, 0.0f, 30.0f, 30.0f);
        [self addSubview:activityView];

        playButton = [[UIButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 30.0f, 30.0f)];
        [playButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [playButton setBackgroundImage:[UIImage imageNamed:@"button_pause.png"] forState:UIControlStateNormal];
        playButton.titleLabel.textAlignment = UITextAlignmentCenter;
        [self addSubview:playButton];

        closeButton = [[UIButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 30.0f, 30.0f)];
        [closeButton setBackgroundImage:[UIImage imageNamed:@"button_close.png"] forState:UIControlStateNormal];
        [closeButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        closeButton.titleLabel.textAlignment = UITextAlignmentCenter;
        [self addSubview:closeButton];        

        titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 240.0f, 30.0f)];
        titleLabel.text = nil;
        titleLabel.textAlignment = UITextAlignmentCenter;
        titleLabel.font = [UIFont boldSystemFontOfSize:13.0f];
        titleLabel.numberOfLines = 2;
        titleLabel.textColor = [UIColor whiteColor];
        titleLabel.backgroundColor = [UIColor clearColor];
        [self addSubview:titleLabel];
    }
    return self;
}

- (void)layoutSubviews
{    

#define PADDING 5.0f

    DLog(@"%@", NSStringFromCGRect(self.bounds));
    CGRect frame = self.bounds;
    CGFloat y = frame.size.height / 2;

    titleLabel.center = CGPointMake(frame.size.width / 2, y);

    CGFloat x = titleLabel.frame.origin.x - (playButton.frame.size.width / 2) - PADDING;
    playButton.center = CGPointMake(x, y);
    activityView.center = CGPointMake(x, y);

    x = titleLabel.frame.origin.x + titleLabel.frame.size.width + (closeButton.frame.size.width / 2) + PADDING;
    closeButton.center = CGPointMake(x, y);
}

#pragma mark - Instance methods

- (void)showPlayer
{
    if (_isAnimating || _playerHidden == NO)
        return;

    _isAnimating = YES;

    [UIView 
     animateWithDuration:0.5f 
     animations:^ 
     {
         CGRect frame = self.frame;
         frame.origin.y -= 40.0f;
         self.frame = frame;         
     } 
     completion:^ (BOOL finished) 
     {
         _isAnimating = NO;
         _playerHidden = NO;    
     }];
}

- (void)hidePlayer
{
    if (_isAnimating || _playerHidden)
        return;

    _isAnimating = YES;

    [UIView 
     animateWithDuration:0.5f 
     animations:^ 
     {        
         CGRect frame = self.frame;
         frame.origin.y += 40.0f;
         self.frame = frame;
     }
     completion:^ (BOOL finished) 
     {
         _isAnimating = NO;
         _playerHidden = YES;    
     }];
}

- (void)setPlayButtonStyle:(PlayButtonStyle)style
{
    playButton.hidden = (style == PlayButtonStyleActivity);
    activityView.hidden = (style != PlayButtonStyleActivity);

    switch (style) 
    {
        case PlayButtonStyleActivity:
        {
            [activityView startAnimating];
        }
            break;
        case PlayButtonStylePause:
        {
            [activityView stopAnimating];

            [playButton setBackgroundImage:[UIImage imageNamed:@"button_pause.png"] forState:UIControlStateNormal];
        }
            break;
        case PlayButtonStylePlay:
        default:
        {
            [activityView stopAnimating];

            [playButton setBackgroundImage:[UIImage imageNamed:@"button_play.png"] forState:UIControlStateNormal];
        }
            break;
    }

    [self setNeedsLayout];
}

@end

AppDelegate - didFinishLaunching

// setup audio player

audioPlayer = [[AudioPlayerViewController alloc] init]; // public property ...
CGRect frame = self.window.rootViewController.view.frame;
UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
CGFloat tabBarHeight = tabBarController.tabBar.frame.size.height;
audioPlayer.view.frame = CGRectMake(0.0f, frame.size.height - tabBarHeight, 320.0f, 40.0f);
[self.window.rootViewController.view insertSubview:audioPlayer.view belowSubview:tabBarController.tabBar];

From any view controller inside the app I start audio with the following code:

- (void)playAudioWithURL:(NSURL *)URL title:(NSString *)title
{
    OnsNieuwsAppDelegate *appDelegate = (OnsNieuwsAppDelegate *)[[UIApplication sharedApplication] delegate];
    [appDelegate.audioPlayer playAudioAtURL:URL withTitle:title];
}

Assets

For the above example, the following assets can be used (button images are white, so hard to see against background):

Buttons: Close Pause Play

Background: Background

Wolfgang Schreurs
  • 11,779
  • 7
  • 51
  • 92
  • Super! It was a great help. I'd been messing around with my code without any success, and could finally find this link and your code works great. However I need to reimplement the rest of my player, but that should be fine. Thanks a lot! P.S. I edited `UITextAlignmentCenter` to `NSTextAlignmentCenter` since the former is deprecated now. – Neeku Nov 25 '13 at 19:55
  • @Wolfgang I know this might be too much, but have you implemented the methods to have a seek slider showing the audio progress and enabling the user update it? If so, could you share that? I had made a nice `AVAudioPlayer`, but since I couldn't get a singleton working for it, so I implemented your code after spending days on it. Now Your code works perfectly, but I can't get the slider work for it. – Neeku Nov 26 '13 at 10:32
  • @Neeku: I haven't implemented a slider, but it's pretty easy to do. If I find some time today, I'll add the code for sliding. In the mean time you might be able to figure out a solution yourself by looking at the following topic: http://stackoverflow.com/questions/2654849/uislider-to-control-avaudioplayer – Wolfgang Schreurs Nov 26 '13 at 11:01
  • @WolfgangSchreurs Thanks a lot for the reply! I saw that link earlier today, but couldn't figure it out. I was trying to implement Apple's sample code: https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010101 But all I've done is that made a huge mess in your code without success. Part of it is that I'm not that great in implementing UI stuff programmatically, and also confused where to use each method. The only part I could do was the fastforward and rewind by 15 seconds (that doesn't sync with the slider!) – Neeku Nov 26 '13 at 11:08
  • @WolfgangSchreurs I know you might be busy, but could you do anything with the seek slider? :-S I'm still stuck with something else now! – Neeku Nov 27 '13 at 10:52
  • @WolfgangSchreurs I am getting this error -[AudioPlayerViewController respondsToSelector:]: message sent to deallocated instance This is my code. [[AudioPlayer sharedAudioPlayer]playAudioAtURL:trackURL]; AudioPlayerViewController *aview = [[AudioPlayerViewController alloc]init]; [appDelegate.window addSubview:aview.view]; – Maulik Vekariya Apr 30 '15 at 06:15
2

There's a lot of discussion (and links to blogs, etc.) about singletons over at What should my Objective-C singleton look like?, and I see a fair number of tutorials as a result of this Google search: http://www.google.com/search?q=+cocoa+touch+singleton+tutorial, but the real answer to your question, I believe, is that you should do one of two things:

If you do want the sound for a particular view to continue playing when the user switches, create the player as you're doing now, but when the view (re)appears, check that a player exists, and don't make a new one.

If you want the sound to stop, then stop the sound when the view changes (i.e., in viewWillDisappear:).

Community
  • 1
  • 1
jscs
  • 63,694
  • 13
  • 151
  • 195
  • I do want the sound to continue if views change but even if I check to see if a player already exists upon returning to the view, the stop button no longer stops the sound that is playing. The code for the stop button is just [audioplayer stop]: . It seems that upon returning to the original view, [audioplayer stop] is no longer associated with that particular instance. – James D. Mar 26 '12 at 19:43