Skip to content

Instantly share code, notes, and snippets.

@rweichler
Created January 9, 2014 20:16
Show Gist options
  • Save rweichler/8341169 to your computer and use it in GitHub Desktop.
Save rweichler/8341169 to your computer and use it in GitHub Desktop.
// Copyright (c) 2012 Alex Wiltschko
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#import <Foundation/Foundation.h>
//#import "RingBuffer.h"
#import "Novocaine.h"
@class AudioFileReader;
@protocol AudioFileReaderDelegate<NSObject>
@optional
-(void)audioFileReaderWillReachEndOfFile:(AudioFileReader *)audioFileReader;
-(void)audioFileReaderReachedEndOfFile:(AudioFileReader *)audioFileReader;
@end
@interface AudioFileReader : NSObject
// ----- Read-write ------
@property (nonatomic, assign, getter=getCurrentTime, setter=setCurrentTime:) float currentTime;
@property (nonatomic, copy) NovocaineInputBlock readerBlock;
@property (nonatomic, assign) float latency;
@property (nonatomic, assign) NSObject<AudioFileReaderDelegate> *delegate;
@property (nonatomic, assign) float minNotify;
// ----- Read-only ------
@property (nonatomic, strong) NSURL *audioFileURL;
@property (nonatomic, assign, readonly, getter=getDuration) float duration;
@property (nonatomic, assign, readonly) float samplingRate;
@property (nonatomic, assign, readonly) UInt32 numChannels;
@property (nonatomic, assign, readonly) BOOL playing;
- (id)initWithAudioFileURL:(NSURL *)urlToAudioFile samplingRate:(float)thisSamplingRate numChannels:(UInt32)thisNumChannels;
// You use this method to grab audio if you have your own callback.
// The buffer'll fill at the speed the audio is normally being played.
- (void)retrieveFreshAudio:(float *)buffer numFrames:(UInt32)thisNumFrames numChannels:(UInt32)thisNumChannels;
//Use this to reopen the file (if you're streaming)
-(BOOL)refreshFile;
-(void)clearBuffer;
- (BOOL)prepareToPlay;
- (void)play;
- (void)pause;
- (void)stop;
@end
//
// AudioFileReader.m
// Novocaine
//
// Copyright (c) 2012 Alex Wiltschko
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#import "AudioFileReader.h"
#import "RingBuffer.h"
#define OUR_QUEUE dispatch_queue_create("AudioFileReader", NULL)
@interface AudioFileReader ()
{
RingBuffer *ringBuffer;
SInt64 _frameOffset;
SInt64 _frameOffsetForLastNotify;
SInt64 _frameOffsetForLastFileEnd;
BOOL _prepared;
}
// redeclaration as readwrite in class continuation
//@property (nonatomic, copy, readwrite) NSURL *audioFileURL;
@property (nonatomic, assign, readwrite, getter=getDuration) float duration;
@property (nonatomic, assign, readwrite) float samplingRate;
@property (nonatomic, assign, readwrite) UInt32 numChannels;
@property (nonatomic, assign, readwrite) BOOL playing;
@property (nonatomic, assign) AudioStreamBasicDescription outputFormat;
@property (nonatomic, assign) ExtAudioFileRef inputFile;
@property (nonatomic, assign) UInt32 outputBufferSize;
@property (nonatomic, assign) float *outputBuffer;
@property (nonatomic, assign) float *holdingBuffer;
@property (nonatomic, assign) UInt32 numSamplesReadPerPacket;
@property (nonatomic, assign) UInt32 desiredPrebufferedSamples;
@property (nonatomic, assign) dispatch_source_t callbackTimer;
@property (nonatomic, assign, readonly) float currentFileTime;
- (void)bufferNewAudio;
@end
@implementation AudioFileReader
static dispatch_queue_t our_queue = NULL;
- (void)dealloc
{
// If the dispatch timer is active, close it off
if (self.playing)
[self pause];
self.readerBlock = nil;
// Close the ExtAudioFile
ExtAudioFileDispose(self.inputFile);
free(self.outputBuffer);
free(self.holdingBuffer);
delete ringBuffer;
}
- (id)initWithAudioFileURL:(NSURL *)urlToAudioFile samplingRate:(float)thisSamplingRate numChannels:(UInt32)thisNumChannels
{
self = [super init];
if (self)
{
if(our_queue == NULL) our_queue = OUR_QUEUE;
self.audioFileURL = urlToAudioFile;
// Zero-out our timer, so we know we're not using our callback yet
self.callbackTimer = nil;
// Set a few defaults and presets
self.samplingRate = thisSamplingRate;
self.numChannels = thisNumChannels;
self.latency = .011609977; // 512 samples / ( 44100 samples / sec ) default
// We're going to impose a format upon the input file
// Single-channel float does the trick.
_outputFormat.mSampleRate = self.samplingRate;
_outputFormat.mFormatID = kAudioFormatLinearPCM;
_outputFormat.mFormatFlags = kAudioFormatFlagIsFloat;
_outputFormat.mBytesPerPacket = 4*self.numChannels;
_outputFormat.mFramesPerPacket = 1;
_outputFormat.mBytesPerFrame = 4*self.numChannels;
_outputFormat.mChannelsPerFrame = self.numChannels;
_outputFormat.mBitsPerChannel = 32;
// Arbitrary buffer sizes that don't matter so much as long as they're "big enough"
self.outputBufferSize = 65536;
self.numSamplesReadPerPacket = 8192;
self.desiredPrebufferedSamples = self.numSamplesReadPerPacket*2;
self.outputBuffer = (float *)calloc(2*self.samplingRate, sizeof(float));
self.holdingBuffer = (float *)calloc(2*self.samplingRate, sizeof(float));
// Allocate a ring buffer (this is what's going to buffer our audio)
ringBuffer = new RingBuffer(self.outputBufferSize, self.numChannels);
}
return self;
}
-(UInt32)numChannels
{
return Novocaine.audioManager.numOutputChannels;
}
-(float)samplingRate
{
return Novocaine.audioManager.samplingRate;
}
-(BOOL)prepareToPlay
{
if(_prepared) return true;
if(![self refreshFile]) return false;
dispatch_async(our_queue, ^{
[self bufferNewAudio];
});
return true;
}
-(BOOL)openFile:(ExtAudioFileRef *)inputFile
{
// Open a reference to the audio file
CFURLRef audioFileRef = (__bridge CFURLRef)self.audioFileURL;
if(ExtAudioFileOpenURL(audioFileRef, inputFile) != noErr)
{
_prepared = false;
return false;
}
// Apply the format to our file
ExtAudioFileSetProperty(*inputFile, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), &_outputFormat);
_prepared = true;
return true;
}
-(BOOL)refreshFile
{
BOOL wasPrepared = _prepared;
ExtAudioFileRef inputFile;
if(![self openFile:&inputFile])
{
return false;
}
dispatch_async(our_queue, ^{
ExtAudioFileSeek(inputFile, _frameOffset);
ExtAudioFileRef old = self.inputFile;
self.inputFile = inputFile;
if(wasPrepared)
ExtAudioFileDispose(old);
});
return true;
}
- (void)clearBuffer
{
dispatch_async(our_queue, ^{
delete ringBuffer;
ringBuffer = new RingBuffer(self.outputBufferSize, self.numChannels);
});
}
- (void)bufferNewAudio
{
if (ringBuffer->NumUnreadFrames() > self.desiredPrebufferedSamples)
return;
memset(self.outputBuffer, 0, sizeof(float)*self.desiredPrebufferedSamples);
AudioBufferList incomingAudio;
incomingAudio.mNumberBuffers = 1;
incomingAudio.mBuffers[0].mNumberChannels = self.numChannels;
incomingAudio.mBuffers[0].mDataByteSize = self.outputBufferSize;
incomingAudio.mBuffers[0].mData = self.outputBuffer;
// Read the audio
UInt32 framesRead = self.numSamplesReadPerPacket;
ExtAudioFileRead(self.inputFile, &framesRead, &incomingAudio);
// Update where we are in the file
ExtAudioFileTell(self.inputFile, &_frameOffset);
// Add the new audio to the ring buffer
ringBuffer->AddNewInterleavedFloatData(self.outputBuffer, framesRead, self.numChannels);
if (framesRead == 0) {
// modified to allow for auto-stopping. //
// Need to change your output block to check for [fileReader playing] and nuke your fileReader if it is //
// not playing and not paused, on the next frame. Otherwise, the sound clip's final buffer is not played. //
// self.currentTime = 0.0f;
[self stop];
ringBuffer->Clear();
_frameOffsetForLastNotify = 0;
if(_frameOffset != _frameOffsetForLastFileEnd && [self.delegate respondsToSelector:@selector(audioFileReaderReachedEndOfFile:)])
{
_frameOffsetForLastFileEnd = _frameOffset;
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate audioFileReaderReachedEndOfFile:self];
});
}
}
if(self.duration - self.currentFileTime < self.minNotify && (_frameOffset - _frameOffsetForLastNotify)/self.samplingRate > self.minNotify && [self.delegate respondsToSelector:@selector(audioFileReaderWillReachEndOfFile:)])
{
_frameOffsetForLastNotify = _frameOffset;
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate audioFileReaderWillReachEndOfFile:self];
});
}
}
-(float)currentFileTime
{
return (float)_frameOffset / self.samplingRate;
}
- (float)getCurrentTime
{
return self.currentFileTime - ringBuffer->NumUnreadFrames()/self.samplingRate;
}
- (void)setCurrentTime:(float)thisCurrentTime
{
_frameOffset = thisCurrentTime*self.samplingRate;
dispatch_async(our_queue, ^{
ExtAudioFileSeek(self.inputFile, thisCurrentTime*self.samplingRate);
});
}
- (float)getDuration
{
// We're going to directly calculate the duration of the audio file (in seconds)
SInt64 framesInThisFile;
UInt32 propertySize = sizeof(framesInThisFile);
ExtAudioFileGetProperty(self.inputFile, kExtAudioFileProperty_FileLengthFrames, &propertySize, &framesInThisFile);
AudioStreamBasicDescription fileStreamFormat;
propertySize = sizeof(AudioStreamBasicDescription);
ExtAudioFileGetProperty(self.inputFile, kExtAudioFileProperty_FileDataFormat, &propertySize, &fileStreamFormat);
return (float)framesInThisFile/(float)fileStreamFormat.mSampleRate;
}
- (void)configureReaderCallback
{
if (!self.callbackTimer)
{
self.callbackTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, our_queue);
UInt32 numSamplesPerCallback = (UInt32)( self.latency * self.samplingRate );
dispatch_source_set_timer(self.callbackTimer, dispatch_walltime(NULL, 0), self.latency*NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.callbackTimer, ^{
if (self.playing) {
if (self.readerBlock) {
// Suck some audio down from our ring buffer
[self retrieveFreshAudio:self.holdingBuffer numFrames:numSamplesPerCallback numChannels:self.numChannels];
// Call out with the audio that we've got.
self.readerBlock(self.holdingBuffer, numSamplesPerCallback, self.numChannels);
}
// Asynchronously fill up the buffer (if it needs filling)
dispatch_async(our_queue, ^{
[self bufferNewAudio];
});
}
});
dispatch_resume(self.callbackTimer);
}
}
- (void)retrieveFreshAudio:(float *)buffer numFrames:(UInt32)thisNumFrames numChannels:(UInt32)thisNumChannels
{
ringBuffer->FetchInterleavedData(buffer, thisNumFrames, thisNumChannels);
}
- (void)play
{
// Configure (or if necessary, create and start) the timer for retrieving audio
if (!self.playing) {
[self configureReaderCallback];
self.playing = TRUE;
}
}
- (void)pause
{
// Pause the dispatch timer for retrieving the MP3 audio
self.playing = FALSE;
}
- (void)stop
{
// Release the dispatch timer because it holds a reference to this class instance
[self pause];
if (self.callbackTimer) {
dispatch_release(self.callbackTimer);
self.callbackTimer = nil;
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment