AVAEMixerSample/AudioEngine.m

/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this sample’s licensing information
    
    Abstract:
    AudioEngine is the main controller class that creates the following objects:
                    AVAudioEngine       *_engine;
                    AVAudioPlayerNode   *_marimbaPlayer;
                    AVAudioPlayerNode   *_drumPlayer;
                    AVAudioUnitDelay    *_delay;
                    AVAudioUnitReverb   *_reverb;
                    AVAudioPCMBuffer    *_marimbaLoopBuffer;
                    AVAudioPCMBuffer    *_drumLoopBuffer;
                    
                 It connects all the nodes, loads the buffers as well as controls the AVAudioEngine object itself.
*/
 
#import "AudioEngine.h"
 
@import AVFoundation;
@import Accelerate;
 
#pragma mark AudioEngine class extensions
 
@interface AudioEngine() {
    AVAudioEngine       *_engine;
    AVAudioPlayerNode   *_marimbaPlayer;
    AVAudioPlayerNode   *_drumPlayer;
    AVAudioUnitDelay    *_delay;
    AVAudioUnitReverb   *_reverb;
    AVAudioPCMBuffer    *_marimbaLoopBuffer;
    AVAudioPCMBuffer    *_drumLoopBuffer;
    
    // for the node tap
    NSURL               *_mixerOutputFileURL;
    AVAudioPlayerNode   *_mixerOutputFilePlayer;
    BOOL                _mixerOutputFilePlayerIsPaused;
    BOOL                _isRecording;
}
 
- (void)handleInterruption:(NSNotification *)notification;
- (void)handleRouteChange:(NSNotification *)notification;
 
@end
 
#pragma mark AudioEngine implementation
 
@implementation AudioEngine
 
- (instancetype)init
{
    if (self = [super init]) {
        // create the various nodes
        
        /*  AVAudioPlayerNode supports scheduling the playback of AVAudioBuffer instances,
            or segments of audio files opened via AVAudioFile. Buffers and segments may be
            scheduled at specific points in time, or to play immediately following preceding segments. */
        
        _marimbaPlayer = [[AVAudioPlayerNode alloc] init];
        _drumPlayer = [[AVAudioPlayerNode alloc] init];
        
        /*  A delay unit delays the input signal by the specified time interval
            and then blends it with the input signal. The amount of high frequency
            roll-off can also be controlled in order to simulate the effect of
            a tape delay. */
        
        _delay = [[AVAudioUnitDelay alloc] init];
        
        /*  A reverb simulates the acoustic characteristics of a particular environment.
            Use the different presets to simulate a particular space and blend it in with
            the original signal using the wetDryMix parameter. */
        
        _reverb = [[AVAudioUnitReverb alloc] init];
        
        
        _mixerOutputFilePlayer = [[AVAudioPlayerNode alloc] init];
        
        _mixerOutputFileURL = nil;
        _mixerOutputFilePlayerIsPaused = NO;
        _isRecording = NO;
        
        // create an instance of the engine and attach the nodes
        [self createEngineAndAttachNodes];
        
        NSError *error;
        
        // load marimba loop
        NSURL *marimbaLoopURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"marimbaLoop" ofType:@"caf"]];
        AVAudioFile *marimbaLoopFile = [[AVAudioFile alloc] initForReading:marimbaLoopURL error:&error];
        _marimbaLoopBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:[marimbaLoopFile processingFormat] frameCapacity:(AVAudioFrameCount)[marimbaLoopFile length]];
        NSAssert([marimbaLoopFile readIntoBuffer:_marimbaLoopBuffer error:&error], @"couldn't read marimbaLoopFile into buffer, %@", [error localizedDescription]);
        
        // load drum loop
        NSURL *drumLoopURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"drumLoop" ofType:@"caf"]];
        AVAudioFile *drumLoopFile = [[AVAudioFile alloc] initForReading:drumLoopURL error:&error];
        _drumLoopBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:[drumLoopFile processingFormat] frameCapacity:(AVAudioFrameCount)[drumLoopFile length]];
        NSAssert([drumLoopFile readIntoBuffer:_drumLoopBuffer error:&error], @"couldn't read drumLoopFile into buffer, %@", [error localizedDescription]);
        
        // sign up for notifications from the engine if there's a hardware config change
        [[NSNotificationCenter defaultCenter] addObserverForName:AVAudioEngineConfigurationChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
            
            // if we've received this notification, something has changed and the engine has been stopped
            // re-wire all the connections and start the engine
            NSLog(@"Received a %@ notification!", AVAudioEngineConfigurationChangeNotification);
            NSLog(@"Re-wiring connections and starting once again");
            [self makeEngineConnections];
            [self startEngine];
            
            // post notification
            if ([self.delegate respondsToSelector:@selector(engineConfigurationHasChanged)]) {
                [self.delegate engineConfigurationHasChanged];
            }
        }];
        
        // AVAudioSession setup
        [self initAVAudioSession];
        
        // make engine connections
        [self makeEngineConnections];
        
        // settings for effects units
        [_reverb loadFactoryPreset:AVAudioUnitReverbPresetMediumHall3];
        _delay.delayTime = 0.5;
        _delay.wetDryMix = 0.0;
        
        // start the engine
        [self startEngine];
    }
    return self;
}
 
