43

I'm trying to eliminate startup lag when playing a (very short -- less than 2 seconds) audio file via AVAudioPlayer on the iPhone.

First, the code:

NSString *audioFile = [NSString stringWithFormat:@"%@/%@.caf", [[NSBundle mainBundle] resourcePath], @"audiofile"];
NSData *audioData = [NSData dataWithContentsOfMappedFile:audioFile];

NSError *err;
AVAudioPlayer *audioPlayer = [(AVAudioPlayer*)[AVAudioPlayer alloc] initWithData:audioData error:&err];

audioPlayer.delegate = self;
[audioPlayer play];

I also implement the audioPlayerDidFinishPlaying method to release the AVAudioPlayer once I'm done.

The first time I play the audio the lag is palpable -- at least 2 seconds. However, after that the sound plays immediately. I suspect that the culprit, then, is the [NSData dataWithContentsOfMappedFile] taking a long time reading from the flash initially, but then being fast on later reads. I'm not sure how to test that, though.

Is that the case? If so, should I just pre-cache the NSData objects and be aggressive about clearing them in low memory conditions?

Pang
  • 9,564
  • 146
  • 81
  • 122
John Biesnecker
  • 3,782
  • 2
  • 34
  • 43
  • Thanks for the info. You should answer the question and accept your answer. Initializing the AVAudioPlayer on startup ensured the audio played without delay in the rest of the app. – brianegge Jun 13 '10 at 20:54
  • 1
    @johnbiesnecker play any sound with 0 volume, then all the sound play calls should be inside an async global queue block, with a default or high priority, sound should not be played in the main queue. – Juan Boero Jan 07 '16 at 05:35
  • 1
    In addition to what @JuanPabloBoero said, I also played the 0 volume sound when receiving `UIApplicationWillEnterForegroundNotification`, otherwise there's a delay after app is resumed from background. – Pang Jan 17 '16 at 09:29
  • @Pang wow, i missed that, very interesting point. – Juan Boero Jan 17 '16 at 15:09

9 Answers9

41

The delay seems to be related to instantiating AVAudioPlayer for the first time. If I load any audio, run [audioPlayer prepareToPlay] and then immediately release it, the load times for all of my other audio is very close to imperceptible. So now I'm doing that in applicationDidFinishLaunching and everything else runs well.

I can't find anything about this in the docs, but it certainly seems to be the case.

John Biesnecker
  • 3,782
  • 2
  • 34
  • 43
  • 1
    Hmm, I don't know about this. prepareToPlay didn't completely work for me. prepareToPlay sort of works, but since it appears to be loading the audio asynchronously, there is no way to know exactly when the audio is truly ready to play. See my recent answer for the solution I came up with. – Taylor Gautier Mar 15 '11 at 01:16
  • I get a delay with AVQueuePlayer as well whenever I first load an asset into it. The player only gets initialized once. Seems like a common problem: http://stackoverflow.com/questions/11171374/how-to-reduce-ios-avplayer-start-delay – Oren Sep 05 '14 at 22:10
  • It worker perfect for me, now my initializer runs a prepareToPlay() over one of the sounds and I don't have the delay anymore. It's not pretty, but it works. – Joel Dec 21 '20 at 16:06
12

Here's what I've done (in a separate thread):

[audioplayer start]
[audioplayer stop]
self.audioplayer = audioplayer

[audioplayer prepareToPlay] seems to be an asynchronous method, so you can't be sure when it returns if the audio is in fact ready to play.

In my case I call start to actually start playing - this appears to be synchronous. Then I stop it immediately. In the simulator anyway I don't hear any sound coming out from this activity. Now that the sound is "really" ready to play, I assign the local variable to a member variable so code outside the thread has access to it.

I must say I find it somewhat surprising that even on iOS 4 it takes some 2 seconds just to load an audio file that is only 4 seconds in length....

Taylor Gautier
  • 4,916
  • 7
  • 30
  • 24
8

If your audio is less than 30 seconds long in length and is in linear PCM or IMA4 format, and is packaged as a .caf, .wav, or .aiff you can use system sounds:

Import the AudioToolbox Framework

In your .h file create this variable:

SystemSoundID mySound;

In your .m file implement it in your init method:

