JavaScriptベースのウェブアプリケーションのUIWebViewのほとんどはアプリです。私たちが打ち破った要件は、ユーザーにオーディオを再生してからユーザーを録音し、その録音を確認のために再生してからサーバーに送信することです。この機能はブラウザに組み込まれているため、Chrome、Android、その他のプラットフォームで動作します。ネイティブコードは必要ありません。オーディオを録音し、iOS 8/9のUIWebView(JavascriptCore)にデータを渡す
悲しいことに、iOS(iOS 8/9)ウェブビューには音声を録音できません。
AudioQueueでオーディオを録音し、データ(LinearPCM 16bit)をJS AudioNodeに渡すことで、Webアプリケーションが他のプラットフォームとまったく同じ方法でiOSオーディオを処理できるようにしました。これは、我々はJSにオーディオを渡すことができるポイントになったが、アプリケーションが最終的に悪いメモリアクセスエラーでクラッシュするか、JavaScriptの側はちょうど送信されたデータに追いつくことができませんでした。
次のアイデアは、録音中に表示される基本的なオーディオビジュアライザーであるビジュアルフィードバック用に、オーディオ録音をファイルに保存し、部分的なオーディオデータをJSに送信することでした。
リニアPCMが16ビットで署名しているため、オーディオレコードとWAVEファイルは正常に再生されます。 JSビジュアライザーは、私たちが立ち往生している場所です。リニアPCMの符号なし8bitが期待されているので、間違っているかもしれない変換ステップを追加しました。私は、主にオンラインで見つかったいくつかの異なる方法を試してきましたが、私たちが変換ステップに到達する前に、何かが間違っているか不足していると思うように働くものを見つけませんでした。
私は何が問題になっているのかわからないので、私はオーディオ録音および再生クラスのために以下のコードをダンプします。何らかの理由でこの問題を解決することをお勧めします。
私が持っていた考えの1つは、異なるフォーマットフラグを使用して異なるフォーマット(CAF)で記録することでした。生成された値を見ると、符号付き16ビット整数のうち、最大値に近いものもあります。私は+/- 1000以上のものはめったに見ません。 AudioStreamPacketDescriptionのkLinearPCMFormatFlagIsPackedフラグが原因ですか?このフラグを削除すると、無効な形式のためにオーディオファイルが作成されなくなります。たぶん、CAFへの切り替えはうまくいくかもしれませんが、WAVEに変換してからオーディオをサーバーに送り返す必要があります。
多分、符号付き16ビットから符号なし8ビットへの変換が間違っていますか?私もビットシフトとキャストを試みました。唯一の違いは、この変換ではすべてのオーディオ値が125と130の間に圧縮されることです。ビットシフトとキャストは0-5と250-255に変更されます。それはJS側の問題を本当に解決するものではありません。
次のステップは、データをJSに渡す代わりにFFT関数を実行し、オーディオビジュアライザー用にJSによって直接使用される値を生成することです。私はむしろ、その方向に向かう前に明らかに間違ったことをしたかどうかを確かめたい。
AQRecorder.h - EDIT:オーディオフォーマットを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:リニアPCMの32ビット浮動小数点への更新オーディオフォーマット。オーディオデータを直接送信するのではなく、processSamplesForJSにFFTステップを追加しました。
#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
オーディオの再生と録音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
オーディオ。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
JSMonitorクラスのUIWebView JavaScriptCoreにおけるネイティブコードとの間のブリッジです。私は、これらのクラスとJSCoreの間のデータ/呼び出しをパスする以外には、オーディオのために何もしないので、それを含めません。
EDIT
オーディオのフォーマットはリニアPCMフロート32ビットに変更されました。オーディオデータを送信する代わりに、FFT機能を介して送信され、dB値が平均化されて送信されます。
あなたはobjective-cの中から受け取った値をチェックしましたか、またはUIWebViewの最後の部分だけをチェックしましたか? –
サンプルを直接渡すわけではありません。代わりに、最後の32サンプル(avg + = * v; avg/= 2;)の移動平均のようなものを渡すようです。これはあなたの意図ですか? –
なぜAUDIO_DATA_TYPE_FORMAT * vがポインタですか?サンプル値ではないでしょうか? –