InterAppAudioSampler/InterAppAudioSampler/Sampler.mm
/* |
File: Sampler.mm |
Abstract: |
Version: 1.1.2 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
*/ |
#import "Sampler.h" |
#import <AVFoundation/AVAudioSession.h> |
#import <AudioToolbox/AudioToolbox.h> |
#define Check(expr) do { OSStatus err = (expr); if (err) { NSLog(@"error %d from %s", (int)err, #expr); abort(); } } while (0) |
#define NSCheck(expr) do { NSError *err = nil; if (!(expr)) { NSLog(@"error from %s: %@", #expr, err); abort(); } } while (0) |
extern "C" NSString *kTransportStateChangedNotificiation; |
extern "C" UIImage *scaleImageToSize(UIImage *image, CGSize newSize); |
//Use Category to hide private listener method used by c callback |
@interface Sampler (Private) |
-(void)audioUnitPropertyChangedListener:(void *) inObject unit:(AudioUnit) inUnit propID:(AudioUnitPropertyID) inID scope:(AudioUnitScope) inScope element:(AudioUnitElement) inElement; |
-(OSStatus)sendMusicDeviceMIDIEvent:(UInt32) inStatus data1:(UInt32) inData1 data2:(UInt32) inData2 offsetSampleFrame:(UInt32) inOffsetSampleFrame; |
@end |
//Callback for audio units bouncing from c to objective c |
void AudioUnitPropertyChangeDispatcher(void *inRefCon, AudioUnit inUnit, AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement) { |
Sampler *SELF = (Sampler *)inRefCon; |
[SELF audioUnitPropertyChangedListener:inRefCon unit:inUnit propID:inID scope:inScope element:inElement]; |
} |
@implementation Sampler |
{ |
@private |
NSURL *bankURL; |
AUGraph synthGraph; |
AudioUnit synthUnit; |
AudioUnit outputUnit; |
UInt32 patchNumber; |
Boolean graphStarted; |
bool inForeground; |
HostCallbackInfo *callBackInfo; |
} |
#pragma mark Initialization/dealloc |
- (id) init { |
self = [super init]; |
if (self) { |
// Do any additional setup after loading the view, typically from a nib. |
bankURL = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:@"Vibraphone" ofType:@"aupreset"]]; |
if (!bankURL) |
NSLog(@"[%@ %@] could not get bank path", NSStringFromClass([self class]), NSStringFromSelector(_cmd)); |
self.playing = NO; |
self.recording = NO; |
UIApplicationState appstate = [UIApplication sharedApplication].applicationState; |
inForeground = (appstate != UIApplicationStateBackground); |
} |
return self; |
} |
- (void) dealloc { |
if (callBackInfo) |
free(callBackInfo); |
[bankURL release]; |
[super dealloc]; |
} |
-(void) cleanup { // throw away engine state |
[self stopGraph]; |
[self setAudioSessionInActive]; |
AUGraphClose(synthGraph); |
DisposeAUGraph(synthGraph); // this will also cleanup any listeners that have been registered. If you are using AURemoteIO instead of AUGraph, you should make sure you cleanup that instead |
synthGraph = nil; |
} |
#pragma mark Properties |
@synthesize audioUnitIcon = _audioUnitIcon; |
#pragma mark CAUITransportEngine Protocol- Required properties |
@synthesize playing = _playing; |
@synthesize recording = _recording; |
@synthesize connected = _connected; |
@synthesize playTime = _playTime; |
#pragma mark CAUITransportEngine Protocol- Required methods |
- (BOOL) canPlay { return [self isHostConnected];} |
- (BOOL) canRewind { return [self isHostConnected];} |
- (BOOL) canRecord { return outputUnit != nil && ![self isHostPlaying]; } |
- (BOOL) isHostPlaying { return self.playing; } |
- (BOOL) isHostRecording { return self.recording; } |
- (BOOL) isHostConnected { |
if (outputUnit) { |
UInt32 connect; |
UInt32 dataSize = sizeof(UInt32); |
Check(AudioUnitGetProperty(outputUnit, kAudioUnitProperty_IsInterAppConnected, kAudioUnitScope_Global, 0, &connect, &dataSize)); |
if (connect != self.connected) { |
self.connected = connect; |
//Transition is from not connected to connected |
if (self.connected) { |
[self checkStartStopGraph]; |
//Get the appropriate callback info |
[self getHostCallBackInfo]; |
[self getAudioUnitIcon]; |
} |
//Transition is from connected to not connected; |
else { |
//If the graph is started stop it. |
if ([self isGraphStarted]) |
[self stopGraph]; |
//Attempt to restart the graph |
[self checkStartStopGraph]; |
} |
} |
} |
return self.connected; |
} |
-(void) gotoHost { |
if (outputUnit) { |
CFURLRef instrumentUrl; |
UInt32 dataSize = sizeof(instrumentUrl); |
OSStatus result = AudioUnitGetProperty(outputUnit, kAudioUnitProperty_PeerURL, kAudioUnitScope_Global, 0, &instrumentUrl, &dataSize); |
if (result == noErr) |
[[UIApplication sharedApplication] openURL:(NSURL*)instrumentUrl]; |
} |
} |
-(void) getHostCallBackInfo { |
if (self.connected) { |
if (callBackInfo) |
free(callBackInfo); |
UInt32 dataSize = sizeof(HostCallbackInfo); |
callBackInfo = (HostCallbackInfo*) malloc(dataSize); |
OSStatus result = AudioUnitGetProperty(outputUnit, kAudioUnitProperty_HostCallbacks, kAudioUnitScope_Global, 0, callBackInfo, &dataSize); |
if (result != noErr) { |
NSLog(@"Error occured fetching kAudioUnitProperty_HostCallbacks : %d", (int)result); |
free(callBackInfo); |
callBackInfo = NULL; |
} |
} |
} |
-(void) togglePlay { |
[self sendStateToRemoteHost:kAudioUnitRemoteControlEvent_TogglePlayPause]; |
[[NSNotificationCenter defaultCenter] postNotificationName:kTransportStateChangedNotificiation object:self]; |
} |
-(void) toggleRecord { |
[self sendStateToRemoteHost:kAudioUnitRemoteControlEvent_ToggleRecord]; |
[[NSNotificationCenter defaultCenter] postNotificationName:kTransportStateChangedNotificiation object:self]; |
} |
-(void) rewind { |
[self sendStateToRemoteHost:kAudioUnitRemoteControlEvent_Rewind]; |
[[NSNotificationCenter defaultCenter] postNotificationName:kTransportStateChangedNotificiation object:self]; |
} |
-(void) sendStateToRemoteHost:(AudioUnitRemoteControlEvent)state { |
if (outputUnit) { |
UInt32 controlEvent = state; |
UInt32 dataSize = sizeof(controlEvent); |
Check(AudioUnitSetProperty(outputUnit, kAudioOutputUnitProperty_RemoteControlToHost, kAudioUnitScope_Global, 0, &controlEvent, dataSize)); |
} |
} |
//Fetch the host's icon via AudioOutputUnitGetHostIcon, draw that in the view |
-(UIImage *) getAudioUnitIcon { |
if (outputUnit) |
self.audioUnitIcon = [scaleImageToSize(AudioOutputUnitGetHostIcon(outputUnit, 114), CGSizeMake(41, 41))retain] ; |
return self.audioUnitIcon; |
} |
- (NSString*) getPlayTimeString { |
[self updateStatefromTransportCallBack]; |
return formattedTimeStringForFrameCount(self.playTime, [[AVAudioSession sharedInstance] sampleRate], NO); |
} |
#pragma mark CAUIKeyboardEngine Protocol- Required methods |
-(AudioUnit) getAudioUnitInstrument { |
return synthUnit; |
} |
#pragma mark Publishing methods |
-(void) connectAndPublishSampler { |
[[NSNotificationCenter defaultCenter] addObserver: self |
selector: @selector(appHasGoneInBackground) |
name: UIApplicationDidEnterBackgroundNotification |
object: nil]; |
[[NSNotificationCenter defaultCenter] addObserver: self |
selector: @selector(appHasGoneForeground) |
name: UIApplicationWillEnterForegroundNotification |
object: nil]; |
// This notification will typically not be posted in normal circumstances because our app supports |
// being a background app. However, in the unlikely event that our app needs to be terminated, we |
// need to cleanup the graph |
[[NSNotificationCenter defaultCenter] addObserver: self |
selector: @selector(cleanup) |
name: UIApplicationWillTerminateNotification |
object: nil]; |
[self createAndPublish]; |
//If media services get reset republish output node |
[[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionMediaServicesWereResetNotification object: nil queue: nil usingBlock: ^(NSNotification *note) { |
//Throw away entire engine and rebuild like starting the app from scratch |
[self cleanup]; |
[self createAndPublish]; |
}]; |
} |
-(void) createAndPublish { |
synthGraph = [self createAUGraphWithSampler:(CFURLRef)bankURL patchNumber:patchNumber synthUnit:&synthUnit outUnit:&outputUnit]; |
[self addAudioUnitPropertyListener]; |
[self publishOutputAudioUnit]; |
[self checkStartStopGraph]; |
} |
- (void) publishOutputAudioUnit { |
AudioComponentDescription desc = { kAudioUnitType_RemoteInstrument,'iasp','appl',0,0 }; |
OSStatus result = AudioOutputUnitPublish(&desc, CFSTR("IAA Sampler Demo"), 0, outputUnit); |
if (result != noErr) |
NSLog(@"AudioOutputUnitPublish instrument result: %d", (int)result); |
desc = { kAudioUnitType_RemoteGenerator,'iasp','appl',0,0 }; |
result = AudioOutputUnitPublish(&desc, CFSTR("IAA Sampler Demo"), 0, outputUnit); |
if (result != noErr) |
NSLog(@"AudioOutputUnitPublish generator result: %d", (int)result); |
[self setupMidiCallBacks:&outputUnit userData:self]; |
} |
-(void) setupMidiCallBacks:(AudioUnit*)output userData:(void*)inUserData { |
AudioOutputUnitMIDICallbacks callBackStruct; |
callBackStruct.userData = inUserData; |
callBackStruct.MIDIEventProc = MIDIEventProcCallBack; |
callBackStruct.MIDISysExProc = NULL; |
Check(AudioUnitSetProperty (*output, |
kAudioOutputUnitProperty_MIDICallbacks, |
kAudioUnitScope_Global, |
0, |
&callBackStruct, |
sizeof(callBackStruct))); |
} |
void MIDIEventProcCallBack(void *userData, UInt32 inStatus, UInt32 inData1, UInt32 inData2, UInt32 inOffsetSampleFrame){ |
Sampler *SELF = (Sampler*)userData; |
[SELF sendMusicDeviceMIDIEvent:inStatus data1:inData1 data2:inData2 offsetSampleFrame:inOffsetSampleFrame]; |
} |
-(void) addAudioUnitPropertyListener { |
Check(AudioUnitAddPropertyListener(outputUnit, |
kAudioUnitProperty_IsInterAppConnected, |
AudioUnitPropertyChangeDispatcher, |
self)); |
Check(AudioUnitAddPropertyListener(outputUnit, |
kAudioOutputUnitProperty_HostTransportState, |
AudioUnitPropertyChangeDispatcher, |
self)); |
} |
#pragma mark Graph management |
-(void) startGraph{ |
if (!graphStarted && synthGraph) { |
Check(AUGraphStart (synthGraph)); |
graphStarted = YES; |
} |
} |
-(void) stopGraph{ |
if(graphStarted && synthGraph) { |
Check(AUGraphStop(synthGraph)); |
graphStarted = NO; |
} |
} |
-(void) setAudioSessionActive { |
AVAudioSession *session = [AVAudioSession sharedInstance]; |
NSCheck([session setPreferredSampleRate: [[AVAudioSession sharedInstance] sampleRate] error: &err]); |
NSCheck([session setCategory: AVAudioSessionCategoryPlayback withOptions: AVAudioSessionCategoryOptionMixWithOthers error: &err]); |
NSCheck([session setActive: YES error: &err]); |
} |
-(void) setAudioSessionInActive { |
AVAudioSession *session = [AVAudioSession sharedInstance]; |
NSCheck([session setActive: NO error: &err]); |
} |
- (BOOL) isGraphStarted { |
if (synthGraph) { |
Check(AUGraphIsRunning (synthGraph, &graphStarted)); |
return graphStarted; |
} else |
graphStarted = NO; |
return graphStarted; |
} |
-(void) checkStartStopGraph { |
if (self.connected || inForeground ) { |
[self setAudioSessionActive]; |
//Initialize the graph if it hasn't been already |
if (synthGraph) { |
Boolean initialized = YES; |
Check(AUGraphIsInitialized(synthGraph, &initialized)); |
if (!initialized) |
Check (AUGraphInitialize (synthGraph)); |
} |
[self startGraph]; |
} else if(!inForeground){ |
[self stopGraph]; |
[self setAudioSessionInActive]; |
} |
} |
-(AUGraph) createAUGraphWithSampler:(CFURLRef) bankURl patchNumber:(int)inPatchNumber synthUnit:(AudioUnit *)pOutSynthUnit outUnit:(AudioUnit *)pOutOutputUnit{ |
AUGraph graph = 0; |
AudioUnit synth; |
OSStatus result = noErr; |
//create the nodes of the graph |
AUNode synthNode, outNode; |
AudioComponentDescription cd; |
cd.componentManufacturer = kAudioUnitManufacturer_Apple; |
cd.componentFlags = 0; |
cd.componentFlagsMask = 0; |
Check(result = NewAUGraph (&graph)); |
cd.componentType = kAudioUnitType_MusicDevice; |
cd.componentSubType = kAudioUnitSubType_Sampler; |
Check(result = AUGraphAddNode (graph, &cd, &synthNode)); |
cd.componentType = kAudioUnitType_Output; |
cd.componentSubType = kAudioUnitSubType_RemoteIO; |
Check(result = AUGraphAddNode (graph, &cd, &outNode)); |
Check(AUGraphOpen (graph)); |
Check(AUGraphConnectNodeInput (graph, synthNode, 0, outNode, 0)); |
// ok we're good to go - get the Synth Unit... |
Check(AUGraphNodeInfo(graph, synthNode, 0, &synth)); |
Check(AUGraphNodeInfo(graph, outNode, 0, pOutOutputUnit)); |
UInt32 maxFrames = 4096; |
AudioUnitSetProperty(*pOutOutputUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFrames, sizeof(maxFrames)); |
// if the user supplies a sound bank, we'll set that before we initialize and start playing |
if (bankURL) |
{ |
AUSamplerInstrumentData bpdata; |
bpdata.fileURL = (CFURLRef)bankURL; |
bpdata.instrumentType = kInstrumentType_AUPreset; |
bpdata.bankMSB = 0x79; |
bpdata.bankLSB = 0; |
bpdata.presetID = (UInt8)patchNumber; |
Check(AudioUnitSetProperty (synth, |
kAUSamplerProperty_LoadInstrument, |
kAudioUnitScope_Global, |
0, &bpdata, sizeof(bpdata))); |
} |
*pOutSynthUnit = synth; |
return graph; |
} |
#pragma mark Application State Handling methods |
-(void) appHasGoneInBackground { |
inForeground = NO; |
[self checkStartStopGraph]; |
} |
-(void) appHasGoneForeground { |
inForeground = YES; |
[self isHostConnected]; |
[self checkStartStopGraph]; |
[self updateStatefromTransportCallBack]; |
} |
-(void) updateStatefromTransportCallBack{ |
if ([self isHostConnected] && inForeground) { |
if (!callBackInfo) |
[self getHostCallBackInfo]; |
if (callBackInfo) { |
Boolean isPlaying = self.playing; |
Boolean isRecording = self.recording; |
Float64 outCurrentSampleInTimeLine = 0; |
void * hostUserData = callBackInfo->hostUserData; |
OSStatus result = callBackInfo->transportStateProc2( hostUserData, |
&isPlaying, |
&isRecording, NULL, |
&outCurrentSampleInTimeLine, |
NULL, NULL, NULL); |
if (result == noErr) { |
self.playing = isPlaying; |
self.recording = isRecording; |
self.playTime = outCurrentSampleInTimeLine; |
} else |
NSLog(@"Error occured fetching callBackInfo->transportStateProc2 : %d", (int)result); |
} |
} |
} |
@end |
#pragma mark Private methods |
@implementation Sampler(Private) |
-(void) audioUnitPropertyChangedListener:(void *) inObject unit:(AudioUnit )inUnit propID:(AudioUnitPropertyID) inID scope:( AudioUnitScope )inScope element:(AudioUnitElement )inElement { |
if (inID == kAudioUnitProperty_IsInterAppConnected) { |
[self isHostConnected]; |
[self postUpdateStateNotification]; |
} else if (inID == kAudioOutputUnitProperty_HostTransportState) { |
[self updateStatefromTransportCallBack]; |
[self postUpdateStateNotification]; |
} |
} |
-(void) postUpdateStateNotification { |
dispatch_async(dispatch_get_main_queue(), ^{ |
[[NSNotificationCenter defaultCenter] postNotificationName:kTransportStateChangedNotificiation object:self]; |
}); |
} |
-(OSStatus) sendMusicDeviceMIDIEvent:(UInt32)inStatus data1:(UInt32)inData1 data2:(UInt32)inData2 offsetSampleFrame:(UInt32)inOffsetSampleFrame { |
return MusicDeviceMIDIEvent(synthUnit, inStatus, inData1, inData2, inOffsetSampleFrame); |
} |
@end |
#pragma mark Utility functions |
NSString *formattedTimeStringForFrameCount(UInt64 inFrameCount, Float64 inSampleRate, BOOL inShowMilliseconds) { |
UInt32 hours = 0; |
UInt32 minutes = 0; |
UInt32 seconds = 0; |
UInt32 milliseconds = 0; |
// calculate pieces |
if ((inFrameCount != 0) && (inSampleRate != 0)) { |
Float64 absoluteSeconds = (Float64)inFrameCount / inSampleRate; |
UInt64 absoluteIntSeconds = (UInt64) absoluteSeconds; |
milliseconds = (UInt32)(round((absoluteSeconds - (Float64)(absoluteIntSeconds)) * 1000.0)); |
hours = (UInt32)absoluteIntSeconds / 3600; |
absoluteIntSeconds -= (hours * 3600); |
minutes = (UInt32)absoluteIntSeconds / 60; |
absoluteIntSeconds -= (minutes * 60); |
seconds = (UInt32)absoluteIntSeconds; |
} |
NSString *retString; |
// construct strings |
NSString *hoursString = nil; |
NSString *minutesString = nil; |
NSString *secondsString = nil; |
if (hours > 0) { |
hoursString = [NSString stringWithFormat:@"%2d", (unsigned int)hours]; |
} |
if (minutes == 0) { |
minutesString = @"00"; |
} else if (minutes < 10) { |
minutesString = [NSString stringWithFormat:@"0%d", (unsigned int)minutes]; |
} else { |
minutesString = [NSString stringWithFormat:@"%d", (unsigned int)minutes]; |
} |
if (seconds == 0) { |
secondsString = @"00"; |
} else if (seconds < 10) { |
secondsString = [NSString stringWithFormat:@"0%d", (unsigned int)seconds]; |
} else { |
secondsString = [NSString stringWithFormat:@"%d", (unsigned int)seconds]; |
} |
if (!inShowMilliseconds) { |
if (hoursString) { |
retString = [NSString stringWithFormat:@"%@:%@:%@", hoursString, minutesString, secondsString]; |
} else { |
retString = [NSString stringWithFormat:@"%@:%@", minutesString, secondsString]; |
} |
} |
if (inShowMilliseconds) { |
NSString *millisecondsString; |
if (milliseconds == 0) { |
millisecondsString = @"000"; |
} else if (milliseconds < 10) { |
millisecondsString = [NSString stringWithFormat:@"00%d", (unsigned int)milliseconds]; |
} else if (milliseconds < 100) { |
millisecondsString = [NSString stringWithFormat:@"0%d", (unsigned int)milliseconds]; |
} else { |
millisecondsString = [NSString stringWithFormat:@"%d", (unsigned int)milliseconds]; |
} |
if (hoursString) { |
retString = [NSString stringWithFormat:@"%@:%@:%@.%@", hoursString, minutesString, secondsString, millisecondsString]; |
} else { |
retString = [NSString stringWithFormat:@"%@:%@.%@", minutesString, secondsString, millisecondsString]; |
} |
} |
return retString; |
} |
UIImage *scaleImageToSize(UIImage *image, CGSize newSize) { |
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0); |
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; |
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); |
UIGraphicsEndImageContext(); |
return newImage; |
} |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-03-24