- (void)createEngineAndAttachNodes
{
    /*  An AVAudioEngine contains a group of connected AVAudioNodes ("nodes"), each of which performs
        an audio signal generation, processing, or input/output task.
        
        Nodes are created separately and attached to the engine.
 
        The engine supports dynamic connection, disconnection and removal of nodes while running,
        with only minor limitations:
        - all dynamic reconnections must occur upstream of a mixer
        - while removals of effects will normally result in the automatic connection of the adjacent
            nodes, removal of a node which has differing input vs. output channel counts, or which
            is a mixer, is likely to result in a broken graph. */
 
    _engine = [[AVAudioEngine alloc] init];
    
    /*  To support the instantiation of arbitrary AVAudioNode subclasses, instances are created
        externally to the engine, but are not usable until they are attached to the engine via
        the attachNode method. */
    
    [_engine attachNode:_marimbaPlayer];
    [_engine attachNode:_drumPlayer];
    [_engine attachNode:_delay];
    [_engine attachNode:_reverb];
    [_engine attachNode:_mixerOutputFilePlayer];
}
 
- (void)makeEngineConnections
{
    /*  The engine will construct a singleton main mixer and connect it to the outputNode on demand,
        when this property is first accessed. You can then connect additional nodes to the mixer.
        
        By default, the mixer's output format (sample rate and channel count) will track the format 
        of the output node. You may however make the connection explicitly with a different format. */
    
    // get the engine's optional singleton main mixer node
    AVAudioMixerNode *mainMixer = [_engine mainMixerNode];
    
    // establish a connection between nodes
    
    /*  Nodes have input and output buses (AVAudioNodeBus). Use connect:to:fromBus:toBus:format: to
        establish connections betweeen nodes. Connections are always one-to-one, never one-to-many or
        many-to-one.
    
        Note that any pre-existing connection(s) involving the source's output bus or the
        destination's input bus will be broken.
    
        @method connect:to:fromBus:toBus:format:
        @param node1 the source node
        @param node2 the destination node
        @param bus1 the output bus on the source node
        @param bus2 the input bus on the destination node
        @param format if non-null, the format of the source node's output bus is set to this
            format. In all cases, the format of the destination node's input bus is set to
            match that of the source node's output bus. */
    
    // marimba player -> delay -> main mixer
    [_engine connect: _marimbaPlayer to:_delay format:_marimbaLoopBuffer.format];
    [_engine connect:_delay to:mainMixer format:_marimbaLoopBuffer.format];
    
    // drum player -> reverb -> main mixer
    [_engine connect:_drumPlayer to:_reverb format:_drumLoopBuffer.format];
    [_engine connect:_reverb to:mainMixer format:_drumLoopBuffer.format];
    
    // node tap player
    [_engine connect:_mixerOutputFilePlayer to:mainMixer format:[mainMixer outputFormatForBus:0]];
}
 
