一起使用 AVCaptureSession 和音频单元会导致 AVAssetWriterInput 出现问题
Using AVCaptureSession and Audio Units Together Causes Problems for AVAssetWriterInput
我正在开发一个 iOS 应用程序,它可以同时做两件事:
- 它捕获音频和视频并将它们中继到服务器以提供视频聊天功能。
- 它捕获本地音频和视频并将它们编码为 mp4 文件以保存以供后代使用。
不幸的是,当我们使用音频单元配置应用程序以启用回声消除时,录音功能会中断:我们用来编码音频的 AVAssetWriterInput
实例拒绝传入样本。当我们不设置音频单元时,录音工作正常,但回声很糟糕。
为了启用回声消除,我们像这样配置一个音频单元(为简洁起见解释):
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
AudioComponent comp = AudioComponentFindNext(NULL, &desc);
OSStatus status = AudioComponentInstanceNew(comp, &_audioUnit);
status = AudioUnitInitialize(_audioUnit);
这对于视频聊天来说效果很好,但它破坏了录音功能,录音功能是这样设置的(再次解释——实际的实现分散在几个方法中)。
_captureSession = [[AVCaptureSession alloc] init];
// Need to use the existing audio session & configuration to ensure we get echo cancellation
_captureSession.usesApplicationAudioSession = YES;
_captureSession.automaticallyConfiguresApplicationAudioSession = NO;
[_captureSession beginConfiguration];
AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self audioCaptureDevice] error:NULL];
[_captureSession addInput:audioInput];
_audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
[_audioDataOutput setSampleBufferDelegate:self queue:_cameraProcessingQueue];
[_captureSession addOutput:_audioDataOutput];
[_captureSession commitConfiguration];
captureOutput
的相关部分看起来像这样:
NSLog(@"Audio format, channels: %d, sample rate: %f, format id: %d, bits per channel: %d", basicFormat->mChannelsPerFrame, basicFormat->mSampleRate, basicFormat->mFormatID, basicFormat->mBitsPerChannel);
if (_assetWriter.status == AVAssetWriterStatusWriting) {
if (_audioEncoder.readyForMoreMediaData) {
if (![_audioEncoder appendSampleBuffer:sampleBuffer]) {
NSLog(@"Audio encoder couldn't append sample buffer");
}
}
}
对 appendSampleBuffer
的调用失败了,但是——这是奇怪的部分——只有当我没有 耳朵phone已插入我的 phone。检查发生这种情况时产生的日志,我发现没有连接 earphones,日志消息中报告的通道数是 3,而连接 earphone已连接,通道数为1。这解释了编码操作失败的原因,因为编码器被配置为只需要一个通道。
我不明白的是 为什么 我在这里得到三个频道。如果我注释掉初始化音频单元的代码,我只能得到一个通道并且录音工作正常,但回声消除不起作用。此外,如果我删除这些行
// Need to use the existing audio session & configuration to ensure we get echo cancellation
_captureSession.usesApplicationAudioSession = YES;
_captureSession.automaticallyConfiguresApplicationAudioSession = NO;
录音有效(我只得到一个带或不带头的单通道phones),但同样,我们失去了回声消除。
所以,我的问题的症结在于:当我配置一个音频单元以提供回声消除时,为什么我会得到三个音频通道?此外,有什么方法可以防止这种情况发生或使用 AVCaptureSession
?
解决此问题
我考虑过将微phone 音频直接从低级音频单元回调传输到编码器以及聊天管道,但似乎需要必要的 Core Media 缓冲区如果可能的话,我想避免这样做。
请注意,聊天和录音功能是由不同的人编写的——他们都不是我——这就是这段代码没有更加集成的原因。如果可能的话,我想避免重构整个混乱局面。
最终,我通过 I/O 音频单元从麦克风收集音频样本,将这些样本重新打包成 CMSampleBuffer
,并将新构建的 CMSampleBuffer
进入编码器。
进行转换的代码如下所示(为简洁起见进行了缩写)。
// Create a CMSampleBufferRef from the list of samples, which we'll own
AudioStreamBasicDescription monoStreamFormat;
memset(&monoStreamFormat, 0, sizeof(monoStreamFormat));
monoStreamFormat.mSampleRate = 48000;
monoStreamFormat.mFormatID = kAudioFormatLinearPCM;
monoStreamFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved;
monoStreamFormat.mBytesPerPacket = 2;
monoStreamFormat.mFramesPerPacket = 1;
monoStreamFormat.mBytesPerFrame = 2;
monoStreamFormat.mChannelsPerFrame = 1;
monoStreamFormat.mBitsPerChannel = 16;
CMFormatDescriptionRef format = NULL;
OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &monoStreamFormat, 0, NULL, 0, NULL, NULL, &format);
// Convert the AudioTimestamp to a CMTime and create a CMTimingInfo for this set of samples
uint64_t timeNS = (uint64_t)(hostTime * _hostTimeToNSFactor);
CMTime presentationTime = CMTimeMake(timeNS, 1000000000);
CMSampleTimingInfo timing = { CMTimeMake(1, 48000), presentationTime, kCMTimeInvalid };
CMSampleBufferRef sampleBuffer = NULL;
status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, numSamples, 1, &timing, 0, NULL, &sampleBuffer);
// add the samples to the buffer
status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer,
kCFAllocatorDefault,
kCFAllocatorDefault,
0,
samples);
// Pass the buffer into the encoder...
请注意,我已经删除了分配对象的错误处理和清理。
我正在开发一个 iOS 应用程序,它可以同时做两件事:
- 它捕获音频和视频并将它们中继到服务器以提供视频聊天功能。
- 它捕获本地音频和视频并将它们编码为 mp4 文件以保存以供后代使用。
不幸的是,当我们使用音频单元配置应用程序以启用回声消除时,录音功能会中断:我们用来编码音频的 AVAssetWriterInput
实例拒绝传入样本。当我们不设置音频单元时,录音工作正常,但回声很糟糕。
为了启用回声消除,我们像这样配置一个音频单元(为简洁起见解释):
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
AudioComponent comp = AudioComponentFindNext(NULL, &desc);
OSStatus status = AudioComponentInstanceNew(comp, &_audioUnit);
status = AudioUnitInitialize(_audioUnit);
这对于视频聊天来说效果很好,但它破坏了录音功能,录音功能是这样设置的(再次解释——实际的实现分散在几个方法中)。
_captureSession = [[AVCaptureSession alloc] init];
// Need to use the existing audio session & configuration to ensure we get echo cancellation
_captureSession.usesApplicationAudioSession = YES;
_captureSession.automaticallyConfiguresApplicationAudioSession = NO;
[_captureSession beginConfiguration];
AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:[self audioCaptureDevice] error:NULL];
[_captureSession addInput:audioInput];
_audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
[_audioDataOutput setSampleBufferDelegate:self queue:_cameraProcessingQueue];
[_captureSession addOutput:_audioDataOutput];
[_captureSession commitConfiguration];
captureOutput
的相关部分看起来像这样:
NSLog(@"Audio format, channels: %d, sample rate: %f, format id: %d, bits per channel: %d", basicFormat->mChannelsPerFrame, basicFormat->mSampleRate, basicFormat->mFormatID, basicFormat->mBitsPerChannel);
if (_assetWriter.status == AVAssetWriterStatusWriting) {
if (_audioEncoder.readyForMoreMediaData) {
if (![_audioEncoder appendSampleBuffer:sampleBuffer]) {
NSLog(@"Audio encoder couldn't append sample buffer");
}
}
}
对 appendSampleBuffer
的调用失败了,但是——这是奇怪的部分——只有当我没有 耳朵phone已插入我的 phone。检查发生这种情况时产生的日志,我发现没有连接 earphones,日志消息中报告的通道数是 3,而连接 earphone已连接,通道数为1。这解释了编码操作失败的原因,因为编码器被配置为只需要一个通道。
我不明白的是 为什么 我在这里得到三个频道。如果我注释掉初始化音频单元的代码,我只能得到一个通道并且录音工作正常,但回声消除不起作用。此外,如果我删除这些行
// Need to use the existing audio session & configuration to ensure we get echo cancellation
_captureSession.usesApplicationAudioSession = YES;
_captureSession.automaticallyConfiguresApplicationAudioSession = NO;
录音有效(我只得到一个带或不带头的单通道phones),但同样,我们失去了回声消除。
所以,我的问题的症结在于:当我配置一个音频单元以提供回声消除时,为什么我会得到三个音频通道?此外,有什么方法可以防止这种情况发生或使用 AVCaptureSession
?
我考虑过将微phone 音频直接从低级音频单元回调传输到编码器以及聊天管道,但似乎需要必要的 Core Media 缓冲区如果可能的话,我想避免这样做。
请注意,聊天和录音功能是由不同的人编写的——他们都不是我——这就是这段代码没有更加集成的原因。如果可能的话,我想避免重构整个混乱局面。
最终,我通过 I/O 音频单元从麦克风收集音频样本,将这些样本重新打包成 CMSampleBuffer
,并将新构建的 CMSampleBuffer
进入编码器。
进行转换的代码如下所示(为简洁起见进行了缩写)。
// Create a CMSampleBufferRef from the list of samples, which we'll own
AudioStreamBasicDescription monoStreamFormat;
memset(&monoStreamFormat, 0, sizeof(monoStreamFormat));
monoStreamFormat.mSampleRate = 48000;
monoStreamFormat.mFormatID = kAudioFormatLinearPCM;
monoStreamFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved;
monoStreamFormat.mBytesPerPacket = 2;
monoStreamFormat.mFramesPerPacket = 1;
monoStreamFormat.mBytesPerFrame = 2;
monoStreamFormat.mChannelsPerFrame = 1;
monoStreamFormat.mBitsPerChannel = 16;
CMFormatDescriptionRef format = NULL;
OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &monoStreamFormat, 0, NULL, 0, NULL, NULL, &format);
// Convert the AudioTimestamp to a CMTime and create a CMTimingInfo for this set of samples
uint64_t timeNS = (uint64_t)(hostTime * _hostTimeToNSFactor);
CMTime presentationTime = CMTimeMake(timeNS, 1000000000);
CMSampleTimingInfo timing = { CMTimeMake(1, 48000), presentationTime, kCMTimeInvalid };
CMSampleBufferRef sampleBuffer = NULL;
status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, numSamples, 1, &timing, 0, NULL, &sampleBuffer);
// add the samples to the buffer
status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer,
kCFAllocatorDefault,
kCFAllocatorDefault,
0,
samples);
// Pass the buffer into the encoder...
请注意,我已经删除了分配对象的错误处理和清理。