Hashem Aboonajmi
Hashem Aboonajmi

Reputation: 13880

childViewController doesn't release after playing sound

I have a pageViewController (UIPageViewController) which has a child view controller (named soundPlayer). sound player has static AVAudioPLayer *player when navigating between pages we must have on player (singleton pattern). so if I navigate between pages without playing sound, and finally back from these view controller (navigation controller) soundPlayer will be released. but when I play sound and navigate between pages, with every page flipping a new soundPlayer doesn't release and a new one will create!

before playing sound with every page flip, only we have on soundPlayerViewController, but when we play sound with every page flip, soundPlayerViewController doesn't release and living objects increases.

before playing sound enter image description here

after playing sound

enter image description here

ignore those leaks in my real app I don't have them

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

@class AppScrollView;


@interface SoundPlayerViewController : UIViewController <AVAudioPlayerDelegate>{


    BOOL                                paused;
    BOOL                                inBackground;
    NSTimer                             *updateTimer;
    UIImage                             *playBtnBG;
    UIImage                             *pauseBtnBG;
    UIImage                             *bookmarkedBtnBG;
}


@property (weak, nonatomic)     IBOutlet    UILabel        *duration;
@property (weak, nonatomic)     IBOutlet    UILabel        *currentTime;
@property (weak, nonatomic)     IBOutlet    UISlider       *progressBar;
@property (nonatomic, strong)               NSTimer        *updateTimer;
@property (nonatomic, assign)   BOOL                       inBackground;
@property (weak, nonatomic)     IBOutlet    UIButton       *playButton;

- (IBAction)playButtonPressed:(UIButton *)sender;

- (IBAction)progressSliderMoved:(UISlider *)sender;

- (void)updateViewForPlayerState:(AVAudioPlayer *)player;
- (void)updateViewForPlayerStateInBackground:(AVAudioPlayer *)player;
- (void)updateViewForPlayerInfo:(AVAudioPlayer *)player;
- (void)updateCurrentTimeForPlayer:(AVAudioPlayer *)player;
- (void)registerForBackgroundNotifications;
- (void)updateBookmarkButton;

- (void)stopSoundPlayer;

@end

SoundPlayerViewController.m

#import "SoundPlayerViewController.h"

@interface SoundPlayerViewController ()

- (void)customizeAppearance;

@end

@implementation SoundPlayerViewController

static AVAudioPlayer              *soundPlayer;

@synthesize numberFormatter;
@synthesize duration;
@synthesize currentTime;
@synthesize progressBar;
@synthesize updateTimer;
@synthesize inBackground;
@synthesize playButton;    

#pragma mark - View lifecycle
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self customizeAppearance];
    paused = true;

    [self registerForBackgroundNotifications];

    NSError *error = nil;

        NSString *fileName = @"sample";
        NSString *fileType = @"m4a";
        NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
        NSURL *soundURL = [NSURL fileURLWithPath:soundFilePath];
        //soundPalyer is Static so there is one for all of instance objects of this class.
        if (!soundPlayer) {

            soundPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error:&error];

        if (soundPlayer) {
            [self updateViewForPlayerState:soundPlayer];
            [self updateViewForPlayerInfo:soundPlayer];
            soundPlayer.numberOfLoops = 0;
            soundPlayer.delegate = self;
            updateTimer = nil;
        }
    }

}

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    //if you pause soundPlayer and curl page you will notice soundPlayer won't update so
    //I update sound player
    [self updateViewForPlayerState:soundPlayer];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [self updateViewForPlayerState:soundPlayer];
    [super viewWillDisappear:animated];

}

#pragma mark - my custom functions
- (void)customizeAppearance
{
    //change slider appearacne
    UIImage *minImage = [[UIImage imageNamed:@"sliderMinTrack"]
                         resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
    UIImage *maxImage = [[UIImage imageNamed:@"sliderMaxTrack"]
                         resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 9)];
    UIImage *thumbImage = [UIImage imageNamed:@"sliderThumb"];

    [progressBar setMaximumTrackImage:maxImage
                             forState:UIControlStateNormal];
    [progressBar setMinimumTrackImage:minImage
                             forState:UIControlStateNormal];
    [[UISlider appearance] setThumbImage:thumbImage
                                forState:UIControlStateNormal];
    [[UISlider appearance] setThumbImage:thumbImage
                                forState:UIControlStateHighlighted];


    [playButton setImage:playBtnBG forState:UIControlStateNormal];


}

- (void)pausePlaybackForPlayer:(AVAudioPlayer *)player
{
    [player pause];
    [self updateViewForPlayerState:player];


}

- (void)startPlaybackForPlayer:(AVAudioPlayer *)player
{
    //I must add paused = false here, else if I stop playing and bring front the app from background, sound player continue playing.
    paused = false;
    player.enableRate = YES;
    [player prepareToPlay];
    [player play];
    [self updateViewForPlayerState:player];
}
- (IBAction)playButtonPressed:(UIButton *)sender
{

    if (soundPlayer.playing == YES) {
        [self pausePlaybackForPlayer:soundPlayer];
        //I use this flag to control when the audio should be played
        paused = true;
    } else {
        [self startPlaybackForPlayer:soundPlayer];
    }

}