- (void)startEngine
{
    // start the engine
    
    /*  startAndReturnError: calls prepare if it has not already been called since stop.
    
        Starts the audio hardware via the AVAudioInputNode and/or AVAudioOutputNode instances in
        the engine. Audio begins flowing through the engine.
    
        This method will return YES for sucess.
     
        Reasons for potential failure include:
        
        1. There is problem in the structure of the graph. Input can't be routed to output or to a
            recording tap through converter type nodes.
        2. An AVAudioSession error.
        3. The driver failed to start the hardware. */
    
    NSError *error;
    NSAssert([_engine startAndReturnError:&error], @"couldn't start engine, %@", [error localizedDescription]);
}
 
- (void)toggleMarimba {
    if (!self.marimbaPlayerIsPlaying) {
        [_marimbaPlayer scheduleBuffer:_marimbaLoopBuffer atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:nil];
        [_marimbaPlayer play];
    } else
        [_marimbaPlayer stop];
}
 
- (void)toggleDrums {
    if (!self.drumPlayerIsPlaying) {
        [_drumPlayer scheduleBuffer:_drumLoopBuffer atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:nil];
        [_drumPlayer play];
    } else
        [_drumPlayer stop];
}
 
- (void)startRecordingMixerOutput
{
    // install a tap on the main mixer output bus and write output buffers to file
    
    /*  The method installTapOnBus:bufferSize:format:block: will create a "tap" to record/monitor/observe the output of the node.
    
        @param bus
            the node output bus to which to attach the tap
        @param bufferSize
            the requested size of the incoming buffers. The implementation may choose another size.
        @param format
            If non-nil, attempts to apply this as the format of the specified output bus. This should
            only be done when attaching to an output bus which is not connected to another node; an
            error will result otherwise.
            The tap and connection formats (if non-nil) on the specified bus should be identical. 
            Otherwise, the latter operation will override any previously set format.
            Note that for AVAudioOutputNode, tap format must be specified as nil.
        @param tapBlock
            a block to be called with audio buffers
 
        Only one tap may be installed on any bus. Taps may be safely installed and removed while
        the engine is running. */
    
    NSError *error;
    if (!_mixerOutputFileURL) _mixerOutputFileURL = [NSURL URLWithString:[NSTemporaryDirectory() stringByAppendingString:@"mixerOutput.caf"]];
    
    AVAudioMixerNode *mainMixer = [_engine mainMixerNode];
    AVAudioFile *mixerOutputFile = [[AVAudioFile alloc] initForWriting:_mixerOutputFileURL settings:[[mainMixer outputFormatForBus:0] settings] error:&error];
    NSAssert(mixerOutputFile != nil, @"mixerOutputFile is nil, %@", [error localizedDescription]);
    
    if (!_engine.isRunning) [self startEngine];
    [mainMixer installTapOnBus:0 bufferSize:4096 format:[mainMixer outputFormatForBus:0] block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) {
        NSError *error;
        
        // as AVAudioPCMBuffer's are delivered this will write sequentially. The buffer's frameLength signifies how much of the buffer is to be written
        // IMPORTANT: The buffer format MUST match the file's processing format which is why outputFormatForBus: was used when creating the AVAudioFile object above
        NSAssert([mixerOutputFile writeFromBuffer:buffer error:&error], @"error writing buffer data to file, %@", [error localizedDescription]);
    }];
    _isRecording = true;
}
 
- (void)stopRecordingMixerOutput
{
    // stop recording really means remove the tap on the main mixer that was created in startRecordingMixerOutput
    if (_isRecording) {
        [[_engine mainMixerNode] removeTapOnBus:0];
        _isRecording = NO;
    }
}
 
