mergmicrophone /mergMicrophone/mergMicrophone.mm

Language Objective C++ Lines 546
MD5 Hash a3fba43199d04d764ef6a8015361bba4
Repository https://bitbucket.org/mgoulding/mergmicrophone.git View Raw File View Project SPDX
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
//          Portions copyright MERGoulding 2012.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

////////////////////////////////////////////////////////////////////////////////
//
// Copyright Š 2011 RunRev Ltd.
//
// This software is provided 'as-is', without any warranties, express or
// implied, including without limitation the implied warranties of non-
// infringement, merchantability and fitness for a particular purpose, regarding
// this software or its use and operation along or in combination with your
// products.
//
// In no event shall RunRev 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 this software, however caused and whether under theory
// of contract, tort (including negligence), strict liability or otherwise, even
// if RunRev has been advised of the possibility of such damage.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and distribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not claim
//    you wrote the original software.
// 2. Altered source versions must be plainly marked as such, and must not be
//    misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
// 4. Any distributed altered source versions, or distributedbinaries resulting
//    from altered source versions when not part of a compiled application must
//    not:
//      i.  Use the 3 letter prefixes 'merg' or 'rev' for any
//          named entities imported by a host application
//          including (but not limited to) external names and
//          external handlers.
//      ii. Use the 11 letter prefix 'com_runrev_' for any
//          named entities imported by a host application
//          including (but not limited to) objective-c classes,
//          objective-c protocols, objective-c categories and
//          objective-c category members.
//
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
//
// 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)
{
}

////////////////////////////////////////////////////////////////////////////////
Back to Top