/mergMicrophone/mergMicrophone.mm
https://bitbucket.org/mgoulding/mergmicrophone · Objective C++ · 515 lines · 250 code · 77 blank · 188 comment · 26 complexity · 7d217bda0c5d85c90eb56179e6c88988 MD5 · raw file
- /*
- Copyright (C) 2011 - 2018 LiveCode Ltd.
- This file is part of mergMicrophone.
- mergMicrophone is free software; you can redistribute it and/or modify it under
- the terms of the GNU General Public License v3 as published by the Free
- Software Foundation.
- mergMicrophone is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or
- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- for more details.
- You should have received a copy of the GNU General Public License
- along with mergMicrophone. If not see <http://www.gnu.org/licenses/>. */
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Source File:
- // mergmicrophone.mm
- //
- // Description:
- // This file contains the implementation of 'mergmicrophone' - a basic wrapper
- // around the iOS AVAudioRecorder class, enabling audio recording to be
- // performed in iOS applications. It is provided as part of the LiveCode
- // SDK.
- //
- // Changes:
- // 2011-06-09 MW Initial release.
- // 2011-06-14 MW Changed LCWaitDestroy to LCWaitRelease inline with 'wait'
- // object improvements.
- // Made sure the AVAudioRecorder's reference to the delegate is
- // removed before release - to ensure any hanging references to
- // the recorder don't invoke the delegate.
- // 2012-08-07 MW Updated to execute system calls on comergct thread.
- // 2012-08-30 MW Replaced var pointers with __block storage var to simplify
- // block usage.
- //
- ////////////////////////////////////////////////////////////////////////////////
- // This external implements a basic wrapper around the iOS AVAudioRecorder
- // class. It allows recording audio to a specified file with a reasonable degree
- // of control over the sampling and resulting output format.
- //
- // The implementation is split (essentially) into two parts. The first part are
- // the external handlers that control the options to be used for the next
- // recording session. These are stored in the following file-local variables:
- // s_microphone_channel_count
- // s_microphone_sample_rate
- // s_microphone_audio_format
- // s_microphone_audio_quality
- // s_microphone_target_bit_rate
- //
- // These options are used in 'StartRecording' to create and set up an instance
- // of AVAudioRecorder to which a custom delegate object (MicrophoneDelegate)
- // is attached. The AVAudioRecorder instance (and associated delegate) persist
- // until 'StopRecording' is invoked.
- //
- // Of particular interest (as an example) is the use of the LCWait abstraction
- // provided the LC API to handle the 'background completion' that occurs when
- // audio recording is stopped. This simplifies interaction with script as it
- // means that 'stop recording' blocks until the audio file is ready for use.
- // We use the usual objective-c datatypes so require Foundation.
- #import <Foundation/Foundation.h>
- // We also use AVAudioRecorder so need the AVFoundation headers.
- #import <AVFoundation/AVFoundation.h>
- // We use some of the LiveCode APIs so require that header too.
- #include <LiveCode.h>
- ////////////////////////////////////////////////////////////////////////////////
- // These enum constants comergspond to the status values as specified as
- // recording-status enum in the spec file.
- enum
- {
- kMicrophoneStatusSuccess = 0,
- kMicrophoneStatusAlreadyRecording = 1,
- kMicrophoneStatusNotRecording = 2,
- kMicrophoneStatusRecordingFailed = 3,
- };
- // These enum constants comergspond to the possible values specified as
- // audio-format-enum in the spec file.
- enum
- {
- kMicrophoneAudioFormatLinearPCM = 0,
- kMicrophoneAudioFormatAppleLossless = 1,
- kMicrophoneAudioFormatMPEG4AAC = 2,
- kMicrophoneAudioFormatILBC = 4,
- };
- ////////////////////////////////////////////////////////////////////////////////
- // The number of channels to record - either 1 (mono) or 2 (stereo).
- static int s_microphone_channel_count;
- // The sample rate (in Hertz) to record at
- static double s_microphone_sample_rate;
- // The audio format to output (see above constants)
- static int s_microphone_audio_format;
- // The audio quality to output (comergsponds to AVAudioQuality constants)
- static int s_microphone_audio_quality;
- // The target bit rate in bits per second to aim for
- static int s_microphone_target_bit_rate;
- // The AVAudioRecorder object cumergntly recording, or nil if recording is not
- // taking place.
- static AVAudioRecorder *s_microphone_recorder;
- ////////////////////////////////////////////////////////////////////////////////
- // Returns 'true' if the device has a microphone, 'false' otherwise.
- bool mergMicrophoneIsAvailable(void)
- {
- #if TARGET_OS_IPHONE
- return [[AVAudioSession sharedInstance] inputIsAvailable] == YES;
- #endif
- return true;
- }
- //////////
- // Set the number of channels to use for the next recording session. The input
- // parameter is an enum which contains 1 or 2, thus no further checking is
- // required.
- void mergMicrophoneSetNumberOfChannels(int p_channel_count)
- {
- s_microphone_channel_count = p_channel_count;
- }
- // Return the number of channels which will be used for the next recording
- // session.
- int mergMicrophoneGetNumberOfChannels(void)
- {
- return s_microphone_channel_count;
- }
- //////////
- // Set the sample rate to use for the next recording session. As the input
- // is specified as 'integer' we need to verify it is a positive number.
- void mergMicrophoneSetSampleRate(double p_sample_rate)
- {
- if (p_sample_rate <= 0)
- {
- LCExceptionRaise("sample rate must be a positive number");
- return;
- }
-
- s_microphone_sample_rate = p_sample_rate;
- }
- // Return the sample rate to use for the next recording session.
- double mergMicrophoneGetSampleRate(void)
- {
- return s_microphone_sample_rate;
- }
- //////////
- // Set the audio format to use for the next recording session. The input
- // parameter is an enum which comergsponds to one of the constants listed
- // above, thus no further checking is required.
- void mergMicrophoneSetAudioFormat(int p_audio_format)
- {
- s_microphone_audio_format = p_audio_format;
- }
- // Return the audio format for the next recording session.
- int mergMicrophoneGetAudioFormat(void)
- {
- return s_microphone_audio_format;
- }
- //////////
- // Set the target bit rate for the next recording session. This is used
- // by codecs such as MP3 and MP4. As the input is specified as 'integer',
- // we need to verify it is a positive number.
- void mergMicrophoneSetTargetBitRate(int p_bit_rate)
- {
- if (p_bit_rate <= 0)
- {
- LCExceptionRaise("target bit rate must be a positive number");
- return;
- }
-
- s_microphone_target_bit_rate = p_bit_rate;
- }
- // Return the target bit-rate for the next recording session.
- int mergMicrophoneGetTargetBitRate(void)
- {
- return s_microphone_target_bit_rate;
- }
- //////////
- // Set the audio quality to use when encoding in the next recording session.
- // The input parameter is tied to an enum with values taken directly from the
- // AVAudioQuality* constants, thus no further checking is required.
- void mergMicrophoneSetAudioQuality(int p_audio_quality)
- {
- s_microphone_audio_quality = p_audio_quality;
- }
- // Return the audio quality to use when encoding for the next recording sesssion.
- int mergMicrophoneGetAudioQuality(void)
- {
- return s_microphone_audio_quality;
- }
- ////////////////////////////////////////////////////////////////////////////////
- // This obj-c class is used as the delegate of the AVAudioRecorder we create for
- // a recording session.
- @interface com_merg_microphone_MicrophoneDelegate : NSObject <AVAudioRecorderDelegate>
- {
- // The wait object used to block until recording has completed at certain
- // points.
- LCWaitRef m_wait;
-
- // The error object generated by recording, should something go wrong.
- NSError *m_error;
- }
- // Initializes the object.
- - (id)init;
- // Finalizes the object.
- - (void)dealloc;
- // Waits until either an error occurs, or recording has finished.
- - (NSError *)waitAndCheckForError;
- // The delegate method invoked upon completion by AVAudioRecorder.
- - (void)audioRecorderDidFinishRecording: (AVAudioRecorder *)recorder successfully: (BOOL)flag;
- // The delegate method invoked upon error by AVAudioRecorder.
- - (void)audioRecorderEncodeErrorDidOccur: (AVAudioRecorder *)recorder error: (NSError *)error;
- @end
- @implementation com_merg_microphone_MicrophoneDelegate
- - (id)init
- {
- // First invoke the superclass implementation.
- self = [super init];
- if (self == nil)
- return nil;
-
- // Create the wait object we will use later. Notice that we use a blocking
- // wait - meaning low-level engine operations will continue to run, but no
- // events and messages will be dispatched.
- LCWaitCreate(kLCWaitOptionBlocking, &m_wait);
-
- // We start off without an error having occured.
- m_error = nil;
-
- // Failure to return 'self' here is a bad idea...
- return self;
- }
- - (void)dealloc
- {
- // Make sure we clean up the wait object
- LCWaitRelease(m_wait);
- // Make sure we clean up any error (this is fine even if m_error is nil)
- [m_error release];
- // Now tell the superclass to clean up.
- [super dealloc];
- }
- - (NSError *)waitAndCheckForError
- {
- // Wait until we are told that recording has finished writing to the output
- // file. Note that it is perfectly possible that the wait has already been
- // broken, in which case it will just carry straight on.
- LCWaitRun(m_wait);
-
- // Return the error, if any.
- return m_error;
- }
- - (void)audioRecorderDidFinishRecording: (AVAudioRecorder *)p_recorder successfully: (BOOL)p_success
- {
- // Invoking break here will result in the wait invoked by 'waitAndCheckForError'
- // being broken, and thus control returning to the 'mergMicrophoneStopRecording'
- // handler.
- LCWaitBreak(m_wait);
- }
- - (void)audioRecorderEncodeErrorDidOccur: (AVAudioRecorder *)p_recorder error: (NSError *)p_error
- {
- // Save the error for future use. Notice we 'retain' it, this is because we do
- // not own the parameter and want to keep it around beyond the lifetime of this
- // handler call.
- m_error = p_error;
- [m_error retain];
-
- // Now break the wait.
- LCWaitBreak(m_wait);
- }
- @end
- // It is important that strict rules are followed in terms of obj-c class naming as
- // all objc-c classes sit in a flat namespace and conflicts can occur very easily if
- // generic names are used.
- // As 'com_runrev_mergmicrophone_MicrophoneDelegate' is a bit long-winded, we use
- // the 'compatibility_alias' feature to map it to a more friendly name. This is
- // like a '#define' but more robust - the association is only at the source-level,
- // when compiled the full qualified name will be used thus avoiding any naming
- // conflicts.
- @compatibility_alias MicrophoneDelegate com_merg_microphone_MicrophoneDelegate;
- // This method returns 'true' if recording is in progress. Note that this fact is tied
- // to the existence of a recorder we created, since s_microphone_recorder is only
- // non-nil if, and only if, recording is occuring.
- bool mergMicrophoneIsRecording(void)
- {
- if (s_microphone_recorder == nil)
- return false;
- return true;
- }
- // This method attempts to start recording to the given output file.
- int mergMicrophoneStartRecording(NSString *p_filename)
- {
- // We can't nest recording sessions (obviously, since there is only one audio input!).
- if (s_microphone_recorder != nil)
- return kMicrophoneStatusAlreadyRecording;
-
- // Build the settings dictionary which is used to describe the sample format and
- // audio format to use. This dictionary is initialized with the cumergnt settings
- // as determined by calls to the mergMicrophoneSet* handlers.
- NSMutableDictionary *t_settings;
- t_settings = [NSMutableDictionary dictionaryWithCapacity: 4];
- [t_settings setObject: [NSNumber numberWithDouble: s_microphone_sample_rate] forKey: AVSampleRateKey];
- [t_settings setObject: [NSNumber numberWithInt: s_microphone_channel_count] forKey: AVNumberOfChannelsKey];
- [t_settings setObject: [NSNumber numberWithInt: s_microphone_target_bit_rate] forKey: AVEncoderBitRateKey];
- [t_settings setObject: [NSNumber numberWithInt: s_microphone_audio_quality] forKey: AVEncoderAudioQualityKey];
-
- // Additional settings are determined by the audio format. A reasonable selection
- // of formats is handled here, although iOS does support more than this.
- switch(s_microphone_audio_format)
- {
- case kMicrophoneAudioFormatLinearPCM:
- [t_settings setObject: [NSNumber numberWithInt: kAudioFormatLinearPCM] forKey: AVFormatIDKey];
- [t_settings setObject: [NSNumber numberWithInt: 16] forKey: AVLinearPCMBitDepthKey];
- [t_settings setObject: [NSNumber numberWithBool: NO] forKey: AVLinearPCMIsBigEndianKey];
- [t_settings setObject: [NSNumber numberWithBool: NO] forKey: AVLinearPCMIsFloatKey];
- break;
- case kMicrophoneAudioFormatAppleLossless:
- [t_settings setObject: [NSNumber numberWithInt: kAudioFormatAppleLossless] forKey: AVFormatIDKey];
- break;
- case kMicrophoneAudioFormatMPEG4AAC:
- [t_settings setObject: [NSNumber numberWithInt: kAudioFormatMPEG4AAC] forKey: AVFormatIDKey];
- break;
- case kMicrophoneAudioFormatILBC:
- [t_settings setObject: [NSNumber numberWithInt: kAudioFormatiLBC] forKey: AVFormatIDKey];
- break;
- }
-
- // Construct a URL object with the filename. Notice that we are using a class method
- // which does not start with 'alloc' to make the NSURL object. This means the returned
- // object will be 'autoreleased' meaning we don't have to worry about freeing it later.
- NSURL *t_url;
- t_url = [NSURL fileURLWithPath: p_filename];
-
- // Now try to construct an audio recorder. This time we use alloc/init, meaning we do
- // have to be concerned about the resulting object's lifetime. Notice that we run the
- // alloc/init of the AVAudioRecorder on the system thread.
- __block NSError *t_error;
- t_error = nil;
- LCRunBlockOnSystemThread(^(void) {
- s_microphone_recorder = [[AVAudioRecorder alloc] initWithURL: t_url settings: t_settings error: &t_error];
- });
- if (s_microphone_recorder == nil)
- {
- // If an object was not created, we will have an error object to inspect.
- NSLog(@"AVAudioRecorder Error: %@ (%d)", [t_error localizedDescription], [t_error code]);
- return kMicrophoneStatusRecordingFailed;
- }
-
- // Next we make a delegate object to use then set it and start recording on the system
- // thread.
- MicrophoneDelegate *t_delegate;
- t_delegate = [[MicrophoneDelegate alloc] init];
- LCRunBlockOnSystemThread(^(void) {
- [s_microphone_recorder setDelegate: t_delegate];
- [s_microphone_recorder setMeteringEnabled:YES];
- if (![s_microphone_recorder record])
- {
- // It's unspecified what exactly happens if 'record' returns NO, indeed,
- // there seems no way of getting any further error information.
-
- // Clean up the recorder since we don't need it anymore.
- [s_microphone_recorder release];
- s_microphone_recorder = nil;
- }
- });
-
- // If the recorder is nil at this point it means starting to record failed.
- if (s_microphone_recorder == nil)
- {
- // Release the delegate since we don't need it anymore.
- [t_delegate release];
-
- return kMicrophoneStatusRecordingFailed;
- }
-
- // If we get to here then we can assume that recording at least started
- // successfully.
- return kMicrophoneStatusSuccess;
- }
- // This method stops and attempts to finish writing the cumergnt recording session.
- int mergMicrophoneStopRecording(void)
- {
- // If there is no recording session, then there is nothing to do.
- if (s_microphone_recorder == nil)
- return kMicrophoneStatusNotRecording;
-
- // Fetch the delegate object that we set when we started.
- MicrophoneDelegate *t_delegate;
- t_delegate = (MicrophoneDelegate *)[s_microphone_recorder delegate];
-
- // Request that the recorder stop recording - we do this on the system thread.
- LCRunBlockOnSystemThread(^(void) {
- [s_microphone_recorder stop];
- });
-
- // Now it is entirely possible that, at this point, writing to the output file
- // has not yet finished. Therefore, we tell the delegate to 'wait' until it gets
- // notified of completion or error.
- NSError *t_error;
- t_error = [t_delegate waitAndCheckForError];
-
- // Now that that's cleared up, we can release resources.
- LCRunBlockOnSystemThread(^(void) {
- // First we make sure the recorder has no delegate (as are about to get rid of it!)
- [s_microphone_recorder setDelegate: nil];
-
- // Now release the recorder...
- [s_microphone_recorder release];
- s_microphone_recorder = nil;
- });
-
- // ... and our delegate.
- [t_delegate release];
-
- // ... and check for an error.
- if (t_error != nil)
- {
- NSLog(@"AVAudioRecorder Error: %@ (%d)", [t_error localizedDescription], [t_error code]);
- return kMicrophoneStatusRecordingFailed;
- }
-
- // Finally, if we get to this point we should have succeeded in producing an
- // output audio file!
- return kMicrophoneStatusSuccess;
- }
- int mergMicrophoneAveragePower(int pChannel)
- {
- if (s_microphone_recorder == nil) {
- LCExceptionRaise("recording not yet started");
- return 0;
- }
-
- [s_microphone_recorder updateMeters];
- return [s_microphone_recorder averagePowerForChannel:pChannel];
- }
- int mergMicrophonePeakPower(int pChannel)
- {
- if (s_microphone_recorder == nil) {
- LCExceptionRaise("recording not yet started");
- return 0;
- }
-
- [s_microphone_recorder updateMeters];
- return [s_microphone_recorder peakPowerForChannel:pChannel];
- }
- ////////////////////////////////////////////////////////////////////////////////
- // This handler is called when the external is loaded. We use it to set all our
- // static locals to default values.
- bool mergMicrophoneStartup(void)
- {
- s_microphone_recorder = nil;
- s_microphone_channel_count = 2;
- s_microphone_sample_rate = 44100;
- s_microphone_audio_format = kMicrophoneAudioFormatLinearPCM;
- s_microphone_audio_quality = AVAudioQualityHigh;
- s_microphone_target_bit_rate = 65536;
- #if TARGET_OS_IPHONE
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
- if ([[[UIDevice currentDevice] systemVersion] compare:@"6.0" options:NSNumericSearch] != NSOrderedAscending)
- return [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil] == YES;
- #endif
- return [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil] == YES;
- #endif
- return true;
- }
- // Should we have anything to clean up, we should do it here.
- void mergMicrophoneShutdown(void)
- {
- }
- ////////////////////////////////////////////////////////////////////////////////