2015-11-12 10 views
5

Wir haben eine App, die meist ein UIWebView für eine stark JavaScript-basierte Web-App ist. Die Anforderung, die uns gestellt wurde, ist die Möglichkeit, dem Benutzer Audio abzuspielen und dann den Benutzer aufzunehmen, diese Aufzeichnung zur Bestätigung abzuspielen und dann das Audio an einen Server zu senden. Dies funktioniert in Chrome, Android und anderen Plattformen, da diese Fähigkeit in den Browser integriert ist. Kein systemeigener Code erforderlich.Aufnahme von Audio und Weitergabe der Daten an ein UIWebView (JavascriptCore) auf iOS 8/9

Leider, die iOS (iOS 8/9) Webansicht fehlt die Fähigkeit, Audio aufzunehmen.

Der erste Workaround, den wir ausprobiert haben, bestand darin, Audio mit einer AudioQueue aufzuzeichnen und die Daten (LinearPCM 16bit) an einen JS AudioNode weiterzuleiten, damit die Web-App das iOS-Audio genauso verarbeiten konnte wie andere Plattformen. Das ging bis zu einem Punkt, wo wir das Audio an JS übergeben konnten, aber die App würde schließlich mit einem schlechten Speicherzugriffsfehler abstürzen oder die JavaScript-Seite konnte einfach nicht mit den gesendeten Daten mithalten.

Die nächste Idee bestand darin, die Audioaufnahme in einer Datei zu speichern und Teil-Audiodaten zur visuellen Rückmeldung an JS zu senden, ein einfacher Audio-Visualizer, der nur während der Aufnahme angezeigt wird.

Die Audiodaten werden in einer WAVE-Datei aufgezeichnet und wiedergegeben, da Linear PCM 16 Bit signierte. Der JS Visualizer ist, wo wir stecken bleiben. Es erwartet Linear PCM unsigned 8bit, also fügte ich einen Umwandlungsschritt hinzu, der falsch sein kann. Ich habe verschiedene Wege ausprobiert, meist online gefunden, und habe keine gefunden, die funktionieren, was mich denken lässt, dass etwas anderes falsch ist oder fehlt, bevor wir überhaupt zum Umwandlungsschritt kommen.

Da ich nicht weiß, was oder wo genau das Problem ist, werde ich den folgenden Code für die Audio-Aufnahme-und Wiedergabe-Klassen ausgeben. Irgendwelche Vorschläge wären willkommen, dieses Problem zu lösen oder irgendwie zu umgehen.

Eine Idee, die ich hatte, war in einem anderen Format (CAF) mit verschiedenen Format Flags aufzunehmen. Betrachtet man die produzierten Werte, kommt keiner der signierten 16bit-Ints dem Maximalwert nahe. Ich sehe selten etwas über +/- 1000. Liegt das an dem kLinearPCMFormatFlagIsPacked-Flag in der AudioStreamPacketDescription? Durch das Entfernen dieses Flags wird die Audiodatei aufgrund eines ungültigen Formats nicht erstellt. Vielleicht würde der Wechsel zu CAF funktionieren, aber wir müssen zu WAVE konvertieren, bevor wir das Audio zurück an unseren Server senden.

Oder ist meine Konvertierung von vorzeichenbehafteten 16bit auf unsigned 8bit falsch? Ich habe auch Bitshifting und Casting versucht. Der einzige Unterschied ist, dass bei dieser Konvertierung alle Audiowerte auf 125 bis 130 komprimiert werden. Das Bit-Shifting und das Casting ändern sich auf 0-5 und 250-255. Das löst keine Probleme auf der JS-Seite.

Der nächste Schritt wäre, statt die Daten an JS zu übergeben, führen Sie es durch eine FFT-Funktion und erzeugen Sie Werte, die direkt von JS für den Audio Visualizer verwendet werden. Ich würde lieber herausfinden, ob ich etwas falsch gemacht habe, bevor ich in diese Richtung gegangen bin.

AQRecorder.h - BEARBEITEN: aktualisiertes Audioformat zu LinearPCM 32bit Float.

AQRecorder.m - BEARBEITEN: aktualisiertes Audioformat zu LinearPCM 32bit Float. FFT-Schritt in processSamplesForJS hinzugefügt, anstatt Audiodaten direkt zu senden.

#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 

Audio-Wiedergabe und -Aufnahme contrller Klassen 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 

Die JSMonitor-Klasse ist eine Brücke zwischen dem Javascript-Kern von UIWebView und dem systemeigenen Code. Ich schließe es nicht ein, weil es nichts anderes für Audiodaten als das Weiterleiten von Daten/Aufrufen zwischen diesen Klassen und JSCore tut.

EDIT

Das Format des Audio LinearPCM Float 32bit geändert hat. Anstatt die Audiodaten zu senden, werden sie über eine FFT-Funktion gesendet und die dB-Werte werden gemittelt und stattdessen gesendet.

+0

Haben Sie die Werte überprüft, die Sie innerhalb von objective-c oder nur ganz am Ende im UIWebView erhalten? –

+0

Sie übergeben die Proben nicht direkt. Stattdessen scheinen Sie so etwas wie einen gleitenden Durchschnitt der letzten 32 Samples zu passieren (avg + = * v; avg/= 2;). Ist das deine Absicht? –

+0

Warum ist AUDIO_DATA_TYPE_FORMAT * v ein Zeiger? Sollte es nicht ein Beispielwert sein? –

Antwort

0

Core Audio ist ein Schmerz, mit dem man arbeiten kann. Glücklicherweise bietet AVFoundation AVAudioRecorder zum Aufzeichnen von Videos und gibt Ihnen außerdem Zugriff auf die durchschnittliche und maximale Audioleistung, die Sie an Ihr JavaScript zurücksenden können, um Ihren UI-Visualizer zu aktualisieren. Von the docs:

Eine Instanz der Klasse AVAudioRecorder, einen Audio-Recorder genannt, bietet Audio-Aufnahmefunktion in Ihrer Anwendung. Mit Hilfe eines Audio-Recorder können Sie:

  • Nehmen Sie, bis der Benutzer die Aufnahme
  • Aufzeichnung für eine bestimmte Dauer eine Aufnahme
  • Pause und Fortsetzen stoppt
  • Audio-Eingangspegeldaten erhalten, die Sie verwenden können, Ebene Dosierung

This Stack Overflow question hat ein Beispiel zu geben, wie zum Verwenden Sie AVAudioRecorder.

+0

Der JS Visualizer benötigt mehr als maximale und durchschnittliche Leistung. Ich kann diese Werte auch von der AudioQueue abrufen. – Simurr