- (void)playRecordedFile
{
    if (_mixerOutputFilePlayerIsPaused) {
        [_mixerOutputFilePlayer play];
    }
    else {
        if (_mixerOutputFileURL) {
            NSError *error;
            AVAudioFile *recordedFile = [[AVAudioFile alloc] initForReading:_mixerOutputFileURL error:&error];
            NSAssert(recordedFile != nil, @"recordedFile is nil, %@", [error localizedDescription]);
            [_mixerOutputFilePlayer scheduleFile:recordedFile atTime:nil completionHandler:^{
                _mixerOutputFilePlayerIsPaused = NO;
                
                // the data in the file has been scheduled but the player isn't actually done playing yet
                // calculate the approximate time remaining for the player to finish playing and then dispatch the notification to the main thread
                AVAudioTime *playerTime = [_mixerOutputFilePlayer playerTimeForNodeTime:_mixerOutputFilePlayer.lastRenderTime];
                double delayInSecs = (recordedFile.length - playerTime.sampleTime) / recordedFile.processingFormat.sampleRate;
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSecs * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    if ([self.delegate respondsToSelector:@selector(mixerOutputFilePlayerHasStopped)])
                        [self.delegate mixerOutputFilePlayerHasStopped];
                    [_mixerOutputFilePlayer stop];
                });
            }];
            [_mixerOutputFilePlayer play];
            _mixerOutputFilePlayerIsPaused = NO;
        }
    }
}
 
- (void)stopPlayingRecordedFile
{
    [_mixerOutputFilePlayer stop];
    _mixerOutputFilePlayerIsPaused = NO;
}
 
- (void)pausePlayingRecordedFile
{
    [_mixerOutputFilePlayer pause];
    _mixerOutputFilePlayerIsPaused = YES;
}
 
- (BOOL)marimbaPlayerIsPlaying
{
    return _marimbaPlayer.isPlaying;
}
 
- (BOOL)drumPlayerIsPlaying
{
    return _drumPlayer.isPlaying;
}
 
- (void)setMarimbaPlayerVolume:(float)marimbaPlayerVolume
{
    _marimbaPlayer.volume = marimbaPlayerVolume;
}
 
- (float)marimbaPlayerVolume
{
    return _marimbaPlayer.volume;
}
 
- (void)setDrumPlayerVolume:(float)drumPlayerVolume
{
    _drumPlayer.volume = drumPlayerVolume;
}
 
- (float)drumPlayerVolume
{
    return _drumPlayer.volume;
}
 
- (void)setOutputVolume:(float)outputVolume
{
    _engine.mainMixerNode.outputVolume = outputVolume;
}
 
- (float)outputVolume
{
    return _engine.mainMixerNode.outputVolume;
}
 
- (void)setMarimbaPlayerPan:(float)marimbaPlayerPan
{
    _marimbaPlayer.pan = marimbaPlayerPan;
}
 
- (float)marimbaPlayerPan
{
    return _marimbaPlayer.pan;
}
 
- (void)setDrumPlayerPan:(float)drumPlayerPan
{
    _drumPlayer.pan = drumPlayerPan;
}
 
- (float)drumPlayerPan
{
    return _drumPlayer.pan;
}
 
- (void)setDelayWetDryMix:(float)delayWetDryMix
{
    _delay.wetDryMix = delayWetDryMix * 100.0;
}
 
- (float)delayWetDryMix
{
    return _delay.wetDryMix/100.0;
}
 
- (void)setReverbWetDryMix:(float)reverbWetDryMix
{
    _reverb.wetDryMix = reverbWetDryMix * 100.0;
}
 
- (float)reverbWetDryMix
{
    return _reverb.wetDryMix/100.0;
}
 
- (void)setBypassDelay:(BOOL)bypassDelay
{
    _delay.bypass = bypassDelay;
}
 
- (BOOL)bypassDelay
{
    return _delay.bypass;
}
 
- (void)setBypassReverb:(BOOL)bypassReverb
{
    _reverb.bypass = bypassReverb;
}
 
- (BOOL)bypassReverb
{
    return _reverb.bypass;
}
 
#pragma mark AVAudioSession
 
