!sretsieM oiduA SOi, sgniteerG
In
our last post
, we looked at how to access iPod Library tracks and stream them from disk in real time, using Apple’s Extended Audio File Services and Audio Unit APIs. We also speculated about the possibility of modifying our code to allow for reverse playback of those streams – you know, so you can hear what stuff sounds like when you play it backwards.
These days, of course, reversing an audio file – or some section of it – is pretty much a snap: just open the file in an audio editor (
Audacity
is very capable – and free), apply whatever reverse effect is available to flip the samples around, and save the result as a new file. Online services that perform this service for you are quite common: you upload a file, wait a little bit, and back it comes, nice and backwards.
But that’s not
playing
a file in reverse; that’s playing a previously reversed file in the usual (forward) manner and its not what we want, No. What we want is to
stream audio files from disk
, in real time, and reverse direction
as playback is occurring
. In other words: we want the ability to read through the file, from disk, in reverse.
Now, technically that’s not exactly practical: most audio file-reading APIs (Extended Audio File Services, included) don’t read samples from a file one-by-one – that would be far too inefficient. Instead, they read them in
blocks
of (for example) 1024 samples, then they process the whole block in one go before shipping it out to the hardware. And the samples in those blocks are usually acquired using the same strategy: you open the file, set the ‘playhead’ to a specific location and grab the next block of samples from that point
forward
.
So: you can’t just ‘go the other way.’
This makes reading an audio file in reverse something of a challenge, but it also suggests a solution: if we could read our forwardly-arranged sample blocks in reverse
order
, and at the same time reverse the order of sample frames
within
each block, we might be able to achieve on-the-fly reverse playback. Let’s have a look at how we might do that.
The Demo Project
If you’d like to follow along, download the
starter demo project
and open it up. (Alternatively, a completed version of the demo can be gotten
here
, on Github.)
This is essentially the same player we set up last time around, but with a few modifications:
Swift:
This time we’re using Swift as our project language (but note we’ve retained the use of Objective-C for our reading and rendering code, for reasons we touched on
here
.)
FileReader Class:
we’ve moved our file reading methods (
openFileAtURL:
and
readFrames:audioBufferList:bufferSize:
) into their own
FileReader
class, where they belong;
we’ve refactored those methods by breaking them into smaller, more tightly-focused sub-methods; and
we’ve given our view controller the necessary property and method for creating a fresh reader object each time a new file is loaded from the iPod Library;
Bridging Header:
We’ve added a bridging header our project, in order to make our Objective-C classes available to Swift.
Scrubbing:
we’ve added
a
UISlider
for scrubbing through the audio file.
a
seekToFrame:
method to
FileReader
(so it can perform the scrubbing);
an
IBAction
to our view controller (so our slider can talk to
FileReader
); and
a
FileReaderDelegate
protocol (in
FileReader
) to which our view controller conforms, so it can be notified of changes to the current playback location and update the slider control accordingly.)
Continuous Looping of the File:
We’ve set things up so when playback reaches the end of the file, it jumps back to the beginning and starts all over again.
UI:
Finally, we’ve gussied up the UI (well, just a smidgen) with:
the aforementioned UISlider for scrubbing through the audio file;
a UISegmentedControl for toggling playback direction;
a pair of labels for displaying title/artist information for the currently loaded track;
a handful of descriptive labels for each of the above.
a little splash of color for some added stimulation!
Like so:
Run the project (on a device, not the simulator) and you’ll see it behaves much as before: we can browse the device’s iPod Library, choose a file, play it and pause it. In addition, we can now scrub to random locations within the file, and when playback reaches the end of the file, it loops back to the beginning.
As we pointed out last time, if you check the app’s memory footprint in the Debug navigator you’ll find that it is in fact quite small, and for good reason: we’re streaming our audio file directly from disk rather than loading the entire thing into memory.
The Reverse segmented control doesn’t do anything, of course, since we haven’t implemented reverse playback yet – that’s what we’re here to talk about!
Reversing The Audio Stream
Like I said, we’re going to tackle this in two steps: first, by attempting to read our forwardly-arranged sample blocks in reverse
order
, and second, by attempting to reverse the order of samples within each block.
The first thing we need is a way to indicate what the current playback direction should be. Add a new property to
FileReader.h
:
@property (assign, nonatomic) BOOL reversePlayback;
Then, in
FileReader.m
, add the following to
initWithFileURL:
, below the existing
_isReading = NO;
line:
_reversePlayback = NO;
Next we need an
@IBAction
for setting the
reversePlayback
flag when the
UISegmentedControl
is toggled, so let's add an action method to
ViewController
:
@IBAction func reversePlayback(sender: UISegmentedControl)
if reader != nil {
if sender.selectedSegmentIndex == 0 {
reader.reversePlayback = true
} else {
reader.reversePlayback = false
Don't forget to wire this action up to the
UISegmentedControl
's "Value Changed" event in the Storyboard!
Finally, head down to the
readFrames:
method and replace the line
_frameIndex += frames;
with the following:
// Reverse Playback
if (self.reversePlayback) {
NSLog(@"Reversing playback");
for (int buff = 0; buff < audioBufferList->mNumberBuffers; buff++) {
Float32* revBuff = audioBufferList->mBuffers[buff].mData;
[self reverseContentsOfBuffer:revBuff numberOfFrames:frames];
_frameIndex -= frames; // decrement frame index
} else {
_frameIndex += frames; // increment frame index
This is actually fairly straightforward:
First we check if our
reversePlayback
flag is set. If its not -
i.e.
we're currently performing normal playback - the only thing we do is what we were doing before: we allow our buffers to continue unmolested on their journey back to the render callback, and we increment
_frameIndex
for the next read cycle.
If
reversePlayback
is
set, we loop through our incoming buffers (we're using stereo interleaved here, so there's only going to be one), set up a new pointer variable and pass that to something called
reverseContentsOfBuffer
. We'll write that method in a moment.
If, at this point, you were to comment out everything in that code block except for the
decrement
of
_frameIndex
, you'd get a nice demonstration of why merely reversing the index is only a partial solution: the sequence of reads does indeed move backwards through the file, but the sample frames in each read block remain arranged in forward sequential order. (If you decide to try this, be sure to first scrub to some location
other
than the very beginning of the file, before reversing playback.)
Let's take care of that now by adding the following method to
FileReader.m
:
- (Float32*)reverseContentsOfBuffer:(Float32*)audioBuffer numberOfFrames:(UInt32)frames
Float32* reversedBuffer = audioBuffer;
Float32 tmp;
int i = 0;
int j = frames - 1;
while (j > i) {
tmp = reversedBuffer[j];
reversedBuffer[j] = reversedBuffer[i];
reversedBuffer[i] = tmp;
return reversedBuffer;
So much power in so few lines of code:
First, we're passing in a pointer to our buffer - the one we declared a few lines back - along with the
size
of that buffer. Remember, this pointer is really pointing to our render callback's outgoing buffer(s), which
ExtAudioFileRead
has just filled with fresh audio samples from the file on disk.
Next, we set up a pair of
int
counter variables, one pointing to the first element in the buffer and another pointing to the last element.
We also set up a third variable,
tmp
, to help us play Three Card Monte with the buffer's samples.
Finally, we set up a
while
loop to perform some magic: on each iteration of the loop, the counters
i
and
j
increment in opposite directions -
i
going up and
j
going down. When they meet in the middle -
j > i
- all the samples in the buffer will have been shuffled into reverse order - which is how the render callback will now ship them out to the hardware.
The last thing we need to do is modify
checkCurrentFrameAgainstLoopMarkers
- which currently handles looping around when playback reaches the end of the file - to perform a similar check when playback is reversed and we reach the
beginning
of the file:
- (void)checkCurrentFrameAgainstLoopMarkers:(SInt64)currentFrame inFrames:(UInt32)frames
if (self.reversePlayback) {
// If we're approaching startOfFile and have fewer than a buffer's worth of samples to go, reduce the size of the read
if (currentFrame - (SInt64)frames < self.lMarker) {
frames = (UInt32)currentFrame;
// If we're *exactly* at startOfFile, seek to (endOfFile - frames)
if (currentFrame - (SInt64)frames == self.lMarker) {
[self seekToFrame:self.rMarker - frames];
} else { // forward
// If we've reached endOfFile, seek to startOfFile
if (currentFrame > self.rMarker) {
[self seekToFrame:self.lMarker];
Try running the app, but with the following caveat: depending on the file you've loaded for playback - whether its compressed or not and, if so, how it was encoded and at what bit rate - you may (or may not) hear disappointing results when you reverse playback.
This means we have one more challenge to overcome.
Dealing With Compressed Formats
Reversing playback on the fly like this works great for PCM formats where each sample frame consists of a single set of samples, spread across all channels at that point in time. It even works well for robust compressed formats like AAC and well-encoded, high bit rate MP3s.
But compressed formats present an inherent challenge: unlike uncompressed formats, the frames of a compressed file consist of
blocks of encoded samples
plus a header (at the beginning of each block) indicating how the samples in that particular block were encoded - information a decoder needs in order to decode!
So what happens ordinarily, when you're playing a compressed file and the next read lands in the
middle
of a block, without the header? The MP3 codec has a mechanism for retaining the header and samples of incomplete blocks, so they can be reunited with the rest of the block on the next read. Clever!
But you can see where this is going: read those sample blocks in reverse order and you've clearly subverted the entire mechanism! The result: buffers coming back from
ExtAudioFileRead
with entire swaths of their samples set to zero. Nasty!
A Solution
What to do? One strategy is to seek to a location in the file
prior
to the data you're actually interested in, read in a
greater number of samples
(in the hopes of capturing the complete block of target samples
including
the header) and, after conversion, simply drop the extra frames.
This actually works surprisingly well, primarily because
ExtAudioFileRead
is optimized to be
fast
. Still, given the nature of compressed formats, it involves something of a gamble, which is: how can you be sure you've gone back far enough in the file to get the header data you need for the frames you actually want to read?
You can't. But by reading a fairly conservative number of extra frames, we can get pretty far with this idea. I'm only going to focus on detecting MP3 files because, honestly, I haven't had much trouble reversing other compressed formats.
Begin by adding a
define
macro to the top of
FileReader.m
#define kExpandedBufferSize 4096
Typically, reading is done in blocks of 512 or 1024 sample frames so 4096 makes for a nice, comfortable multiple. If it seems excessive to be grabbing 4-8x as many samples as we would normally need, keep in mind that not only is
ExtAudioFileRead
very fast, but the way we're going to 'crop' our buffer is by simply reassigning its pointer.
Next, there should be a flag we can set when we're dealing with an .MP3 file. Add another property to
FileReader.m
:
@property (assign, nonatomic) BOOL isMP3;
And add this line to
initWithFileURL
, so we always begin life with
isMP3
turned OFF:
_isMP3 = NO;
For this to be useful, we need some way of ascertaining if the current file is an MP3 or not. Add the following method, which decodes the file's formatID flag (and then tests whether that flag indicates an MP3) to the bottom of
FileReader.m
:
- (BOOL)fileIsMP3:(UInt32)formatID
char formatIDString[5];
UInt32 ID = CFSwapInt32HostToBig (formatID);
bcopy (&ID, formatIDString, 4);
formatIDString[4] = '\0';
// NSLog (@"Format ID: %10s", formatIDString);
NSString* fileExtension = [NSString stringWithCString:formatIDString encoding:NSUTF8StringEncoding];
if ([fileExtension isEqual: @".mp3"]) {
return YES;
return NO;
Now, in
openFileAtURL
we can check (
after
we've retrieved the file's native format, of course) if we've got an MP3 and, if we have, set the flag accordingly::
self.isMP3 = [self fileIsMP3:_fileFormat.mFormatID] ? YES : NO;
With our flag and helper method in place, we can now add the necessary conditional code for reversing an MP3 file stream. Start by modifying
createFileReadingBufferList
as follows:
- (void)createFileReadingBufferList
// AudioBufferList defines a single buffer, so we need to allocate additional buffers ourselves
fileReadingBufferList = (AudioBufferList*)malloc(sizeof(AudioBufferList) + (sizeof(AudioBuffer) * (_clientFormat.mChannelsPerFrame)));
fileReadingBufferList->mNumberBuffers = _clientFormat.mChannelsPerFrame;
for ( int i=0; i < fileReadingBufferList->mNumberBuffers; i++ ) {
fileReadingBufferList->mBuffers[i].mNumberChannels = 1;
UInt32 bufferSize;
if (self.isMP3 == YES) {
bufferSize = kExpandedBufferSize; // for reversed MP3 files
} else {
bufferSize = 1024;
fileReadingBufferList->mBuffers[i].mDataByteSize = bufferSize * sizeof(float);
fileReadingBufferList->mBuffers[i].mData = malloc(bufferSize * sizeof(float));
This sets a larger buffer size (using our
define
macro) for reading in the extra frames. Next, add the following to
readFrames:
, above the code that currently handles reversing:
UInt32 framesToRead;
if (self.isMP3 && self.playbackIsReversed) {
// Larger reads for reversed MP3s
framesToRead = kExpandedBufferSize;
CheckError(ExtAudioFileSeek(_audioFile, _frameIndex - (framesToRead - frames)), "MP3 ExtAudioFileSeek FAILED");
CheckError(ExtAudioFileRead(_audioFile, &framesToRead, fileReadingBufferList),
"Failed to read audio data from audio file");
for (int buff = 0; buff < fileReadingBufferList->mNumberBuffers; buff++) {
Float32* croppedBuffer = fileReadingBufferList->mBuffers[buff].mData;
croppedBuffer = &croppedBuffer[kExpandedBufferSize - frames];
audioBufferList->mBuffers[buff].mData = croppedBuffer;
} else {
// Normal reads otherwise
framesToRead = frames;
CheckError(ExtAudioFileSeek(_audioFile, _frameIndex), "Non-MP3 ExtAudioFileSeek FAILED");
CheckError(ExtAudioFileRead(_audioFile, &framesToRead, audioBufferList),
"Failed to read audio data from audio file");
All we're doing here is checking if we're currently reversing playback and, if we are, is the file we're reversing is an MP3? If we are (and it is), we (a) set our read size (
framesToRead
) to the
kExpandedBufferSize
macro we set up earlier; (b) have
ExtAudioFileRead
read
framesToRead
number of sample frames to a new float buffer; and (c) 'crop' the returned (and filled) buffer by resetting its pointer (which normally points to the first element in the buffer) to point instead to
kExpandedBufferSize
minus
frames
- which gives us the sample frames we were looking for in the first place. Thanks to the expanded read size, however,
ExtAudioFileRead
will have been able to completely fill those frames with properly decoded samples!
With that, we should give it a test run...
check it out
...
And there you have it: On-the-fly reversible playback, streamed right out of your iPod Library!
Core Audio
iOS Development
Post navigation