Say Goodnight Software

iOS & More

Streaming Audio To the iPhone Starting At An Offset

| 13 Comments

Starting with Matt Gallagher’s awesome AudioStreamer class, I wanted to extend it in a couple ways:

  1. Be able to play my radio show .mp3s hosted on my website (static files, not streams)
  2. Be able to start playback at any point in the file (startWithOffsetInSecs)
  3. Obviously, have it work on the iPhone
  4. Be able to have full control of audio & drawing while audio is playing

Actually, #3 & #4 are really what pushed me to find Matt’s class in the first place…so they aren’t really extensions of AudioStreamer, but rather why I chose to use AudioStreamer in the first place. The other major option was using something like MPMoviePlayerController. But that had several limitations which I won’t get into here.

First off I just compiled AudioStreamer as it is packaged on Matt’s blog, and it didn’t work with my static files. However, once I upgraded my machine to the 3.0 SDK (it was an older dev machine), then I was able to play back the static files as a real stream, by simply changing the URL in the UI that Matt provides. Simple as pie. #1, checked off the list! Sweet! (Its never that simple though, is it?)

For now, I thought it was, so I moved on to figuring out how to play from an offset into the file. I was STUMPED. I asked around several places. A little FYI here, I highly recommend all mac/iphone developers take advantage of the Developer Forums on the Apple DevCenter, even while they are still in beta. Apparently quite a few internal Apple devs lurk around there, and are VERY helpful with tech questions. I cannot stress enough how useful they were to me.

However, they weren’t the ones that gave me the answer to the offset question. That came from the man himself, Matt Gallagher. He gave me this little gem:

CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Range"), CFSTR("bytes=1000000-"));

If you add that in right before you call CFReadStreamCreateForHTTPRequest(), the server will start sending bytes at that offset. As I found out, it turns out the ‘minus’ at the end of that statement? NOT A TYPO. :) It normally works as a range value, in this case we tell it just start at the byte offset and continue till the end.

I quickly hacked in that line, and sure enough it worked. GREAT! Check off # 2, right? DONE! Well….not quite. Now the fun part starts.

Issues:

  • How To Convert Seconds To Bytes For A Given MP3?

This is apparently more difficult than it seems. After some quick research I think I tracked down a decent formula, but it still relies on a bit of trial and error. (Programmatic trial and error, so thats ok, right?) First off, you need to know the bitrate that the mp3 was recorded in. For example, many radio stations (like mine) use 128kbps, or 128 kilobits per second. Doing a quick search I found that 1 kilobits per second == 125 bytes per second. So doing some quick math, you should be able to do bitrate * 125 * offsetInSeconds and get the exact byte value to give the server, right? Well….close. I found that I was always NEAR the right location, but never dead on balls accurate (its an industry term).

Here is where the trial and error starts. I played around with it, and found that if I offset by roughly 70000 bytes, I was accurate! So I thought that mp3s just had some header that offset by that much, and this was good enough. However, then I tried a different mp3, and it was wrong again! Turns out that each mp3 can have different offsets (varying by quite a bit actually), but the good news is that it seems consistant throughout the same file. And further AudioStreamer does gather both the bitrate AND the data offset for us! It is important to note regarding the data offset that the offset WE care about is the initial dataoffset for byte 0 of the file. If we take that value and remember it (along with the bitrate), we will have enough information to do our seconds to bytes calculation.

Unfortunately, the only way I could really figure out how to get this information was by opening the file with AudioStreamer and letting it read a few packets from the beginning of the file. Not the most efficient method, but it works. Basically, I ‘start’ using offset 0, wait till the streamer thread gets me the information I need, then ‘stop’, then do the calculation, then ‘start’ at the correct offset! Simple!?@#%$#

  • AudioQueue creation issue

This was a problem. Basically, AudioStreamer would throw a AS_AUDIO_QUEUE_CREATION_FAILED error, but only sometimes. I noticed that whenever it did get that error, the sample rate was always reported in AudioStreamer as something other than what it really was. Best I can figure is that choosing certain offsets to begin reading at just don’t work, cause they confuse the parser into thinking the sample rate is something other than what it really is. I fixed this with a HUUUUGE hack. Basically if I get this error, I eat the dialog box, and subtract 1000 bytes from the offset and try again. Shut up, it works.

  • Unstable on iPhone, fine in simulator.

