Browse files

Implemented AEBlockScheduler and altered AEAudioTimingReceiver to als…

…o receive the frame count of the current interval
  • Loading branch information...
1 parent c173e52 commit b6b8ac2b524ecf76457c390ca65a52a43458d23c @michaeltyson michaeltyson committed Mar 22, 2013
View
8 TheAmazingAudioEngine.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 4C09450116FBD7460054608E /* AEBlockScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C0944FF16FBD7460054608E /* AEBlockScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 4C09450216FBD7460054608E /* AEBlockScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C09450016FBD7460054608E /* AEBlockScheduler.m */; };
4C215CEF1523A7D500D36CAD /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C215CEE1523A7D500D36CAD /* Foundation.framework */; };
4C215D081523A8E500D36CAD /* AEAudioController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAD56811516281D003CE861 /* AEAudioController.m */; };
4C215D091523A8E500D36CAD /* AEAudioFilePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAD56AA15163488003CE861 /* AEAudioFilePlayer.m */; };
@@ -38,6 +40,8 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
+ 4C0944FF16FBD7460054608E /* AEBlockScheduler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AEBlockScheduler.h; sourceTree = "<group>"; };
+ 4C09450016FBD7460054608E /* AEBlockScheduler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AEBlockScheduler.m; sourceTree = "<group>"; };
4C12CC98151D1EDA00562E2A /* AEUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AEUtilities.h; sourceTree = "<group>"; };
4C12CC99151D1EDA00562E2A /* AEUtilities.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = AEUtilities.c; sourceTree = "<group>"; };
4C215CEC1523A7D500D36CAD /* libTheAmazingAudioEngine.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTheAmazingAudioEngine.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -201,6 +205,8 @@
4C25747215F0D8E000D232E8 /* TPCircularBuffer.c */,
4C25747315F0D8E100D232E8 /* TPCircularBuffer.h */,
4CE501971493F82600F23607 /* TheAmazingAudioEngine-Prefix.pch */,
+ 4C0944FF16FBD7460054608E /* AEBlockScheduler.h */,
+ 4C09450016FBD7460054608E /* AEBlockScheduler.m */,
);
path = TheAmazingAudioEngine;
sourceTree = "<group>";
@@ -225,6 +231,7 @@
4C99588D16C0825F0011FB01 /* AEAudioUnitFilter.h in Headers */,
4C456B8D16D59365008ED99D /* AEBlockAudioReceiver.h in Headers */,
4C4B11F416833FDD00A3BA2E /* AEBlockChannel.h in Headers */,
+ 4C09450116FBD7460054608E /* AEBlockScheduler.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -326,6 +333,7 @@
4C99588616BB74720011FB01 /* AEAudioUnitChannel.m in Sources */,
4C99588E16C0825F0011FB01 /* AEAudioUnitFilter.m in Sources */,
4C456B8E16D59365008ED99D /* AEBlockAudioReceiver.m in Sources */,
+ 4C09450216FBD7460054608E /* AEBlockScheduler.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
2 TheAmazingAudioEngine/AEAudioController.h
@@ -342,11 +342,13 @@ typedef enum {
* @param receiver The receiver object
* @param audioController The Audio Controller
* @param time The time the audio was received (for input), or the time it will be played (for output), automatically compensated for hardware latency.
+ * @param frames The number of frames for the current block
* @param context The timing context - either input, or output
*/
typedef void (*AEAudioControllerTimingCallback) (id receiver,
AEAudioController *audioController,
const AudioTimeStamp *time,
+ UInt32 frames,
AEAudioTimingContext context);
/*!
View
4 TheAmazingAudioEngine/AEAudioController.m
@@ -575,7 +575,7 @@ static OSStatus inputAvailableCallback(void *inRefCon, AudioUnitRenderActionFlag
for ( int i=0; i<THIS->_timingCallbacks.count; i++ ) {
callback_t *callback = &THIS->_timingCallbacks.callbacks[i];
- ((AEAudioControllerTimingCallback)callback->callback)(callback->userInfo, THIS, &timestamp, AEAudioTimingContextInput);
+ ((AEAudioControllerTimingCallback)callback->callback)(callback->userInfo, THIS, &timestamp, inNumberFrames, AEAudioTimingContextInput);
}
if ( THIS->_inputAudioConverter ) {
@@ -654,7 +654,7 @@ static OSStatus topRenderNotifyCallback(void *inRefCon, AudioUnitRenderActionFla
// Before render: Perform timing callbacks
for ( int i=0; i<THIS->_timingCallbacks.count; i++ ) {
callback_t *callback = &THIS->_timingCallbacks.callbacks[i];
- ((AEAudioControllerTimingCallback)callback->callback)(callback->userInfo, THIS, inTimeStamp, AEAudioTimingContextOutput);
+ ((AEAudioControllerTimingCallback)callback->callback)(callback->userInfo, THIS, inTimeStamp, inNumberFrames, AEAudioTimingContextOutput);
}
} else {
// After render
View
159 TheAmazingAudioEngine/AEBlockScheduler.h
@@ -0,0 +1,159 @@
+//
+// AEBlockScheduler.h
+// TheAmazingAudioEngine
+//
+// Created by Michael Tyson on 22/03/2013.
+// Copyright (c) 2013 A Tasty Pixel. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "AEAudioController.h"
+
+/*!
+ * Schedule information dictionary keys
+ */
+extern NSString const * AEBlockSchedulerKeyBlock;
+extern NSString const * AEBlockSchedulerKeyTimestampInHostTicks;
+extern NSString const * AEBlockSchedulerKeyResponseBlock;
+extern NSString const * AEBlockSchedulerKeyIdentifier;
+extern NSString const * AEBlockSchedulerKeyTimingContext;
+
+/*!
+ * Scheduler block format
+ *
+ * Will be executed in a Core Audio thread context, so it's very important not to call
+ * any Objective-C methods, allocate or free memory, or hold locks.
+ *
+ * @param intervalStartTime The timestamp corresponding to the start of this time interval
+ * @param offsetInFrames The offset, in frames, of this schedule's fire timestamp into the current time interval
+ */
+typedef void (^AEBlockSchedulerBlock)(const AudioTimeStamp *intervalStartTime, UInt32 offsetInFrames);
+
+/*!
+ * Scheduler response block
+ *
+ * Will be called on the main thread
+ */
+typedef void (^AEBlockSchedulerResponseBlock)();
+
+/*!
+ * Block scheduler
+ *
+ * This class allows you to schedule blocks to be performed at
+ * a particular time, on the Core Audio thread.
+ *
+ * To use this class, create an instance, then add it as a timing
+ * receiver using AEAudioController's @link AEAudioController::addTimingReceiver: addTimingReceiver: @endlink.
+ *
+ * Then begin scheduling blocks using @link scheduleBlock:atTime:timingContext:identifier: @endlink.
+ */
+@interface AEBlockScheduler : NSObject <AEAudioTimingReceiver>
+
+/*!
+ * Utility: Get current time
+ */
++ (uint64_t)now;
+
+/*!
+ * Utility: Convert time in seconds to host ticks
+ */
++ (uint64_t)hostTicksFromSeconds:(NSTimeInterval)seconds;
+
+/*!
+ * Utility: Convert time in host ticks to seconds
+ */
++ (NSTimeInterval)secondsFromHostTicks:(uint64_t)ticks;
+
+/*!
+ * Utility: Create a timestamp in host ticks the given number of seconds in the future
+ */
++ (uint64_t)timestampWithSecondsFromNow:(NSTimeInterval)seconds;
+
+/*!
+ * Utility: Determine the number of seconds until a given timestamp
+ */
++ (NSTimeInterval)secondsUntilTimestamp:(uint64_t)timestamp;
+
+/*!
+ * Initialize
+ *
+ * @param audioController The audio controller
+ */
+- (id)initWithAudioController:(AEAudioController*)audioController;
+
+/*!
+ * Schedule a block for execution
+ *
+ * Once scheduled, the given block will be performed at or before the given
+ * time. Depending on the [hardware buffer duration](@ref AEAudioController::preferredBufferDuration),
+ * this may occur some milliseconds before the scheduled time.
+ *
+ * The actual time corresponding to the beginning of the time interval in which the
+ * scheduled block was actually invoked will be passed to the block as an argument, as well
+ * as the number of frames into the time interval that the block is scheduled.
+ *
+ * VERY IMPORTANT NOTE: This block will be invoked on the Core Audio thread. You must never
+ * call any Objective-C methods, allocate or free memory, or hold locks within this block,
+ * or you will cause audio glitches to occur.
+ *
+ * @param block Block to perform
+ * @param time Time at which block will be performed, in host ticks
+ * @param context Timing context
+ * @param identifier An identifier used to refer to the schedule later, if necessary (may not be nil)
+ */
+- (void)scheduleBlock:(AEBlockSchedulerBlock)block atTime:(uint64_t)time timingContext:(AEAudioTimingContext)context identifier:(id<NSCopying>)identifier;
+
+/*!
+ * Schedule a block for execution, with a response block to be performed on the main thread
+ *
+ * Once scheduled, the given block will be performed at or before the given
+ * time. Depending on the [hardware buffer duration](@ref AEAudioController::preferredBufferDuration),
+ * this may occur some milliseconds before the scheduled time.
+ *
+ * The actual time corresponding to the beginning of the time interval in which the
+ * scheduled block was actually invoked will be passed to the block as an argument, as well
+ * as the number of frames into the time interval that the block is scheduled.
+ *
+ * VERY IMPORTANT NOTE: This block will be invoked on the Core Audio thread. You must never
+ * call any Objective-C methods, allocate or free memory, or hold locks within this block,
+ * or you will cause audio glitches to occur.
+ *
+ * Once the schedule has finished, the response block will be performed on the main thread.
+ *
+ * @param block Block to perform
+ * @param time Time at which block will be performed, in host ticks
+ * @param context Timing context
+ * @param identifier An identifier used to refer to the schedule later, if necessary (may not be nil)
+ * @param response A block to be performed on the main thread after the main block has been performed
+ */
+- (void)scheduleBlock:(AEBlockSchedulerBlock)block atTime:(uint64_t)time timingContext:(AEAudioTimingContext)context identifier:(id<NSCopying>)identifier mainThreadResponseBlock:(AEBlockSchedulerResponseBlock)response;
+
+/*!
+ * Obtain a list of schedules awaiting execution
+ *
+ * This will return an array of schedule identifiers, which you passed
+ * as the 'identifier' parameter when scheduling.
+ *
+ * @returns Array of block identifiers
+ */
+- (NSArray*)schedules;
+
+/*!
+ * Obtain information about a particular schedule
+ *
+ * This will return a dictionary with information about the schedule associated
+ * with the given identifier.
+ */
+- (NSDictionary*)infoForScheduleWithIdentifier:(id<NSCopying>)identifier;
+
+/*!
+ * Cancel a given schedule, so that it will not be performed
+ *
+ * Note: If you have scheduled multiple blocks with the same identifier,
+ * all of these blocks will be cancelled.
+ *
+ * @param identifier The schedule identifier
+ */
+- (void)cancelScheduleWithIdentifier:(id<NSCopying>)identifier;
+
+@end
View
220 TheAmazingAudioEngine/AEBlockScheduler.m
@@ -0,0 +1,220 @@
+//
+// AEBlockScheduler.m
+// TheAmazingAudioEngine
+//
+// Created by Michael Tyson on 22/03/2013.
+// Copyright (c) 2013 A Tasty Pixel. All rights reserved.
+//
+
+#import "AEBlockScheduler.h"
+#import <libkern/OSAtomic.h>
+#import <mach/mach_time.h>
+
+static double __hostTicksToSeconds = 0.0;
+static double __secondsToHostTicks = 0.0;
+
+const int kMaximumSchedules = 30;
+
+NSString const * AEBlockSchedulerKeyBlock = @"block";
+NSString const * AEBlockSchedulerKeyTimestampInHostTicks = @"time";
+NSString const * AEBlockSchedulerKeyResponseBlock = @"response";
+NSString const * AEBlockSchedulerKeyIdentifier = @"identifier";
+NSString const * AEBlockSchedulerKeyTimingContext = @"context";
+
+struct _schedule_t {
+ AEBlockSchedulerBlock block;
+ void (^responseBlock)();
+ uint64_t time;
+ AEAudioTimingContext context;
+ id identifier;
+};
+
+@interface AEBlockScheduler () {
+ struct _schedule_t _schedule[kMaximumSchedules];
+}
+@property (nonatomic, retain) NSMutableArray *scheduledIdentifiers;
+@property (nonatomic, assign) AEAudioController *audioController;
+@end
+
+@implementation AEBlockScheduler
+
++(void)initialize {
+ mach_timebase_info_data_t tinfo;
+ mach_timebase_info(&tinfo);
+ __hostTicksToSeconds = ((double)tinfo.numer / tinfo.denom) * 1.0e-9;
+ __secondsToHostTicks = 1.0 / __hostTicksToSeconds;
+}
+
++ (uint64_t)now {
+ return mach_absolute_time();
+}
+
++ (uint64_t)hostTicksFromSeconds:(NSTimeInterval)seconds {
+ return seconds * __secondsToHostTicks;
+}
+
++ (NSTimeInterval)secondsFromHostTicks:(uint64_t)ticks {
+ return (double)ticks * __hostTicksToSeconds;
+}
+
++ (uint64_t)timestampWithSecondsFromNow:(NSTimeInterval)seconds {
+ return mach_absolute_time() + (seconds * __secondsToHostTicks);
+}
+
++ (NSTimeInterval)secondsUntilTimestamp:(uint64_t)timestamp {
+ return (timestamp - mach_absolute_time()) * __hostTicksToSeconds;
+}
+
+- (id)initWithAudioController:(AEAudioController *)audioController {
+ if ( !(self = [super init]) ) return nil;
+
+ self.audioController = audioController;
+ self.scheduledIdentifiers = [NSMutableArray array];
+
+ return self;
+}
+
+-(void)dealloc {
+ for ( int i=0; i<kMaximumSchedules; i++ ) {
+ if ( _schedule[i].block ) {
+ [_schedule[i].block release];
+ if ( _schedule[i].responseBlock ) {
+ [_schedule[i].responseBlock release];
+ }
+ [_schedule[i].identifier release];
+ }
+ }
+ self.scheduledIdentifiers = nil;
+ self.audioController = nil;
+ [super dealloc];
+}
+
+-(void)scheduleBlock:(AEBlockSchedulerBlock)block atTime:(uint64_t)time timingContext:(AEAudioTimingContext)context identifier:(id<NSCopying>)identifier {
+ [self scheduleBlock:block atTime:time timingContext:context identifier:identifier mainThreadResponseBlock:nil];
+}
+
+-(void)scheduleBlock:(AEBlockSchedulerBlock)block atTime:(uint64_t)time timingContext:(AEAudioTimingContext)context identifier:(id<NSCopying>)identifier mainThreadResponseBlock:(AEBlockSchedulerResponseBlock)response {
+ NSAssert(identifier != nil && block != nil, @"Identifier and block must not be nil");
+
+ struct _schedule_t *schedule = [self scheduleWithIdentifier:nil];
+ if ( !schedule ) {
+ NSLog(@"Unable to schedule block %@: No space in scheduling table.", identifier);
+ return;
+ }
+
+ struct _schedule_t scheduleValue;
+ scheduleValue.identifier = [(NSObject*)identifier copy];
+ scheduleValue.block = [block copy];
+ scheduleValue.responseBlock = response ? [response copy] : nil;
+ scheduleValue.time = time;
+ scheduleValue.context = context;
+
+ OSMemoryBarrier();
+
+ *schedule = scheduleValue;
+
+ [_scheduledIdentifiers addObject:identifier];
+}
+
+-(NSArray *)schedules {
+ return _scheduledIdentifiers;
+}
+
+-(void)cancelScheduleWithIdentifier:(id<NSCopying>)identifier {
+ NSAssert(identifier != nil, @"Identifier must not be nil");
+
+ struct _schedule_t *pointers[kMaximumSchedules];
+ struct _schedule_t values[kMaximumSchedules];
+ int scheduleCount = 0;
+
+ for ( int i=0; i<kMaximumSchedules; i++ ) {
+ if ( _schedule[i].identifier && [_schedule[i].identifier isEqual:identifier] ) {
+ pointers[scheduleCount] = &_schedule[i];
+ values[scheduleCount] = _schedule[i];
+ scheduleCount++;
+ }
+ }
+
+ if ( scheduleCount == 0 ) return;
+
+ struct _schedule_t **pointers_array = pointers;
+ [_audioController performSynchronousMessageExchangeWithBlock:^{
+ for ( int i=0; i<scheduleCount; i++ ) {
+ memset(pointers_array[i], 0, sizeof(struct _schedule_t));
+ }
+ }];
+
+ [_scheduledIdentifiers removeObject:identifier];
+
+ for ( int i=0; i<scheduleCount; i++ ) {
+ [values[i].block release];
+ if ( values[i].responseBlock ) {
+ [values[i].responseBlock release];
+ }
+ [values[i].identifier release];
+ }
+}
+
+- (NSDictionary*)infoForScheduleWithIdentifier:(id<NSCopying>)identifier {
+ struct _schedule_t *schedule = [self scheduleWithIdentifier:identifier];
+ if ( !schedule ) return nil;
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ schedule->block, AEBlockSchedulerKeyBlock,
+ schedule->identifier, AEBlockSchedulerKeyIdentifier,
+ schedule->responseBlock ? (id)schedule->responseBlock : [NSNull null], AEBlockSchedulerKeyResponseBlock,
+ [NSNumber numberWithLongLong:schedule->time], AEBlockSchedulerKeyTimestampInHostTicks,
+ [NSNumber numberWithInt:schedule->context], AEBlockSchedulerKeyTimingContext,
+ nil];
+}
+
+- (struct _schedule_t*)scheduleWithIdentifier:(id<NSCopying>)identifier {
+ for ( int i=0; i<kMaximumSchedules; i++ ) {
+ if ( (identifier && _schedule[i].identifier && [_schedule[i].identifier isEqual:identifier]) || (!identifier && !_schedule[i].identifier) ) {
+ return &_schedule[i];
+ }
+ }
+ return NULL;
+}
+
+struct _timingReceiverFinishSchedule_t { struct _schedule_t schedule; AEBlockScheduler *THIS; };
+static void timingReceiverFinishSchedule(AEAudioController *audioController, void *userInfo, int len) {
+ struct _timingReceiverFinishSchedule_t *arg = (struct _timingReceiverFinishSchedule_t*)userInfo;
+
+ if ( arg->schedule.responseBlock ) {
+ arg->schedule.responseBlock();
+ [arg->schedule.responseBlock release];
+ }
+ [arg->schedule.block release];
+
+ [arg->THIS->_scheduledIdentifiers removeObject:arg->schedule.identifier];
+
+ [arg->schedule.identifier release];
+}
+
+static void timingReceiver(id receiver,
+ AEAudioController *audioController,
+ const AudioTimeStamp *time,
+ UInt32 const frames,
+ AEAudioTimingContext context) {
+ AEBlockScheduler *THIS = receiver;
+ uint64_t endTime = time->mHostTime + AEConvertFramesToSeconds(audioController, frames)*__secondsToHostTicks;
+
+ for ( int i=0; i<kMaximumSchedules; i++ ) {
+ if ( THIS->_schedule[i].block && THIS->_schedule[i].context == context && THIS->_schedule[i].time && endTime >= THIS->_schedule[i].time ) {
+ UInt32 offset = THIS->_schedule[i].time > time->mHostTime ? AEConvertSecondsToFrames(audioController, (THIS->_schedule[i].time - time->mHostTime)*__hostTicksToSeconds) : 0;
+ THIS->_schedule[i].block(time, offset);
+ AEAudioControllerSendAsynchronousMessageToMainThread(audioController,
+ timingReceiverFinishSchedule,
+ &(struct _timingReceiverFinishSchedule_t) { .schedule = THIS->_schedule[i], .THIS = THIS },
+ sizeof(struct _timingReceiverFinishSchedule_t));
+ memset(&THIS->_schedule[i], 0, sizeof(struct _schedule_t));
+ }
+ }
+}
+
+-(AEAudioControllerTimingCallback)timingReceiverCallback {
+ return timingReceiver;
+}
+
+@end
View
36 TheAmazingAudioEngine/TheAmazingAudioEngine.h
@@ -36,6 +36,7 @@
#import "AEAudioUnitChannel.h"
#import "AEAudioUnitFilter.h"
#import "AEFloatConverter.h"
+#import "AEBlockScheduler.h"
#import "AEUtilities.h"
/*!
@@ -855,6 +856,41 @@ self.filter = [AEBlockFilter filterWithBlock:^(AEAudioControllerFilterProducer p
generated (@link AEAudioTimingContextOutput @endlink). In both cases, the timing receivers will be notified before
any of the audio receivers or channels are invoked, so that you can set app state that will affect the current time interval.
+ @subsection Scheduling Scheduling Events
+
+ AEBlockScheduler is a class you can use to schedule blocks for execution at a particular time. This implements the
+ @link AEAudioTimingReceiver @endlink protocol, and provides an interface for scheduling blocks with sample-level
+ accuracy.
+
+ To use it, instantiate AEBlockScheduler, add it as a timing receiver with [addTimingReceiver:](@ref AEAudioController::addTimingReceiver:),
+ then begin scheduling events using
+ @link AEBlockScheduler::scheduleBlock:atTime:timingContext:identifier: scheduleBlock:atTime:timingContext:identifier: @endlink:
+
+ @code
+ self.scheduler = [[AEBlockScheduler alloc] initWithAudioController:_audioController];
+ [_audioController addTimingReceiver:_scheduler];
+
+ ...
+
+ [_scheduler scheduleBlock:^(const AudioTimeStamp *time, UInt32 offset) {
+ // We are now on the Core Audio thread at *time*, which is *offset* frames
+ // before the time we scheduled, *timestamp*.
+ }
+ atTime:timestamp
+ timingContext:AEAudioTimingContextOutput
+ identifier:@"my event"];
+ @endcode
+
+ The block will be passed the current time, and the number of frames offset between the current time
+ and the scheduled time.
+
+ The alternate scheduling method, @link AEBlockScheduler::scheduleBlock:atTime:timingContext:identifier:mainThreadResponseBlock: scheduleBlock:atTime:timingContext:identifier:mainThreadResponseBlock: @endlink,
+ allows you to provide a block that will be called on the main thread after the schedule has completed.
+
+ There are a number of utilities you can use to construct and calculate timestamps, including
+ [now](@ref AEBlockScheduler::now), [timestampWithSecondsFromNow:](@ref AEBlockScheduler::timestampWithSecondsFromNow:),
+ [hostTicksFromSeconds:](@ref AEBlockScheduler::hostTicksFromSeconds:) and
+ [secondsFromHostTicks:](@ref AEBlockScheduler::secondsFromHostTicks:).
@page Contributing Contributing

0 comments on commit b6b8ac2

Please sign in to comment.