- (IBAction)progressSliderMoved:(UISlider *)sender
{
    soundPlayer.currentTime = sender.value;
    progressBar.maximumValue = soundPlayer.duration;

    [self updateViewForPlayerState:soundPlayer];
}

- (void)stopSoundPlayer
{
    if (self.mailPickerIsPresented) {
        if ([soundPlayer isPlaying]) {
            self.mailPickerhasPausedPlayback = true;
            [self pausePlaybackForPlayer:soundPlayer];
        }
    }
    else
    {
        [soundPlayer stop];
        soundPlayer = nil;
    }
}

- (void)updateViewForPlayerState:(AVAudioPlayer *)player
{
    if (updateTimer)
        [updateTimer invalidate];

    if (player.playing) {

        if(pauseBtnBG)
        {
            [playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
        }

        updateTimer = [NSTimer scheduledTimerWithTimeInterval:.01 target:self selector:@selector(updateCurrentTime) userInfo:player repeats:YES];
    } else {

        [playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
        //I called this function below to set currentTimeLabel=0 after finishig playing
        //[self updateCurrentTimeForPlayer:soundPlayer];

        updateTimer = nil;
    }
}

- (void)updateViewForPlayerStateInBackground:(AVAudioPlayer *)player
{
    [self updateCurrentTimeForPlayer:player];

    if (player.playing)
    {
        [playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
    }
    else
    {
        [playButton setImage:((player.playing == YES) ? pauseBtnBG : playBtnBG) forState:UIControlStateNormal];
    }
}

-(void)updateViewForPlayerInfo:(AVAudioPlayer*)player
{
    duration.text = [NSString stringWithFormat:@"%d:%02d",(int)player.duration / 60, (int)player.duration % 60, nil];
    progressBar.maximumValue = player.duration;
}

- (void)updateCurrentTime
{
    [self updateCurrentTimeForPlayer:soundPlayer];
}

- (void)updateCurrentTimeForPlayer:(AVAudioPlayer *)player
{
    currentTime.text = [NSString stringWithFormat:@"%d:%02d", (int)soundPlayer.currentTime / 60, (int)soundPlayer.currentTime % 60, nil];
    progressBar.value = player.currentTime;}

#pragma mark background notifications
- (void)registerForBackgroundNotifications
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(setInBackgroundFlag)
                                                 name:UIApplicationWillResignActiveNotification
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(clearInBackgroundFlag)
                                                 name:UIApplicationWillEnterForegroundNotification
                                               object:nil];
}

- (void)setInBackgroundFlag
{
    inBackground = true;
    [self pausePlaybackForPlayer:soundPlayer];

}

- (void)clearInBackgroundFlag
{

    inBackground = false;
    //we are checking if palyer was playing, if so, we continue its playing.
    if (paused == false) {
        [self startPlaybackForPlayer:soundPlayer];
    }
}

#pragma mark AVAudioPlayer delegate methods

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{if (flag == NO)
    NSLog(@"Playback finished unsuccessfully");

    [player setCurrentTime:0.0];
    if (inBackground)
    {
        [self updateViewForPlayerStateInBackground:player];
    }
    else
    {
        [self updateViewForPlayerState:player];
    }
}


// we will only get these notifications if playback was interrupted
- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)p
{
    NSLog(@"Interruption begin. Updating UI for new state");
    // the object has already been paused,  we just need to update UI
    if (inBackground)
    {
        [self updateViewForPlayerStateInBackground:p];
    }
    else
    {
        [self updateViewForPlayerState:p];
    }
}

- (void)audioPlayerEndInterruption:(AVAudioPlayer *)p
{
    NSLog(@"Interruption ended. Resuming playback");
    [self startPlaybackForPlayer:p];
}


@end

I have uploaded a sample app

https://www.dropbox.com/s/9fxqsly62euhr8p/pageBasedApp.zip

Upvotes: 0

Views: 133

Answers (2)

ldindu
ldindu

Reputation: 4380

I would recommend you to create Singleton to manage your AVAudioPlayer as follows

Interface

// AudioManager.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface AudioManager : NSObject

@property (nonatomic, strong) AVAudioPlayer *soundPlayer;

+ (id)sharedManager;

@end

// AudioManager.m
#import "AudioManager.h"
#define kSoundFileName @"sample.m4a"

Implementation

@implementation AudioManager

+ (id)sharedManager
{
    static AudioManager *sharedMyManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedMyManager = [[self alloc] init];
    });
    return sharedMyManager;
}

- (AVAudioPlayer *)soundPlayer
{
     NSError *error;
    if (_soundPlayer == nil)
    {
        _soundPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[self soundURL] error:&error];
    }
    return _soundPlayer;
}

- (NSURL *)soundURL
{
    NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"bgMusic" ofType:@"mp3"];
    return [NSURL fileURLWithPath:soundFilePath];
}

@end

in other view controllers, you can refer to AVAudioPlayer property on singleton object where only single AudioManager object will be created as follows

 #import "AudioManager.h"

 [[[AudioManager sharedManager] soundPlayer] play];

Upvotes: 1

Duncan C
Duncan C

Reputation: 131481

Short answer: Don'e use static variables. You don't understand how they work, and are mis-using them. You should remove the "static" qualifier on your sound player.

Upvotes: 0

Related Questions