Как использовать AudioConverter CoreAudio для кодирования AAC в реальном времени?

Все образцы кода, которые я могу найти, использующие AudioConverterRef, ориентированы на случаи использования, когда у меня есть все данные заранее (например, преобразование файла на диске). Обычно они вызывают AudioConverterFillComplexBuffer с PCM для преобразования в inInputDataProcUserData и просто заполняют его в обратном вызове. (Действительно ли это так, как это должно использоваться? Зачем тогда нужен обратный вызов?) В моем случае использования я пытаюсь передать аудио в формате aac с микрофона, поэтому у меня нет файла, и мой буфер PCM заполняется в режиме реального времени.

Поскольку у меня нет всех данных заранее, я попытался выполнить *ioNumberDataPackets = 0 в обратном вызове после того, как мои входные данные вышли, но это просто переводит AudioConverter в мертвое состояние, когда его нужно AudioConverterReset()ted, и я не t получить какие-либо данные из него.

Один подход, который я видел в Интернете, заключается в том, чтобы вернуть ошибку обратного вызова, если данные, которые я сохранил, слишком малы, и просто попробовать еще раз, когда у меня будет больше данных, но это кажется такой пустой тратой ресурсов, что я не могу заставить себя хотя бы попробовать.

Мне действительно нужно «повторить попытку, пока мой входной буфер не станет достаточно большим», или есть лучший способ?


person nevyn    schedule 16.05.2015    source источник


Ответы (2)


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];
}
person nevyn    schedule 16.05.2015
comment
У меня возникли проблемы с заполнением iodate с помощью _inputBuffer и установкой ioNumberDataPackets, не могли бы вы заполнить код? Некоторые вопросы: нужно ли нам устанавливать ioData.mNumberBuffers равным 1? Нужно ли нам заполнять все данные из _inputBuffer в ioData.mBuffers[0]? Как мы можем рассчитать ioNumberDataPackets? или просто установить его на 1? Что вы имеете в виду, установите ioNumberDataPackets на количество оставшихся данных? в то время как документ говорит о выходе, количество пакетов аудиоданных, фактически предоставленных для ввода? - person lancy; 16.02.2017
comment
@nevyn В настоящее время я пытаюсь преобразовать AudioBufferList в CMSampleBuffer, который вы здесь не указали. Не могли бы вы поделиться, как вы это сделали? Спасибо! - person Grzegorz Aperliński; 18.12.2019
comment
Спасибо! Мой первоначальный подход заключался в том, чтобы поместить семафор для ожидания входящих данных поверх цикла FillComplexBuffer (выход). Но это неподходящее место для этого. Перемещение ожидания в цикл в обратном вызове FillBuffer (ввод) помещает обратное давление точно в нужное место. Ваши вопросы и ответы отлично объясняют, почему. - person natevw; 18.03.2020

На будущее, есть более простой вариант.

Состояние заголовка CoreAudio:

Если обратный вызов возвращает ошибку, он должен возвращать нулевые пакеты данных. AudioConverterFillComplexBuffer прекратит создание вывода и вернет любой вывод, который уже был произведен вызывающей стороне, вместе с кодом ошибки. Этот механизм можно использовать, когда во входной процедуре временно закончились данные, но еще не достигнут конец потока.

Итак, сделайте именно это. Вместо того, чтобы возвращать noErr с *ioNumberDataPackets = 0, верните любую ошибку (просто придумайте, я использовал -1), и будут возвращены уже преобразованные данные, а Audio Converter останется живым и его не нужно сбрасывать.

person Colin Cornaby    schedule 05.01.2017
comment
Я пробовал это; когда я пробовал этот подход, AudioConverter выдавал мне 12-байтовый буфер только с заголовком mpeg, а затем отказывался принимать больше данных. Я предположил, что это означает, что AC требуется достаточно данных для передачи полных кадров AAC для работы. - person nevyn; 06.01.2017
comment
Ааа. Возможно. Я работаю только с выходом PCM, и он отлично работает для меня. AudioConverter поддерживает собственную внутреннюю буферизацию, поэтому странно, что это не сработает и для AAC. Но API заявляет, что он должен что-то выводить, так что, возможно, это ставит их в странное место. - person Colin Cornaby; 09.01.2017