2015-11-12 11 views
5

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.

+0

Hai controllato i valori che ricevi dall'obiettivo-c o solo alla fine in UIWebView? –

+0

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? –

+0

perché AUDIO_DATA_TYPE_FORMAT * v è un puntatore? Non dovrebbe essere un valore di esempio? –

risposta

0

Core Audio è un problema con cui lavorare. Fortunatamente, AVFoundation fornisce AVAudioRecorder per registrare video e ti dà anche accesso alla potenza audio media e di picco che puoi inviare al tuo JavaScript per aggiornare il tuo visualizzatore dell'interfaccia utente. Da the docs:

Un'istanza della classe AVAudioRecorder, chiamato un registratore audio, fornisce capacità di registrazione audio nell'applicazione. L'utilizzo di un registratore audio è possibile:

  • Registra fino a quando l'utente interrompe la registrazione
  • record per una durata specifica
  • pausa e riprendere una registrazione
  • Ottenere input dei dati a livello di audio che è possibile utilizzare fornire livello dosaggio

This Stack Overflow question ha un esempio di come utilizzare AVAudioRecorder.

+0

Il visualizzatore JS ha bisogno di più del picco e della potenza media per funzionare. Posso anche ottenere quei valori da AudioQueue. – Simurr

Problemi correlati