-(id)init{
if (self) {
    //Get path of VICTORY.WAV <-- the sound file in your bundle
    NSString* soundPath = [[NSBundle mainBundle] pathForResource:@"VICTORY" ofType:@"WAV"];
    //If the file is in the bundle
    if (soundPath) {
        //Create a file URL with this path
        NSURL* soundURL = [NSURL fileURLWithPath:soundPath];

        //Register sound file located at that URL as a system sound
        OSStatus err = AudioServicesCreateSystemSoundID((CFURLRef)soundURL, &mySound);

            if (err != kAudioServicesNoError) {
                NSLog(@"Could not load %@, error code: %ld", soundURL, err);
            }
        }
    }
return self;
}

In your IBAction method you call the sound with this:

AudioServicesPlaySystemSound(mySound);

This works for me, plays the sound pretty damn close to when the button is pressed. Hope this helps you.

M Jesse
  • 2,213
  • 6
  • 31
  • 37
5

I've taken an alternative approach that works for me. I've seen this technique mentioned elsewhere (tho I don't recall at the moment where that was).

In short, preflight the sound system by loading and playing a short, "blank" sound before you do anything else. The code looks like this for a short mp3 file I preload in my view controller'sviewDidLoad method that's of .1 second duration:

   NSError* error = nil;
   NSString* soundfilePath = [[NSBundle mainBundle] pathForResource:@"point1sec" ofType:@"mp3"];
   NSURL* soundfileURL = [NSURL fileURLWithPath:soundfilePath];
   AVAudioPlayer* player = [[[AVAudioPlayer alloc] initWithContentsOfURL:soundfileURL error:&error] autorelease];
   [player play];  

You can create your own blank mp3 if you want, or do a google search on "blank mp3s" to find and download one already constructed by somebody else.

hkatz
  • 951
  • 11
  • 21
  • using this with NSZombies enabled reveals an "impl" message being sent to the player after dealloc. i got it to avoid this by not autoreleasing it, and instead manually sending it a [player release] message in the audioPlayerDidFinishPlaying:successfully: event (in callback) – unsynchronized Dec 21 '11 at 02:40
  • further to last comment, see my updated answer to this question for a source code solution: http://stackoverflow.com/a/7426406/830899 – unsynchronized Dec 21 '11 at 02:48
3

I wrote a simple wrapper for AVAudioPlayer that provides a prepareToPlay method that actually works:

https://github.com/nicklockwood/SoundManager

It basically just scans your app for sound files, picks one and plays it at zero volume. That initialises the audio hardware and allows the next sound to play instantly.

Nick Lockwood
  • 40,865
  • 11
  • 112
  • 103
3

Here's a simple Swift extension to AVAudioPlayer that uses the play-and-stop at 0 volume idea presented in previous answers. PrepareToPlay() unfortunately at least for me did not do the trick.

extension AVAudioPlayer {
    private func initDelaylessPlayback() {
        volume = 0
        play()
        stop()
        volume = 1
    }

    convenience init(contentsOfWithoutDelay : URL) throws {
        try self.init(contentsOf: contentsOfWithoutDelay, fileTypeHint: nil)
        initDelaylessPlayback()
    }
}
OwlOCR
  • 1,127
  • 11
  • 22
0

Other workaround is to create a short & silent audio and play it on the first time player is initiated.

  1. Set the microphone level to 0 on System Preferences.
  2. Open QuickTime.
  3. Create new Audio Recording (File > New Audio Recording).
  4. Record for 1 or 2 second.
  5. Save / Export the audio file. (it will have .m4a extension and about 2 kb on size).
  6. Add the file to your project and play it.

If you dont want to create a new sound file yourself, you may download a short & silent audio file here : https://www.dropbox.com/s/3u45x9v72ic70tk/silent.m4a?dl=0

Adi Nugroho
  • 860
  • 10
  • 12
-1

The answer is

AVAudioPlayer *audioplayer;
[audioplayer prepareToPlay];
Pang
  • 9,564
  • 146
  • 81
  • 122
Ben
  • 19
  • 2
  • 2
    Actually, [audioplayer prepareToPlay] isn't the answer... the delay I was talking about happens the first time you alloc and init an AVAudioPlayer object, before you call any of its methods. It only happens the very first time you load one, though. prepareToPlay just preloads the audio, but doesn't eliminate the lag I was actually asking about. Incidentally, UIWebView exhibits the same behavior -- if you alloc and then immediately release one, the rest load very quickly. – John Biesnecker Aug 12 '10 at 07:15
-1

I don't know for sure, but I suspect the NSData object is being lazy and loading the contents of the file on demand. You can try "cheating" by calling [audioData getBytes:someBuffer length:1]; at some early point to get it to load that file before it's needed.

n8gray
  • 4,939
  • 3
  • 36
  • 33