/mergMicrophone/mergMicrophone.mm

https://bitbucket.org/mgoulding/mergmicrophone · Objective C++ · 515 lines · 250 code · 77 blank · 188 comment · 26 complexity · 7d217bda0c5d85c90eb56179e6c88988 MD5 · raw file

  1. /*
  2. Copyright (C) 2011 - 2018 LiveCode Ltd.
  3. This file is part of mergMicrophone.
  4. mergMicrophone is free software; you can redistribute it and/or modify it under
  5. the terms of the GNU General Public License v3 as published by the Free
  6. Software Foundation.
  7. mergMicrophone is distributed in the hope that it will be useful, but WITHOUT ANY
  8. WARRANTY; without even the implied warranty of MERCHANTABILITY or
  9. FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  10. for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with mergMicrophone. If not see <http://www.gnu.org/licenses/>. */
  13. ////////////////////////////////////////////////////////////////////////////////
  14. //
  15. // Source File:
  16. // mergmicrophone.mm
  17. //
  18. // Description:
  19. // This file contains the implementation of 'mergmicrophone' - a basic wrapper
  20. // around the iOS AVAudioRecorder class, enabling audio recording to be
  21. // performed in iOS applications. It is provided as part of the LiveCode
  22. // SDK.
  23. //
  24. // Changes:
  25. // 2011-06-09 MW Initial release.
  26. // 2011-06-14 MW Changed LCWaitDestroy to LCWaitRelease inline with 'wait'
  27. // object improvements.
  28. // Made sure the AVAudioRecorder's reference to the delegate is
  29. // removed before release - to ensure any hanging references to
  30. // the recorder don't invoke the delegate.
  31. // 2012-08-07 MW Updated to execute system calls on comergct thread.
  32. // 2012-08-30 MW Replaced var pointers with __block storage var to simplify
  33. // block usage.
  34. //
  35. ////////////////////////////////////////////////////////////////////////////////
  36. // This external implements a basic wrapper around the iOS AVAudioRecorder
  37. // class. It allows recording audio to a specified file with a reasonable degree
  38. // of control over the sampling and resulting output format.
  39. //
  40. // The implementation is split (essentially) into two parts. The first part are
  41. // the external handlers that control the options to be used for the next
  42. // recording session. These are stored in the following file-local variables:
  43. // s_microphone_channel_count
  44. // s_microphone_sample_rate
  45. // s_microphone_audio_format
  46. // s_microphone_audio_quality
  47. // s_microphone_target_bit_rate
  48. //
  49. // These options are used in 'StartRecording' to create and set up an instance
  50. // of AVAudioRecorder to which a custom delegate object (MicrophoneDelegate)
  51. // is attached. The AVAudioRecorder instance (and associated delegate) persist
  52. // until 'StopRecording' is invoked.
  53. //
  54. // Of particular interest (as an example) is the use of the LCWait abstraction
  55. // provided the LC API to handle the 'background completion' that occurs when
  56. // audio recording is stopped. This simplifies interaction with script as it
  57. // means that 'stop recording' blocks until the audio file is ready for use.
  58. // We use the usual objective-c datatypes so require Foundation.
  59. #import <Foundation/Foundation.h>
  60. // We also use AVAudioRecorder so need the AVFoundation headers.
  61. #import <AVFoundation/AVFoundation.h>
  62. // We use some of the LiveCode APIs so require that header too.
  63. #include <LiveCode.h>
  64. ////////////////////////////////////////////////////////////////////////////////
  65. // These enum constants comergspond to the status values as specified as
  66. // recording-status enum in the spec file.
  67. enum
  68. {
  69. kMicrophoneStatusSuccess = 0,
  70. kMicrophoneStatusAlreadyRecording = 1,
  71. kMicrophoneStatusNotRecording = 2,
  72. kMicrophoneStatusRecordingFailed = 3,
  73. };
  74. // These enum constants comergspond to the possible values specified as
  75. // audio-format-enum in the spec file.
  76. enum
  77. {
  78. kMicrophoneAudioFormatLinearPCM = 0,
  79. kMicrophoneAudioFormatAppleLossless = 1,
  80. kMicrophoneAudioFormatMPEG4AAC = 2,
  81. kMicrophoneAudioFormatILBC = 4,
  82. };
  83. ////////////////////////////////////////////////////////////////////////////////
  84. // The number of channels to record - either 1 (mono) or 2 (stereo).
  85. static int s_microphone_channel_count;
  86. // The sample rate (in Hertz) to record at
  87. static double s_microphone_sample_rate;
  88. // The audio format to output (see above constants)
  89. static int s_microphone_audio_format;
  90. // The audio quality to output (comergsponds to AVAudioQuality constants)
  91. static int s_microphone_audio_quality;
  92. // The target bit rate in bits per second to aim for
  93. static int s_microphone_target_bit_rate;
  94. // The AVAudioRecorder object cumergntly recording, or nil if recording is not
  95. // taking place.
  96. static AVAudioRecorder *s_microphone_recorder;
  97. ////////////////////////////////////////////////////////////////////////////////
  98. // Returns 'true' if the device has a microphone, 'false' otherwise.
  99. bool mergMicrophoneIsAvailable(void)
  100. {
  101. #if TARGET_OS_IPHONE
  102. return [[AVAudioSession sharedInstance] inputIsAvailable] == YES;
  103. #endif
  104. return true;
  105. }
  106. //////////
  107. // Set the number of channels to use for the next recording session. The input
  108. // parameter is an enum which contains 1 or 2, thus no further checking is
  109. // required.
  110. void mergMicrophoneSetNumberOfChannels(int p_channel_count)
  111. {
  112. s_microphone_channel_count = p_channel_count;
  113. }
  114. // Return the number of channels which will be used for the next recording
  115. // session.
  116. int mergMicrophoneGetNumberOfChannels(void)
  117. {
  118. return s_microphone_channel_count;
  119. }
  120. //////////
  121. // Set the sample rate to use for the next recording session. As the input
  122. // is specified as 'integer' we need to verify it is a positive number.
  123. void mergMicrophoneSetSampleRate(double p_sample_rate)
  124. {
  125. if (p_sample_rate <= 0)
  126. {
  127. LCExceptionRaise("sample rate must be a positive number");
  128. return;
  129. }
  130. s_microphone_sample_rate = p_sample_rate;
  131. }
  132. // Return the sample rate to use for the next recording session.
  133. double mergMicrophoneGetSampleRate(void)
  134. {
  135. return s_microphone_sample_rate;
  136. }
  137. //////////
  138. // Set the audio format to use for the next recording session. The input
  139. // parameter is an enum which comergsponds to one of the constants listed
  140. // above, thus no further checking is required.
  141. void mergMicrophoneSetAudioFormat(int p_audio_format)
  142. {
  143. s_microphone_audio_format = p_audio_format;
  144. }
  145. // Return the audio format for the next recording session.
  146. int mergMicrophoneGetAudioFormat(void)
  147. {
  148. return s_microphone_audio_format;
  149. }
  150. //////////
  151. // Set the target bit rate for the next recording session. This is used
  152. // by codecs such as MP3 and MP4. As the input is specified as 'integer',
  153. // we need to verify it is a positive number.
  154. void mergMicrophoneSetTargetBitRate(int p_bit_rate)
  155. {
  156. if (p_bit_rate <= 0)
  157. {
  158. LCExceptionRaise("target bit rate must be a positive number");
  159. return;
  160. }
  161. s_microphone_target_bit_rate = p_bit_rate;
  162. }
  163. // Return the target bit-rate for the next recording session.
  164. int mergMicrophoneGetTargetBitRate(void)
  165. {
  166. return s_microphone_target_bit_rate;
  167. }
  168. //////////
  169. // Set the audio quality to use when encoding in the next recording session.
  170. // The input parameter is tied to an enum with values taken directly from the
  171. // AVAudioQuality* constants, thus no further checking is required.
  172. void mergMicrophoneSetAudioQuality(int p_audio_quality)
  173. {
  174. s_microphone_audio_quality = p_audio_quality;
  175. }
  176. // Return the audio quality to use when encoding for the next recording sesssion.
  177. int mergMicrophoneGetAudioQuality(void)
  178. {
  179. return s_microphone_audio_quality;
  180. }
  181. ////////////////////////////////////////////////////////////////////////////////
  182. // This obj-c class is used as the delegate of the AVAudioRecorder we create for
  183. // a recording session.
  184. @interface com_merg_microphone_MicrophoneDelegate : NSObject <AVAudioRecorderDelegate>
  185. {
  186. // The wait object used to block until recording has completed at certain
  187. // points.
  188. LCWaitRef m_wait;
  189. // The error object generated by recording, should something go wrong.
  190. NSError *m_error;
  191. }
  192. // Initializes the object.
  193. - (id)init;
  194. // Finalizes the object.
  195. - (void)dealloc;
  196. // Waits until either an error occurs, or recording has finished.
  197. - (NSError *)waitAndCheckForError;
  198. // The delegate method invoked upon completion by AVAudioRecorder.
  199. - (void)audioRecorderDidFinishRecording: (AVAudioRecorder *)recorder successfully: (BOOL)flag;
  200. // The delegate method invoked upon error by AVAudioRecorder.
  201. - (void)audioRecorderEncodeErrorDidOccur: (AVAudioRecorder *)recorder error: (NSError *)error;
  202. @end
  203. @implementation com_merg_microphone_MicrophoneDelegate
  204. - (id)init
  205. {
  206. // First invoke the superclass implementation.
  207. self = [super init];
  208. if (self == nil)
  209. return nil;
  210. // Create the wait object we will use later. Notice that we use a blocking
  211. // wait - meaning low-level engine operations will continue to run, but no
  212. // events and messages will be dispatched.
  213. LCWaitCreate(kLCWaitOptionBlocking, &m_wait);
  214. // We start off without an error having occured.
  215. m_error = nil;
  216. // Failure to return 'self' here is a bad idea...
  217. return self;
  218. }
  219. - (void)dealloc
  220. {
  221. // Make sure we clean up the wait object
  222. LCWaitRelease(m_wait);
  223. // Make sure we clean up any error (this is fine even if m_error is nil)
  224. [m_error release];
  225. // Now tell the superclass to clean up.
  226. [super dealloc];
  227. }
  228. - (NSError *)waitAndCheckForError
  229. {
  230. // Wait until we are told that recording has finished writing to the output
  231. // file. Note that it is perfectly possible that the wait has already been
  232. // broken, in which case it will just carry straight on.
  233. LCWaitRun(m_wait);
  234. // Return the error, if any.
  235. return m_error;
  236. }
  237. - (void)audioRecorderDidFinishRecording: (AVAudioRecorder *)p_recorder successfully: (BOOL)p_success
  238. {
  239. // Invoking break here will result in the wait invoked by 'waitAndCheckForError'
  240. // being broken, and thus control returning to the 'mergMicrophoneStopRecording'
  241. // handler.
  242. LCWaitBreak(m_wait);
  243. }
  244. - (void)audioRecorderEncodeErrorDidOccur: (AVAudioRecorder *)p_recorder error: (NSError *)p_error
  245. {
  246. // Save the error for future use. Notice we 'retain' it, this is because we do
  247. // not own the parameter and want to keep it around beyond the lifetime of this
  248. // handler call.
  249. m_error = p_error;
  250. [m_error retain];
  251. // Now break the wait.
  252. LCWaitBreak(m_wait);
  253. }
  254. @end
  255. // It is important that strict rules are followed in terms of obj-c class naming as
  256. // all objc-c classes sit in a flat namespace and conflicts can occur very easily if
  257. // generic names are used.
  258. // As 'com_runrev_mergmicrophone_MicrophoneDelegate' is a bit long-winded, we use
  259. // the 'compatibility_alias' feature to map it to a more friendly name. This is
  260. // like a '#define' but more robust - the association is only at the source-level,
  261. // when compiled the full qualified name will be used thus avoiding any naming
  262. // conflicts.
  263. @compatibility_alias MicrophoneDelegate com_merg_microphone_MicrophoneDelegate;
  264. // This method returns 'true' if recording is in progress. Note that this fact is tied
  265. // to the existence of a recorder we created, since s_microphone_recorder is only
  266. // non-nil if, and only if, recording is occuring.
  267. bool mergMicrophoneIsRecording(void)
  268. {
  269. if (s_microphone_recorder == nil)
  270. return false;
  271. return true;
  272. }
  273. // This method attempts to start recording to the given output file.
  274. int mergMicrophoneStartRecording(NSString *p_filename)
  275. {
  276. // We can't nest recording sessions (obviously, since there is only one audio input!).
  277. if (s_microphone_recorder != nil)
  278. return kMicrophoneStatusAlreadyRecording;
  279. // Build the settings dictionary which is used to describe the sample format and
  280. // audio format to use. This dictionary is initialized with the cumergnt settings
  281. // as determined by calls to the mergMicrophoneSet* handlers.
  282. NSMutableDictionary *t_settings;
  283. t_settings = [NSMutableDictionary dictionaryWithCapacity: 4];
  284. [t_settings setObject: [NSNumber numberWithDouble: s_microphone_sample_rate] forKey: AVSampleRateKey];
  285. [t_settings setObject: [NSNumber numberWithInt: s_microphone_channel_count] forKey: AVNumberOfChannelsKey];
  286. [t_settings setObject: [NSNumber numberWithInt: s_microphone_target_bit_rate] forKey: AVEncoderBitRateKey];
  287. [t_settings setObject: [NSNumber numberWithInt: s_microphone_audio_quality] forKey: AVEncoderAudioQualityKey];
  288. // Additional settings are determined by the audio format. A reasonable selection
  289. // of formats is handled here, although iOS does support more than this.
  290. switch(s_microphone_audio_format)
  291. {
  292. case kMicrophoneAudioFormatLinearPCM:
  293. [t_settings setObject: [NSNumber numberWithInt: kAudioFormatLinearPCM] forKey: AVFormatIDKey];
  294. [t_settings setObject: [NSNumber numberWithInt: 16] forKey: AVLinearPCMBitDepthKey];
  295. [t_settings setObject: [NSNumber numberWithBool: NO] forKey: AVLinearPCMIsBigEndianKey];
  296. [t_settings setObject: [NSNumber numberWithBool: NO] forKey: AVLinearPCMIsFloatKey];
  297. break;
  298. case kMicrophoneAudioFormatAppleLossless:
  299. [t_settings setObject: [NSNumber numberWithInt: kAudioFormatAppleLossless] forKey: AVFormatIDKey];
  300. break;
  301. case kMicrophoneAudioFormatMPEG4AAC:
  302. [t_settings setObject: [NSNumber numberWithInt: kAudioFormatMPEG4AAC] forKey: AVFormatIDKey];
  303. break;
  304. case kMicrophoneAudioFormatILBC:
  305. [t_settings setObject: [NSNumber numberWithInt: kAudioFormatiLBC] forKey: AVFormatIDKey];
  306. break;
  307. }
  308. // Construct a URL object with the filename. Notice that we are using a class method
  309. // which does not start with 'alloc' to make the NSURL object. This means the returned
  310. // object will be 'autoreleased' meaning we don't have to worry about freeing it later.
  311. NSURL *t_url;
  312. t_url = [NSURL fileURLWithPath: p_filename];
  313. // Now try to construct an audio recorder. This time we use alloc/init, meaning we do
  314. // have to be concerned about the resulting object's lifetime. Notice that we run the
  315. // alloc/init of the AVAudioRecorder on the system thread.
  316. __block NSError *t_error;
  317. t_error = nil;
  318. LCRunBlockOnSystemThread(^(void) {
  319. s_microphone_recorder = [[AVAudioRecorder alloc] initWithURL: t_url settings: t_settings error: &t_error];
  320. });
  321. if (s_microphone_recorder == nil)
  322. {
  323. // If an object was not created, we will have an error object to inspect.
  324. NSLog(@"AVAudioRecorder Error: %@ (%d)", [t_error localizedDescription], [t_error code]);
  325. return kMicrophoneStatusRecordingFailed;
  326. }
  327. // Next we make a delegate object to use then set it and start recording on the system
  328. // thread.
  329. MicrophoneDelegate *t_delegate;
  330. t_delegate = [[MicrophoneDelegate alloc] init];
  331. LCRunBlockOnSystemThread(^(void) {
  332. [s_microphone_recorder setDelegate: t_delegate];
  333. [s_microphone_recorder setMeteringEnabled:YES];
  334. if (![s_microphone_recorder record])
  335. {
  336. // It's unspecified what exactly happens if 'record' returns NO, indeed,
  337. // there seems no way of getting any further error information.
  338. // Clean up the recorder since we don't need it anymore.
  339. [s_microphone_recorder release];
  340. s_microphone_recorder = nil;
  341. }
  342. });
  343. // If the recorder is nil at this point it means starting to record failed.
  344. if (s_microphone_recorder == nil)
  345. {
  346. // Release the delegate since we don't need it anymore.
  347. [t_delegate release];
  348. return kMicrophoneStatusRecordingFailed;
  349. }
  350. // If we get to here then we can assume that recording at least started
  351. // successfully.
  352. return kMicrophoneStatusSuccess;
  353. }
  354. // This method stops and attempts to finish writing the cumergnt recording session.
  355. int mergMicrophoneStopRecording(void)
  356. {
  357. // If there is no recording session, then there is nothing to do.
  358. if (s_microphone_recorder == nil)
  359. return kMicrophoneStatusNotRecording;
  360. // Fetch the delegate object that we set when we started.
  361. MicrophoneDelegate *t_delegate;
  362. t_delegate = (MicrophoneDelegate *)[s_microphone_recorder delegate];
  363. // Request that the recorder stop recording - we do this on the system thread.
  364. LCRunBlockOnSystemThread(^(void) {
  365. [s_microphone_recorder stop];
  366. });
  367. // Now it is entirely possible that, at this point, writing to the output file
  368. // has not yet finished. Therefore, we tell the delegate to 'wait' until it gets
  369. // notified of completion or error.
  370. NSError *t_error;
  371. t_error = [t_delegate waitAndCheckForError];
  372. // Now that that's cleared up, we can release resources.
  373. LCRunBlockOnSystemThread(^(void) {
  374. // First we make sure the recorder has no delegate (as are about to get rid of it!)
  375. [s_microphone_recorder setDelegate: nil];
  376. // Now release the recorder...
  377. [s_microphone_recorder release];
  378. s_microphone_recorder = nil;
  379. });
  380. // ... and our delegate.
  381. [t_delegate release];
  382. // ... and check for an error.
  383. if (t_error != nil)
  384. {
  385. NSLog(@"AVAudioRecorder Error: %@ (%d)", [t_error localizedDescription], [t_error code]);
  386. return kMicrophoneStatusRecordingFailed;
  387. }
  388. // Finally, if we get to this point we should have succeeded in producing an
  389. // output audio file!
  390. return kMicrophoneStatusSuccess;
  391. }
  392. int mergMicrophoneAveragePower(int pChannel)
  393. {
  394. if (s_microphone_recorder == nil) {
  395. LCExceptionRaise("recording not yet started");
  396. return 0;
  397. }
  398. [s_microphone_recorder updateMeters];
  399. return [s_microphone_recorder averagePowerForChannel:pChannel];
  400. }
  401. int mergMicrophonePeakPower(int pChannel)
  402. {
  403. if (s_microphone_recorder == nil) {
  404. LCExceptionRaise("recording not yet started");
  405. return 0;
  406. }
  407. [s_microphone_recorder updateMeters];
  408. return [s_microphone_recorder peakPowerForChannel:pChannel];
  409. }
  410. ////////////////////////////////////////////////////////////////////////////////
  411. // This handler is called when the external is loaded. We use it to set all our
  412. // static locals to default values.
  413. bool mergMicrophoneStartup(void)
  414. {
  415. s_microphone_recorder = nil;
  416. s_microphone_channel_count = 2;
  417. s_microphone_sample_rate = 44100;
  418. s_microphone_audio_format = kMicrophoneAudioFormatLinearPCM;
  419. s_microphone_audio_quality = AVAudioQualityHigh;
  420. s_microphone_target_bit_rate = 65536;
  421. #if TARGET_OS_IPHONE
  422. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
  423. if ([[[UIDevice currentDevice] systemVersion] compare:@"6.0" options:NSNumericSearch] != NSOrderedAscending)
  424. return [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil] == YES;
  425. #endif
  426. return [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil] == YES;
  427. #endif
  428. return true;
  429. }
  430. // Should we have anything to clean up, we should do it here.
  431. void mergMicrophoneShutdown(void)
  432. {
  433. }
  434. ////////////////////////////////////////////////////////////////////////////////