Abbiamo un'app che è principalmente UIWebView per un'applicazione web basata su javascript. Il requisito che abbiamo incontrato è la possibilità di riprodurre audio per l'utente e quindi registrare l'utente, riprodurre quella registrazione per conferma e quindi inviare l'audio a un server. Funziona su Chrome, Android e altre piattaforme perché tale abilità è integrata nel browser. Nessun codice nativo richiesto.Registrazione audio e trasferimento dei dati a UIWebView (JavascriptCore) su iOS 8/9
Purtroppo, la vista Web iOS (iOS 8/9) non ha la capacità di registrare l'audio.
La prima soluzione abbiamo provato stava registrando l'audio con un AudioQueue e passando i dati (16 bit) per LinearPCM un JS AudioNode così l'applicazione web potrebbe elaborare l'audio esattamente come IOS altre piattaforme. Questo arrivò a un punto in cui potevamo trasmettere l'audio a JS, ma l'app finì per bloccarsi con un brutto errore di accesso alla memoria o il lato javascript non riuscì a tenere il passo con i dati inviati.
L'idea successiva era di salvare la registrazione audio in un file e inviare dati audio parziali a JS per il feedback visivo, un visualizzatore audio di base visualizzato solo durante la registrazione.
L'audio registra e riproduce fino a un file WAVE come PCM lineare firmato a 16 bit. Il visualizzatore JS è dove siamo bloccati. Si aspetta Linear PCM senza segno a 8 bit, quindi ho aggiunto un passaggio di conversione che potrebbe essere sbagliato. Ho provato diversi modi, per lo più reperibili online, e non ne ho trovato uno che funzioni, il che mi fa pensare che qualcos'altro sia sbagliato o mancante prima ancora di arrivare alla fase di conversione.
Poiché non so quale o dove si trova esattamente il problema, eseguirò il dump del codice seguente per le lezioni di registrazione e riproduzione audio. Qualsiasi suggerimento sarebbe il benvenuto per risolvere, o aggirare in qualche modo, questo problema.
Un'idea che avevo era di registrare in un formato diverso (CAF) utilizzando bandiere di formato diverso. Guardando i valori che vengono prodotti, non quelli a 16 bit con segno vengono addirittura vicini al valore massimo. Raramente vedo qualcosa sopra +/- 1000. È a causa del flag kLinearPCMFormatFlagIsPacked in AudioStreamPacketDescription? Rimuovendo quel flag, il file audio non può essere creato a causa di un formato non valido. Forse il passaggio a CAF funzionerebbe, ma è necessario convertire in WAVE prima di inviare l'audio al nostro server.
O forse la mia conversione da 16 bit a 8 bit senza segno è errata? Ho anche provato il bithifting e il casting. L'unica differenza è che con questa conversione tutti i valori audio vengono compressi tra 125 e 130. Lo spostamento e il cambio di bit cambiano a 0-5 e 250-255. Questo in realtà non risolve alcun problema sul lato JS.
Il prossimo passo sarebbe, invece di passare i dati a JS, eseguirlo attraverso una funzione FFT e produrre valori da utilizzare direttamente da JS per il visualizzatore audio. Preferirei capire se ho fatto qualcosa di chiaramente sbagliato prima di andare in quella direzione.
AQRecorder.h - EDIT: formato audio aggiornato a LinearPCM 32 bit 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: formato audio aggiornato a LinearPCM 32 bit Float. Aggiunto passo FFT in processSamplesForJS invece di inviare direttamente i dati audio.
#import <AVFoundation/AVFoundation.h>
#import "AQRecorder.h"
#import "JSMonitor.h"
@implementation AQRecorder
void AudioQueueCallback(void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription* inPacketDescs)
{
AQRecorder *aqr = (__bridge AQRecorder *)inUserData;
if ([aqr isRunning])
{
if (inNumberPacketDescriptions > 0)
{
AudioFileWritePackets(aqr->mAudioFile, FALSE, inBuffer->mAudioDataByteSize, inPacketDescs, aqr->mCurrentPacket, &inNumberPacketDescriptions, inBuffer->mAudioData);
aqr->mCurrentPacket += inNumberPacketDescriptions;
[aqr processSamplesForJS:inBuffer->mAudioDataBytesCapacity audioData:inBuffer->mAudioData];
}
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
}
}
- (void)debugDataFormat
{
NSLog(@"format=%i, sampleRate=%f, channels=%i, flags=%i, BPC=%i, BPF=%i", mDataFormat.mFormatID, mDataFormat.mSampleRate, (unsigned int)mDataFormat.mChannelsPerFrame, mDataFormat.mFormatFlags, mDataFormat.mBitsPerChannel, mDataFormat.mBytesPerFrame);
}
- (void)setupAudioFormat
{
memset(&mDataFormat, 0, sizeof(mDataFormat));
mDataFormat.mSampleRate = 44100.;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | kLinearPCMFormatFlagIsPacked;
int sampleSize = sizeof(AUDIO_DATA_TYPE_FORMAT);
mDataFormat.mBitsPerChannel = 32;
mDataFormat.mBytesPerPacket = mDataFormat.mBytesPerFrame = (mDataFormat.mBitsPerChannel/8) * mDataFormat.mChannelsPerFrame;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mReserved = 0;
[self debugDataFormat];
}
- (void)startRecording/
{
[self setupAudioFormat];
mCurrentPacket = 0;
NSString *recordFile = [NSTemporaryDirectory() stringByAppendingPathComponent: @"AudioFile.wav"];
CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)recordFile, NULL);;
OSStatus *stat =
AudioFileCreateWithURL(url, kAudioFileWAVEType, &mDataFormat, kAudioFileFlags_EraseFile, &mAudioFile);
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:stat userInfo:nil];
NSLog(@"AudioFileCreateWithURL OSStatus :: %@", error);
CFRelease(url);
bufferByteSize = 896 * mDataFormat.mBytesPerFrame;
AudioQueueNewInput(&mDataFormat, AudioQueueCallback, (__bridge void *)(self), NULL, NULL, 0, &mQueue);
for (int i = 0; i < NUM_BUFFERS; i++)
{
AudioQueueAllocateBuffer(mQueue, bufferByteSize, &mBuffers[i]);
AudioQueueEnqueueBuffer(mQueue, mBuffers[i], 0, NULL);
}
mIsRunning = true;
AudioQueueStart(mQueue, NULL);
}
- (void)stopRecording
{
mIsRunning = false;
AudioQueueStop(mQueue, false);
AudioQueueDispose(mQueue, false);
AudioFileClose(mAudioFile);
}
- (void)processSamplesForJS:(UInt32)audioDataBytesCapacity audioData:(void *)audioData
{
int sampleCount = audioDataBytesCapacity/sizeof(AUDIO_DATA_TYPE_FORMAT);
AUDIO_DATA_TYPE_FORMAT *samples = (AUDIO_DATA_TYPE_FORMAT*)audioData;
NSMutableArray *audioDataBuffer = [[NSMutableArray alloc] initWithCapacity:JS_AUDIO_DATA_SIZE];
// FFT stuff taken mostly from Apples aurioTouch example
const Float32 kAdjust0DB = 1.5849e-13;
int bufferFrames = sampleCount;
int bufferlog2 = round(log2(bufferFrames));
float fftNormFactor = (1.0/(2*bufferFrames));
FFTSetup fftSetup = vDSP_create_fftsetup(bufferlog2, kFFTRadix2);
Float32 *outReal = (Float32*) malloc((bufferFrames/2)*sizeof(Float32));
Float32 *outImaginary = (Float32*) malloc((bufferFrames/2)*sizeof(Float32));
COMPLEX_SPLIT mDspSplitComplex = { .realp = outReal, .imagp = outImaginary };
Float32 *outFFTData = (Float32*) malloc((bufferFrames/2)*sizeof(Float32));
//Generate a split complex vector from the real data
vDSP_ctoz((COMPLEX *)samples, 2, &mDspSplitComplex, 1, bufferFrames/2);
//Take the fft and scale appropriately
vDSP_fft_zrip(fftSetup, &mDspSplitComplex, 1, bufferlog2, kFFTDirection_Forward);
vDSP_vsmul(mDspSplitComplex.realp, 1, &fftNormFactor, mDspSplitComplex.realp, 1, bufferFrames/2);
vDSP_vsmul(mDspSplitComplex.imagp, 1, &fftNormFactor, mDspSplitComplex.imagp, 1, bufferFrames/2);
//Zero out the nyquist value
mDspSplitComplex.imagp[0] = 0.0;
//Convert the fft data to dB
vDSP_zvmags(&mDspSplitComplex, 1, outFFTData, 1, bufferFrames/2);
//In order to avoid taking log10 of zero, an adjusting factor is added in to make the minimum value equal -128dB
vDSP_vsadd(outFFTData, 1, &kAdjust0DB, outFFTData, 1, bufferFrames/2);
Float32 one = 1;
vDSP_vdbcon(outFFTData, 1, &one, outFFTData, 1, bufferFrames/2, 0);
// Average out FFT dB values
int grpSize = (bufferFrames/2)/32;
int c = 1;
Float32 avg = 0;
int d = 1;
for (int i = 1; i < bufferFrames/2; i++)
{
if (outFFTData[ i ] != outFFTData[ i ] || outFFTData[ i ] == INFINITY)
{ // NAN/INFINITE check
c++;
}
else
{
avg += outFFTData[ i ];
d++;
//NSLog(@"db = %f, avg = %f", outFFTData[ i ], avg);
if (++c >= grpSize)
{
uint8_t u = (uint8_t)((avg/d) + 128); //dB values seem to range from -128 to 0.
NSLog(@"%i = %i (%f)", i, u, avg);
[audioDataBuffer addObject:[NSNumber numberWithUnsignedInt:u]];
avg = 0;
c = 0;
d = 1;
}
}
}
[[JSMonitor shared] passAudioDataToJavascriptBridge:audioDataBuffer];
}
- (Boolean)isRunning
{
return mIsRunning;
}
@end
di riproduzione e registrazione audio classi 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
La classe JSMonitor è un ponte tra l'UIWebView JavaScriptCore e il codice nativo. Non lo includo perché non fa nulla per l'audio se non per passare dati/chiamate tra queste classi e JSCore.
EDIT
Il formato dell'audio è cambiato per LinearPCM Float 32 bit. Invece di inviare i dati audio, viene inviato attraverso una funzione FFT e i valori dB vengono calcolati in media e inviati.
Hai controllato i valori che ricevi dall'obiettivo-c o solo alla fine in UIWebView? –
Non stai passando i campioni direttamente. Invece, sembra passare qualcosa come una media mobile degli ultimi 32 campioni (avg + = * v; avg/= 2;). È questa la tua intenzione? –
perché AUDIO_DATA_TYPE_FORMAT * v è un puntatore? Non dovrebbe essere un valore di esempio? –