Home > Techniques > Prebuffering iPhone Audio Streams For Gapless Playback

Prebuffering iPhone Audio Streams For Gapless Playback

prebuff

I have been working on a project that requires me to stream audio from a webserver to the iPhone, starting at an offset in an mp3 file. I showed how to modify Matt Gallagers awesome AudioStreamer code in a previous post. What I find myself doing quite a bit is stopping play on one file and starting play on another. Obviously you want the gap between tracks to be as minimal as possible.

The easy way to do this would be just to keep track of duration in a ’switcher’ object, when the progress of the ‘A’ streamer reaches the duration, start playing the ‘B’ streamer. This can be managed in the switcher object. However, it isn’t quite optimal because the cost of starting play on a new streamer is paid after the first streamer has finished. What would be best is if you could get the ‘B’ streamer all ready to play, so as soon the ‘A’ streamer finished you could seamlessly start hearing the audio from the ‘B’ streamer.

Unfortunately, the iPhone hardware as it exists today only allows for one mp3 audio queue to be played or ‘primed’ at a time. The priming of the audio queue is what is desired here, as it will decompress the buffers getting them set to be output. (Apparently there are CPU concerns when it comes to decompression)

How do I play multiple sounds simultaneously?

Use the interfaces in Audio Queue Services (AudioToolbox/AudioQueue.h). Create one audio queue object for each sound that you want to play. Then specify simultaneous start times for the first audio buffer in each audio queue, using the AudioQueueEnqueueBufferWithParameters function.

The following limitations pertain for simultaneous sounds in iPhone OS, depending on the audio data format:

AAC, MP3, and ALAC (Apple Lossless) audio: no simultaneous playback; one sound at a time.

Linear PCM and IMA/ADPCM (IMA4 audio): You can play multiple linear PCM or IMA4 format sounds simultaneously without CPU resource concerns.

However, this is not all that happens in the AudioStreamer object. We also have to read the bytes from the network and get them in our array, ready to be processed by the audio queue. This work can be done while the other streamer is finishing, so that while playback won’t be immediate when switching streamers, it will be minimized.

So I will make a class called AudioStreamSwitcher that will wrap 2 AudioStreamers:

#import <Foundation/Foundation.h>

#import "AudioStreamer.h"

@interface AudioStreamSwitcher : NSObject
{
	AudioStreamer* streamerA;
	AudioStreamer* streamerB;
	AudioStreamer* currentStreamer;

	BOOL streamerAIsCurrent;

	UInt32 currentDuration;

	NSTimer *checkForEndTimer;

	BOOL sentCueNotification;

}

@property (readonly) double progress;
@property (readonly) UInt32 bitRate;

- (void) startForDurationInSecs:(UInt32) inDurationSecs;
- (void) startWithOffsetInSecs:(UInt32)offsetInSecs forDurationInSecs:(UInt32) inDurationSecs;
- (void) startPlayingURLWhenFinished:(NSURL*)inURL withOffsetInSecs:(UInt32) offsetInSecs forDurationInSecs:(UInt32) inDurationSecs;
- (void) stop;

- (void)checkForEnd:(NSTimer *)aNotification;

- (BOOL)isPlaying;
- (BOOL)isPaused;
- (BOOL)isWaiting;
- (BOOL)isIdle;

@end

Normal usage will be:
- start playing
- right before finish, send notification we are about to finish
- observer responds by calling startPlayingURLWhenFinished
- we get the opposite streamer ready to go as much as possible
- at finish switch and start playing the opposite streamer

Also in the code are some fixes for problems with my earlier post. I put in a workaround for calculating the byte offset based on an offset in seconds. You need the bitrate from the file, so rather than blocking on waiting for the bitrate to get set, I have the AudioStreamer tear down the ReadStream object once it has the bitrate based on reading the first packet from the file. Then it does the calculation to get the correct byte offset, and reopens the ReadStream at the right place. This is all done with one AudioFileStream.

Further, if any errors occur when attempting to create the AudioQueue, it is likely due to the offset putting us in a weird place in the file. I have found the best (and easiest) remedy is to just back up and try again. Rather than blocking on waiting to start play, I send a notification that an error occured, and the AudioStreamer is the only observer, and it knows to back up 1000 bytes and try again.

While this method is good…(fantastic on the simulator), it is only soso on the iPhone hardware. We always get the delay of waiting for starting of the second queue…unless we could feed one queue with 2 different files! Since we control the files, there is no reason to have a 1 to 1 relationship between files and queues. I started implementing this method, and I have no doubt it will work. However, you can’t change the format of an AudioQueue, so unless your files are all the same format it won’t work. My files might be different, so I gave up on this method.

Anyway, hope this discussion & code is helpful for some people. I definitely learned alot while hacking away at this code. I am sure I’ll make more changes as I go… thats the fun part!

Here is the code for the AudioStreamSwitcher example.

  1. undefined_v@yahoo.com
    August 18th, 2009 at 08:26 | #1

    thank you ,
    but how can i get the duration of the static mp3 file

  2. PJ Gray
    August 22nd, 2009 at 14:10 | #2

    Well I got the length/duration information using QTKit. See this blog entry for information. Basically, I wrote a utility app that grabbed all the information I needed from the mp3 and added it to an XML file which I access from the iPhone app.

  3. November 15th, 2009 at 11:44 | #3

    About the gap in between two songs: Have you considered the network / http request setup and the time it takes to get the first playable data. As I look through the code, I keep thinking it can be improved by starting the stream download 5 seconds before actual playback for example. So when you switch, it does indeed take some time to setup the audio stuff, but data is immediately available.

    Although currently it’s not possible to start a download before playing audio as I understand it?

  4. February 4th, 2010 at 03:22 | #4

    I just pushed a version of AudioStreamer that will accept multiple mp3 urls. This was mentioned in the post.
    http://github.com/ckhsponge/AudioStreamer

    I can still hear a small gap so there may be some padding in my files. The next url seems to start loading sufficiently early so I don’t think network time is the issue.

  1. No trackbacks yet.