Objective-C, need help building a singleton AVAudioPlayer

I'm working on a sound card device that has multiple pages of buttons to play sound effects with a stop button on each page if the user wants to manually interrupt a clip. I am using avaudioplayer in each view to play audio on button click for that clip. It works fine until the view is changed. If the user navigates to a new page, the sound continues to play and the stop button stops working even if they return to their original view. Pressing a sound button no longer interrupts the sound, resulting in two sounds on top of each other.

From searching and searching this site, I know the problem is that each view change creates a new player instance, and the tool creates a singleton class. Unfortunately, I have yet to find further examples of how to actually do this. If someone could point or point the path to a beginner's guide for creating an avaudioplayer single, I would really appreciate it. All I have to do is pass the filename to the shared player and start playing with the sound clip button and stop the stop button, no matter what kind of user is on. I am using ios 5.1 sdk with storyboards and ARC support.

+3


source to share


2 answers


My solution used in one of my own projects is posted below. Feel free to copy and paste, I intend to open this open source 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 a sound with the following code:

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

      

Assets

In the example above, the following assets can be used (button images are white, so difficult to see in the background):

Buttons: ClosePausePlay

Background: Background

+10


source


There's a lot of discussion (and links to blogs, etc.) about singlons at What should an Objective-C singleton look like? and I can see from 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 want the sound for a particular view to keep playing when the user switches, create the player as you are doing now, but when the view (re) appears, make sure the player exists and don't make a new one.



If you want the sound to stop, stop the sound when you change the view (i.e. at viewWillDisappear:

).

+2


source







All Articles