Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AVAudioPlayer and Random Slowness

This question may be too vague to pass StackOverflow's standards, but I have to try posting it because I am running out of options... :/

Long story short: I have an app that experiences random periods of slowness. It does not happen too often (probably once every month), but when it does only a complete reboot of the iDevice it runs on helps. Symptoms are: 2-3 second response times and sluggish, choppy animations; the whole app basically becomes unusable.

I had ran the app through every possible diagnostics tool, none of which found anything wrong; no memory leaks or unusually high CPU usage. But, this is not surprising, considering the app is extremely simple, a tracker app for a card game.

All this led me to believe that the AVAudioPlayer I use to play sounds when the user taps a button might be the cause of the problem (it is the only, relatively high complexity element in the entire app). I am, however, not sure, which is where I need help. I include a sample code here, and perhaps someone with experience in iOS audio playback could look at it and see if there is some mistake I overlooked.

Here we go: First, I initialize a "silent player" that keeps playing a silent track every second to keep AVAudioPlayer alive. It is necessary because of the relatively long response time AVAudioPlayer experiences when being called after a longer period of inactivity.

NSString *silenceFilePath = [[NSBundle mainBundle] pathForResource: @"silence" ofType: @"wav"];
NSURL *silenceFileURL = [[NSURL alloc] initFileURLWithPath: silenceFilePath];
silencePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL: silenceFileURL error: nil];
[silencePlayer setDelegate: self];
[silencePlayer prepareToPlay];

silenceTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
                                                target:self
                                              selector:@selector(repeatSilence)
                                              userInfo:nil
                                               repeats:YES];

The NSTimer calls the following method:

- (void) repeatSilence
{
    if (isAudioON == YES)
    {
        if (silencePlayer.playing)
        {
            [silencePlayer stop];
            silencePlayer.currentTime = 0;
            [silencePlayer play];
        }
        else
        {
            [silencePlayer play];
        }
    }
}

The rest is fairly straightforward. I initiate another AVAudioPlayer to play a specific button sound (there are two of these):

NSString *buttonSoundFilePath = [[NSBundle mainBundle] pathForResource: @"button_pushed" ofType: @"wav"];
NSURL *buttonFileURL = [[NSURL alloc] initFileURLWithPath: buttonSoundFilePath];
buttonPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL: buttonFileURL error: nil];
[buttonPlayer setDelegate: self];
[buttonPlayer prepareToPlay];

And when the button is pushed I play the sound (the same way as I did with the silent player):

if (isAudioON == YES)
{
    if (buttonPlayer.playing)
    {
        [buttonPlayer stop];
         buttonPlayer.currentTime = 0;
        [buttonPlayer play];
    }
    else
    {
        [buttonPlayer play];
    }
}

And really all there is to it. I am afraid, however, that, despite the simple nature of this method, somehow this continuous audio playback creates a rare instance where iOS just goes crazy. But all this is just a theory, and that is why I need someone with more experience to take a look at my code and share his opinion.

Thanks!

Update: I found another few lines of code that might also be relevant to the problem. With them, I set the AVAudioPlayer to work simultaneously with music playing in the background (from another app for instance):

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[AVAudioSession sharedInstance] setDelegate:self];
like image 959
Gergely Kovacs Avatar asked Apr 27 '13 21:04

Gergely Kovacs


2 Answers

From what I gather from your question, it seems that the only sounds you want to be playing are relatively simple and short sound effects. If this is in fact the case, I would not use the AVAudioPlayer, as it is a resource heavy library for something as simple as playing sound effects at a fixed volume. I would use Audio Services directly. Here is an example of a Sound Effect Class that I pulled from somewhere else (can't remember where anymore).

SoundEffect.h

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

@interface SoundEffect : NSObject
{
    SystemSoundID soundID;
}

- (id)initWithSoundNamed:(NSString *)filename;
- (void)play;

@end

SoundEffect.m

#import "SoundEffect.h"

@implementation SoundEffect

- (id)initWithSoundNamed:(NSString *)filename
{
    if ((self = [super init]))
    {
        NSURL *fileURL = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
        if (fileURL != nil)
        {
            SystemSoundID theSoundID;
            OSStatus error = AudioServicesCreateSystemSoundID((__bridge CFURLRef)fileURL, &theSoundID);
            if (error == kAudioServicesNoError)
                soundID = theSoundID;
        }
    }
    return self;
}

- (void)dealloc
{
    AudioServicesDisposeSystemSoundID(soundID);
}

- (void)play
{
    AudioServicesPlaySystemSound(soundID);
}

@end

Once you have this class, you can easily play any sound effect by using the following code:

SoundEffect *mySoundEffect = [[SoundEffect alloc] initWithSoundNamed:@"mySound.wav"];
[shareSoundEffect play];

For optimization, I would load all of your sound effects somewhere and store them statically, so that the actual sound files are preloaded, then just call "play" on them whenever you need them. Doing this is a MUCH less resource heavy way of playing sound effects than using the AVAudioPlayer. Who knows, maybe it will solve your problem.

Preloading the sound file:

What I meant by this is simpler than it sounds. I just mean that you will create a static reference to each Sound Effect object after initializing them when the program first loads. There are several ways to do this. I typically use the +load method. It would look something like this:

Add a static variable in the implementation of the SoundEffect class

static SoundEffect *mySoundEffect;

Next, initialize that variable in the +load method for the Sound Effect class.

+(void)load
{
    mySoundEffect = [[SoundEffect alloc] initWithSoundNamed:@"mySound.wav"];
}

Now just create a static method for the sound effect class like this:

+(void)playMySound
{
    [mySoundEffect play];
}

After that, you can just play that sound at any time by calling:

[SoundEffect playMySound];

A note about volume controls:

I use this code in several of my IOS apps, and I can confirm the following:

  1. The standard volume controls on the side of the device will work normally, but only if the user's settings specify that ringers and alerts can be changed with buttons.
  2. There is no way to control the volume of the sound in your code.
like image 130
csundman Avatar answered Sep 30 '22 07:09

csundman


What I observe from the code above, I would suggest two changes.

1) You have used two instances of AVAudioPlayer, one for playing silent sound continuously every second. A second instance of AVAudioPlayer is for playing the sound when the button is clicked. Running two instances of AVAudioPlayer at the same time, as at some point of time when memory is low on the device and the silent sound and button sound needs to be played at the same time the device will act sluggish. It's better to use one AVAudioPlayer instance to play both sounds.

2) Keep playing the silent sound until the user clicks the button. When user clicks on the button, pause the timer and stop the current sound and play the button sound using the same AVAudioPlayer instance. This way the timer that is not a recommend mostly will be stoped for a while when the button sound needs to be played, when the button sound is finished playing resume the timer to play silent sound with same the AVAudioPlayer instance. This will take away a lot of load from the memory and processing.

like image 27
Sharon Nathaniel Avatar answered Sep 30 '22 05:09

Sharon Nathaniel