Last issue, then I’ll post some code. This problem was an annoying one. I thought I had everything finished last night, and I went to try it on my iPhone versus using the simulator and it didn’t exactly work. First off, it only played for about 1 second. Secondly, sometimes it didn’t even play for one second. I tracked this down to the ‘inuse’ array that AudioStreamer uses to denote which buffers are currently in use. If you call ‘start’ then ‘stop’ (as I do, in order to get the bitrate and data offset), this inuse array can be left in a bad state. Sometimes, its fine (if your buffer index is away from the fake-used up buffers, you might be able to play for about a second! Then it will just block waiting for a buffer that will never become free, cause it was marked as inuse by a previous ‘start’. So, in my startWithOffsetInSecs function, I just reset the entire inuse array to false. This seemed to solve all of these thread deadlock issues.

So the code! Here is the main function I added:

- (void)startWithOffsetInSecs:(UInt32) offsetInSecs
{
	NSLog(@"AudioStreamer::startAt - starting at second %d", offsetInSecs);

	if ([self isPlaying])
		[self stop];

	if ( bitRate == 0 )
	{
		self.offsetStart = 0;
		[self start];
		while ( !self.bitRate ) {}
		[self stop];

		for (int i=0; i<kNumAQBufs;i++)
			inuse[i] = false;

		thisFileDataOffset = dataOffset;
	}

	NSLog(@"AudioStreamer::startAt - this files total offset %d", thisFileDataOffset);

	// 1 kilobit == 125 bytes
	self.offsetStart = ( (self.bitRate / 1000) * 125 * offsetInSecs ) + thisFileDataOffset;
	[self start];

	BOOL keepWaiting = YES;
	while (keepWaiting)
	{
		if (errorCode)
		{
			self.offsetStart -= 1000;
			err = 0;
			errorCode = 0;
			[self stop];
			[self start];
		}
		else
			keepWaiting = ![self isPlaying];
	}

}

So anyway, I am working on this for a project with my other website, hopfully that app will be finished soon! Here is a zip of Matt’s original example, with my extensions added, so you can play with it.

AudioStreamer_startWithOffsetInSecs.zip

13 Comments

  1. nice work!

  2. I am trying to adapt this to work with files on disk for a project. Like huge mp3′s hours long, with chaptermarks and stuff.
    I am trying to adapt your seeking with a stream idea to projects I have in NSData or with a file already on disk.
    Any suggestions?

    Been trying this for almost days now, and not really making good progress.

  3. What format are the mp3s in? They have chaptermarks? Are they m4a’s? I don’t really “seek” here. This code simply starts “streaming” the static file at an offset.

  4. yeah true.
    I took the mp4′s and make them mp3′s , saved the “seconds” location of chaptermarks in a different file.

    yes, you stream a file at an offset,and i think that is good enough for seeking in the way I am talking. I would just close the file, and re-open at a future byte offset. this would work faster than AVAUdioplayer I think.

    In any event, I am playing with your code, and if i modify it successfuly for a fileatPath URL instead of a HTTP url, I will let you know.

  5. I don’t follow what you are doing though…if you have local files, not located on a server someplace, why are you streaming at all? I havn’t had any experience with it, but won’t AVAudioPlayer do the trick? The only reason I am not using that class, is because my file is located on a webserver, so I can’t. You can still do the sync up with images and whatnot yourself, but use AVAudioPlayer for the audio playback. Or maybe I am missing something?

  6. Any thoughts on how to have multiple buttons down the view with different stream sources?
    I tried doing this myself but the second button only activates the first source playing.
    Hints tips or abuse are welcome. thanks!
    jdispensa/gmail.com

  7. Thanks for sharing this code, I’ve implemented it, and it seems to work pretty well. One thing you might want to do is set the seektime variable (that Matt uses) at the end of your function:

    seekTime = (self.offsetStart – thisFileDataOffset) / self.bitRate * 1000 / 125;

    so that the progress value is accurate.

    cheers

  8. right on….yeah i didn’t really have a need for progress at that point in my project so I had left it out. And my project ended up being in violation of the DMCA so its on hold for a while. On to other projects!

  9. Hi Thanks for the code,
    Has one query, can we use above code for playing other audio formats like .wav and aiff from an specified offset.
    I tried, but we can play only from “0″ offset, if we specify any offset other than zero then it throws an error.
    any help would be greatly appreciated.
    Thanks

  10. Hi Michel,
    Can you give me a quick update like does AVAUdioplayer support streaming?
    Br,
    Surajit

  11. Hi!
    Is anyone else experiencing this problem when trying to playback a mp3:

    2010-03-26 13:59:35.017 iPhoneStreamingPlayer[38846:4803] Error loading /Library/QuickTime/LiveType.component/Contents/MacOS/LiveType: dlopen(/Library/QuickTime/LiveType.component/Contents/MacOS/LiveType, 262): Symbol not found: _SCDynamicStoreCopyConsoleUser
    Referenced from: /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/LangAnalysis.framework/Versions/A/LangAnalysis
    Expected in: /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator2.0.sdk/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration

    /Br Johannes

  12. Hi Matt,
    Can you help me out in seeking in Your code. Some file i am observing its starta from the beginning from the file though i try to seek.

    Br,
    Surajit

  13. I have used your code but still there are some issues are coming in song seek..

    AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &offset);

    this method returns offset of starting position many times

    and this leads to play songs from starting position

    Please help

Leave a Reply

Required fields are marked *.