We have an application, which is basically a UIWebView for a web application with great javascript support. The requirement that we are faced with is the ability to play the sound to the user, and then record the user, play this record for confirmation, and then send the audio to the server. This works on Chrome, Android and other platforms because this feature is built into the browser. No required code needed.
Unfortunately, the iOS webpage (iOS 8/9) does not have the ability to record sound.
The first workaround we tried was recording audio using AudioQueue and transferring data (LinearPCM 16bit) to JS AudioNode so that the web application could process iOS sound exactly like other platforms. This went so far as to allow us to transfer audio to JS, but the application will eventually fail with a memory access error or the javascript side simply cannot keep up with the data being sent.
The next idea was to save the audio recording to a file and send partial JS audio data for visual feedback - the main audio visualizer displayed only during recording.
The audio recording is recorded and played back in the WAVE file, as Linear PCM has a 16-bit format. We are stuck in the JS visualizer. The unsigned linear PCM is expected to be 8 bits, so I added a conversion step that might be wrong. I tried several different methods, mainly found on the Internet, and did not find what works, which makes me think that something else didnโt or didnโt work out before we even move on to the conversion step.
Since I donโt know what and where exactly the problem is, I will give the code below for sound recording and playback classes. Any suggestions can be resolved to solve this problem or somehow.
One of my ideas was to record in a different format (CAF) using different format flags. Considering the values โโthat are produced, unsigned 16-bit ints approach the maximum value. I rarely see something above +/- 1000. Is it because of the kLinearPCMFormatFlagIsPacked flag in AudioStreamPacketDescription? Removing this flag causes the audio file to not be created due to an invalid format. Maybe switching to CAF will work, but we need to convert to WAVE before sending the sound back to our server.
Or maybe my conversion from signed 16-bit to unsigned 8bit is wrong? I also tried bithitting and casting. The only difference is that with this conversion all sound values โโare compressed to 125 and 130. The bit offset and casting are changed to 0-5 and 250-255. This does not really solve any problems on the part of JS.
The next step is, instead of transferring data to JS, run it through the FFT function and create values โโthat will be used directly by JS for the audio visualizer. I would rather find out if I did something clearly wrong before going in that direction.
AQRecorder.h - EDIT: Updated Audio Format for LinearPCM 32bit Float.
#ifndef AQRecorder_h #define AQRecorder_h #import <AudioToolbox/AudioToolbox.h> #define NUM_BUFFERS 3 #define AUDIO_DATA_TYPE_FORMAT float #define JS_AUDIO_DATA_SIZE 32 @interface AQRecorder : NSObject { AudioStreamBasicDescription mDataFormat; AudioQueueRef mQueue; AudioQueueBufferRef mBuffers[ NUM_BUFFERS ]; AudioFileID mAudioFile; UInt32 bufferByteSize; SInt64 mCurrentPacket; bool mIsRunning; } - (void)setupAudioFormat; - (void)startRecording; - (void)stopRecording; - (void)processSamplesForJS:(UInt32)audioDataBytesCapacity audioData:(void *)audioData; - (Boolean)isRunning; @end #endif
AQRecorder.m - EDIT: Updated Audio Format for LinearPCM 32bit Float. Added FFT step to processSamplesForJS instead of sending direct audio data.
Play audio and record classes contrller Audio.h
#ifndef Audio_h #define Audio_h #import <AVFoundation/AVFoundation.h> #import "AQRecorder.h" @interface Audio : NSObject <AVAudioPlayerDelegate> { AQRecorder* recorder; AVAudioPlayer* player; bool mIsSetup; bool mIsRecording; bool mIsPlaying; } - (void)setupAudio; - (void)startRecording; - (void)stopRecording; - (void)startPlaying; - (void)stopPlaying; - (Boolean)isRecording; - (Boolean)isPlaying; - (NSString *) getAudioDataBase64String; @end #endif
Audio.m
#import "Audio.h" #import <AudioToolbox/AudioToolbox.h> #import "JSMonitor.h" @implementation Audio - (void)setupAudio { NSLog(@"Audio->setupAudio"); AVAudioSession *session = [AVAudioSession sharedInstance]; NSError * error; [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; [session setActive:YES error:nil]; recorder = [[AQRecorder alloc] init]; mIsSetup = YES; } - (void)startRecording { NSLog(@"Audio->startRecording"); if ( !mIsSetup ) { [self setupAudio]; } if ( mIsRecording ) { return; } if ( [recorder isRunning] == NO ) { [recorder startRecording]; } mIsRecording = [recorder isRunning]; } - (void)stopRecording { NSLog(@"Audio->stopRecording"); [recorder stopRecording]; mIsRecording = [recorder isRunning]; [[JSMonitor shared] sendAudioInputStoppedEvent]; } - (void)startPlaying { if ( mIsPlaying ) { return; } mIsPlaying = YES; NSLog(@"Audio->startPlaying"); NSError* error = nil; NSString *recordFile = [NSTemporaryDirectory() stringByAppendingPathComponent: @"AudioFile.wav"]; player = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:recordFile] error:&error]; if ( error ) { NSLog(@"AVAudioPlayer failed :: %@", error); } player.delegate = self; [player play]; } - (void)stopPlaying { NSLog(@"Audio->stopPlaying"); [player stop]; mIsPlaying = NO; [[JSMonitor shared] sendAudioPlaybackCompleteEvent]; } - (NSString *) getAudioDataBase64String { NSString *recordFile = [NSTemporaryDirectory() stringByAppendingPathComponent: @"AudioFile.wav"]; NSError* error = nil; NSData *fileData = [NSData dataWithContentsOfFile:recordFile options: 0 error: &error]; if ( fileData == nil ) { NSLog(@"Failed to read file, error %@", error); return @"DATAENCODINGFAILED"; } else { return [fileData base64EncodedStringWithOptions:0]; } } - (Boolean)isRecording { return mIsRecording; } - (Boolean)isPlaying { return mIsPlaying; } - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { NSLog(@"Audio->audioPlayerDidFinishPlaying: %i", flag); mIsPlaying = NO; [[JSMonitor shared] sendAudioPlaybackCompleteEvent]; } - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error { NSLog(@"Audio->audioPlayerDecodeErrorDidOccur: %@", error.localizedFailureReason); mIsPlaying = NO; [[JSMonitor shared] sendAudioPlaybackCompleteEvent]; } @end
The JSMonitor class is the bridge between the UIWebView javascriptcore and native code. I do not turn it on because it does nothing for audio other than transferring data / calls between these classes and JSCore.
EDIT
The audio format has changed to LinearPCM Float 32bit. Instead of sending audio, it is sent via the FFT function, and the dB values โโare averaged and sent instead.