【问题标题】:How do I use CoreAudio's AudioConverter to encode AAC in real-time?如何使用 CoreAudio 的 AudioConverter 实时编码 AAC?
【发布时间】:2015-07-28 01:37:36
【问题描述】:

我能找到的所有使用AudioConverterRef 的示例代码都侧重于我预先拥有所有数据的用例(例如转换磁盘上的文件)。他们通常调用AudioConverterFillComplexBuffer 并将PCM 转换为inInputDataProcUserData 并将其填写在回调中。 (这真的是应该如何使用的吗?为什么它需要回调呢?)对于我的用例,我正在尝试从麦克风流式传输 aac 音频,所以我没有文件,并且我的 PCM 缓冲区正在实时填写。

由于我没有预先准备好所有数据,因此我尝试在输入数据输出后在回调中执行*ioNumberDataPackets = 0,但这只会使 AudioConverter 处于需要@的死状态987654325@ted,我没有从中得到任何数据。

我在网上看到建议的一种方法是,如果我存储的数据太小,则从回调中返回错误,并在我有更多数据后重试,但这似乎是一种资源浪费,我连尝试都不敢。

我真的需要“重试直到我的输入缓冲区足够大”,还是有更好的方法?

【问题讨论】:

    标签: ios audio core-audio aac audio-converter


    【解决方案1】:

    AudioConverterFillComplexBuffer 实际上并不意味着“用我这里的输入缓冲区填充编码器”。它的意思是“在这里用来自编码器的编码数据填充这个输出缓冲区”。从这个角度来看,回调突然变得有意义——它用于获取源数据以满足“为我填充此输出缓冲区”的请求。也许这对其他人来说是显而易见的,但我花了 很长时间 时间来理解这一点(从我看到的所有 AudioConverter 示例代码中我看到人们通过inInputDataProcUserData 发送输入数据的地方,我猜我不是唯一一个)。

    AudioConverterFillComplexBuffer 调用处于阻塞状态,并希望您通过回调同步向其传递数据。如果您正在实时编码,则需要在您自己设置的单独线程上调用FillComplexBuffer。在回调中,您可以检查可用的输入数据,如果不可用,则需要阻塞信号量。使用 NSCondition,编码器线程将如下所示:

    - (void)startEncoder
    {
        OSStatus creationStatus = AudioConverterNew(&_fromFormat, &_toFormat, &_converter);
    
        _running = YES;
        _condition = [[NSCondition alloc] init];
        [self performSelectorInBackground:@selector(_encoderThread) withObject:nil];
    }
    
    - (void)_encoderThread
    {
        while(_running) {
            // Make quarter-second buffers.
            size_t bufferSize = (_outputBitrate/8) * 0.25;
            NSMutableData *outAudioBuffer = [NSMutableData dataWithLength:bufferSize];
            AudioBufferList outAudioBufferList;
            outAudioBufferList.mNumberBuffers = 1;
            outAudioBufferList.mBuffers[0].mNumberChannels = _toFormat.mChannelsPerFrame;
            outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32)bufferSize;
            outAudioBufferList.mBuffers[0].mData = [outAudioBuffer mutableBytes];
    
            UInt32 ioOutputDataPacketSize = 1;
    
            _currentPresentationTime = kCMTimeInvalid; // you need to fill this in during FillComplexBuffer
            const OSStatus conversionResult = AudioConverterFillComplexBuffer(_converter, FillBufferTrampoline, (__bridge void*)self, &ioOutputDataPacketSize, &outAudioBufferList, NULL);
    
            // here I convert the AudioBufferList into a CMSampleBuffer, which I've omitted for brevity.
            // Ping me if you need it.
            [self.delegate encoder:self encodedSampleBuffer:outSampleBuffer];
        }
    }
    

    并且回调可能如下所示:(请注意,我通常使用此蹦床立即转发到我的实例上的方法(通过在inUserData 中转发我的实例;为简洁起见省略此步骤):

    static OSStatus FillBufferTrampoline(AudioConverterRef               inAudioConverter,
                                            UInt32*                         ioNumberDataPackets,
                                            AudioBufferList*                ioData,
                                            AudioStreamPacketDescription**  outDataPacketDescription,
                                            void*                           inUserData)
    {
        [_condition lock];
    
        UInt32 countOfPacketsWritten = 0;
    
        while (true) {
            // If the condition fires and we have shut down the encoder, just pretend like we have written 0 bytes and are done.
            if(!_running) break;
    
            // Out of input data? Wait on the condition.
            if(_inputBuffer.length == 0) {
                [_condition wait];
                continue;
            }
    
            // We have data! Fill ioData from your _inputBuffer here.
            // Also save the input buffer's start presentationTime here.
    
            // Exit out of the loop, since we're done waiting for data
            break;
        }
    
        [_condition unlock];
    
            // 2. Set ioNumberDataPackets to the amount of data remaining
    
    
        // if running is false, this will be 0, indicating EndOfStream
        *ioNumberDataPackets = countOfPacketsWritten;
    
        return noErr;
    }
    

    为了完整起见,以下是您如何向该编码器提供数据,以及如何正确关闭它:

    - (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer
    {
        [_condition lock];
        // Convert sampleBuffer and put it into _inputBuffer here
        [_condition broadcast];
        [_condition unlock];
    }
    
    - (void)stopEncoding
    {
        [_condition lock];
        _running = NO;
        [_condition broadcast];
        [_condition unlock];
    }
    

    【讨论】:

    • 我在用 _inputBuffer 填充 iodate 并设置 ioNumberDataPackets 时遇到了一些问题,请您填写代码吗?一些问题:我们需要将 ioData.mNumberBuffers 设置为 1 吗?我们是否需要将 _inputBuffer 中的所有数据填充到 ioData.mBuffers[0] 中?我们如何计算 ioNumberDataPackets?还是只是将其设置为 1?您是什么意思“将 ioNumberDataPackets 设置为剩余的数据量?”而文档说“在退出时,实际提供输入的音频数据包的数量”?
    • @nevyn 我目前正在努力将 AudioBufferList 转换为 CMSampleBuffer,您在此处省略了它。你能分享一下你是怎么做到的吗?谢谢!
    • 谢谢!我最初的方法是在FillComplexBuffer(输出)循环顶部放置一个信号量来等待传入数据。但那是错误的地方。将等待移动到FillBuffer(输入)回调中的循环将“背压”放置在正确的位置。您的问答很好地解释了原因。
    【解决方案2】:

    为了将来参考,有一种更简单的选择。

    CoreAudio 标头的状态:

    如果回调返回错误,它必须返回零数据包。 AudioConverterFillComplexBuffer 将停止产生输出并返回任何内容 已将输出连同错误代码一起生成给它的调用者。这 当输入过程暂时用完数据时,可以使用机制,但是 尚未到达流的末尾。

    所以,就这样做吧。不是返回 noErr 且 *ioNumberDataPackets = 0,而是返回任何错误(只是补一个,我使用了 -1),并且将返回已经转换的数据,而 Audio Converter 保持活动状态,不需要重置。

    【讨论】:

    • 我试过了;当我尝试这种方法时,AudioConverter 会给我一个只有一个 mpeg 标头的 12 字节缓冲区,然后拒绝接收更多数据。我认为这意味着 AC 需要足够的数据来发出完整的 aac 帧才能工作。
    • 啊。它可能是。我只使用 PCM 输出,它对我来说非常有用。 AudioConverter 确实维护它自己的内部缓冲,所以奇怪的是这也不适用于 AAC。但是 API 确实声明它必须输出一些东西,所以也许这会把它们放在一个奇怪的地方。
    猜你喜欢
    • 2016-07-20
    • 1970-01-01
    • 2013-09-24
    • 1970-01-01
    • 2017-04-04
    • 1970-01-01
    • 1970-01-01
    • 2011-06-24
    • 2014-04-26
    相关资源
    最近更新 更多