Created
January 9, 2014 20:16
-
-
Save rweichler/8341169 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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