- (void)initAVAudioSession
{
    // For complete details regarding the use of AVAudioSession see the AVAudioSession Programming Guide
    // /RU/iOS/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html
    
    // Configure the audio session
    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    NSError *error;
    
    // set the session category
    bool success = [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
    if (!success) NSLog(@"Error setting AVAudioSession category! %@\n", [error localizedDescription]);
    
    double hwSampleRate = 44100.0;
    success = [sessionInstance setPreferredSampleRate:hwSampleRate error:&error];
    if (!success) NSLog(@"Error setting preferred sample rate! %@\n", [error localizedDescription]);
    
    NSTimeInterval ioBufferDuration = 0.0029;
    success = [sessionInstance setPreferredIOBufferDuration:ioBufferDuration error:&error];
    if (!success) NSLog(@"Error setting preferred io buffer duration! %@\n", [error localizedDescription]);
    
    // add interruption handler
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleInterruption:)
                                                 name:AVAudioSessionInterruptionNotification
                                               object:sessionInstance];
    
    // we don't do anything special in the route change notification
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleRouteChange:)
                                                 name:AVAudioSessionRouteChangeNotification
                                               object:sessionInstance];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleMediaServicesReset:)
                                                 name:AVAudioSessionMediaServicesWereResetNotification
                                               object:sessionInstance];
    
    // activate the audio session
    success = [sessionInstance setActive:YES error:&error];
    if (!success) NSLog(@"Error setting session active! %@\n", [error localizedDescription]);
}
 
- (void)handleInterruption:(NSNotification *)notification
{
    UInt8 theInterruptionType = [[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] intValue];
    
    NSLog(@"Session interrupted > --- %s ---\n", theInterruptionType == AVAudioSessionInterruptionTypeBegan ? "Begin Interruption" : "End Interruption");
    
    if (theInterruptionType == AVAudioSessionInterruptionTypeBegan) {
        // the engine will pause itself
    }
    if (theInterruptionType == AVAudioSessionInterruptionTypeEnded) {
        // make sure to activate the session
        NSError *error;
        bool success = [[AVAudioSession sharedInstance] setActive:YES error:&error];
        if (!success) NSLog(@"AVAudioSession set active failed with error: %@", [error localizedDescription]);
        
        // start the engine once again
        [self startEngine];
    }
}
 
- (void)handleRouteChange:(NSNotification *)notification
{
    UInt8 reasonValue = [[notification.userInfo valueForKey:AVAudioSessionRouteChangeReasonKey] intValue];
    AVAudioSessionRouteDescription *routeDescription = [notification.userInfo valueForKey:AVAudioSessionRouteChangePreviousRouteKey];
    
    NSLog(@"Route change:");
    switch (reasonValue) {
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            NSLog(@"     NewDeviceAvailable");
            break;
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
            NSLog(@"     OldDeviceUnavailable");
            break;
        case AVAudioSessionRouteChangeReasonCategoryChange:
            NSLog(@"     CategoryChange");
            NSLog(@" New Category: %@", [[AVAudioSession sharedInstance] category]);
            break;
        case AVAudioSessionRouteChangeReasonOverride:
            NSLog(@"     Override");
            break;
        case AVAudioSessionRouteChangeReasonWakeFromSleep:
            NSLog(@"     WakeFromSleep");
            break;
        case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
            NSLog(@"     NoSuitableRouteForCategory");
            break;
        default:
            NSLog(@"     ReasonUnknown");
    }
    
    NSLog(@"Previous route:\n");
    NSLog(@"%@", routeDescription);
}
 
- (void)handleMediaServicesReset:(NSNotification *)notification
{
    // if we've received this notification, the media server has been reset
    // re-wire all the connections and start the engine
    NSLog(@"Media services have been reset!");
    NSLog(@"Re-wiring connections and starting once again");
    
    [self createEngineAndAttachNodes];
    [self makeEngineConnections];
    [self startEngine];
    
    // post notification
    if ([self.delegate respondsToSelector:@selector(engineConfigurationHasChanged)]) {
        [self.delegate engineConfigurationHasChanged];
    }
}
 
@end