Образец точного извлечения фрагментов аудио с помощью AVFoundation

Проблема

Я хочу извлечь диапазоны звука LPCM с точностью до образца из звуковых дорожек в видеофайлах. В настоящее время я пытаюсь добиться этого, используя AVAssetReaderTrackOutput против AVAssetTrack, полученного при чтении AVURLAsset.

Несмотря на подготовку и обеспечение инициализации актива с помощью AVURLAssetPreferPreciseDurationAndTimingKey, установленного на YES, поиск позиции с точностью до выборки в активе кажется неточным.

NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey : @(YES) };
_asset = [[AVURLAsset alloc] initWithURL:fileURL options:options];

Это проявляется, например, в кодированные потоки AAC с переменным битрейтом. Хотя я знаю, что аудиопотоки VBR приводят к снижению производительности при точном поиске, я готов заплатить, если мне будут доставлены точные образцы.

При использовании, например. Расширенные службы аудиофайлов и ExtAudioFileRef API позволяют выполнять поиск и извлечение аудио с точностью до семпла. То же самое с AVAudioFile, так как он строится поверх ExtAudioFileRef.

Проблема, однако, в том, что я также хотел бы извлекать аудио из медиаконтейнеров, которые отвергаются только API-интерфейсами аудиофайлов, но которые поддерживаются в AVFoundation через AVURLAsset.

Метод

Примерный точный временной диапазон для извлечения определяется с помощью CMTime и CMTimeRange и устанавливается в AVAssetReaderTrackOutput. Образцы затем итеративно извлекаются.

-(NSData *)readFromFrame:(SInt64)startFrame
      requestedFrameCount:(UInt32)frameCount
{
    NSUInteger expectedByteCount = frameCount * _bytesPerFrame;
    NSMutableData *data = [NSMutableData dataWithCapacity:expectedByteCount];
    
    //
    // Configure Output
    //

    NSDictionary *settings = @{ AVFormatIDKey               : @( kAudioFormatLinearPCM ),
                                AVLinearPCMIsNonInterleaved : @( NO ),
                                AVLinearPCMIsBigEndianKey   : @( NO ),
                                AVLinearPCMIsFloatKey       : @( YES ),
                                AVLinearPCMBitDepthKey      : @( 32 ),
                                AVNumberOfChannelsKey       : @( 2 ) };

    AVAssetReaderOutput *output = [[AVAssetReaderTrackOutput alloc] initWithTrack:_track outputSettings:settings];

    CMTime startTime    = CMTimeMake( startFrame, _sampleRate );
    CMTime durationTime = CMTimeMake( frameCount, _sampleRate );
    CMTimeRange range   = CMTimeRangeMake( startTime, durationTime );

    //
    // Configure Reader
    //

    NSError *error = nil;
    AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:_asset error:&error];

    if( !reader )
    {
        fprintf( stderr, "avf : failed to initialize reader\n" );
        fprintf( stderr, "avf : %s\n%s\n", error.localizedDescription.UTF8String, error.localizedFailureReason.UTF8String );
        exit( EXIT_FAILURE );
    }

    [reader addOutput:output];
    [reader setTimeRange:range];
    BOOL startOK = [reader startReading];

    NSAssert( startOK && reader.status == AVAssetReaderStatusReading, @"Ensure we've started reading." );

    NSAssert( _asset.providesPreciseDurationAndTiming, @"We expect the asset to provide accurate timing." );

    //
    // Start reading samples
    //

    CMSampleBufferRef sample = NULL;
    while(( sample = [output copyNextSampleBuffer] ))
    {
        CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp( sample );
        if( data.length == 0 )
        {
            // First read - we should be at the expected presentation time requested.
            int32_t comparisonResult = CMTimeCompare( presentationTime, startTime );
            NSAssert( comparisonResult == 0, @"We expect sample accurate seeking" );
        }

        CMBlockBufferRef buffer = CMSampleBufferGetDataBuffer( sample );

        if( !buffer )
        {
            fprintf( stderr, "avf : failed to obtain buffer" );
            exit( EXIT_FAILURE );
        }

        size_t lengthAtOffset = 0;
        size_t totalLength = 0;
        char *bufferData = NULL;

        if( CMBlockBufferGetDataPointer( buffer, 0, &lengthAtOffset, &totalLength, &bufferData ) != kCMBlockBufferNoErr )
        {
            fprintf( stderr, "avf : failed to get sample\n" );
            exit( EXIT_FAILURE );
        }

        if( bufferData && lengthAtOffset )
        {
            [data appendBytes:bufferData length:lengthAtOffset];
        }

        CFRelease( sample );
    }

    NSAssert( reader.status == AVAssetReaderStatusCompleted, @"Completed reading" );

    [output release];
    [reader release];

    return [NSData dataWithData:data];
}

Примечания

Время презентации, которое дает мне CMSampleBufferGetPresentationTimeStamp, похоже, соответствует тому, что я искал, но, поскольку оно кажется неточным, у меня нет возможности исправить и выровнять образцы, которые я получаю.

Любые мысли о том, как это сделать?

В качестве альтернативы, есть ли способ адаптировать AVAssetTrack для использования AVAudioFile или ExtAudioFile?

Можно ли получить доступ к звуковой дорожке через AudioFileOpenWithCallbacks?

Можно ли по-другому получить аудиопоток из видеоконтейнера в macOS?


person Dan    schedule 06.11.2017    source источник
comment
Следует отметить, что иногда AVFoundation предоставляет меньше образцов, чем необходимо для достаточного удовлетворения требования durationTime. Это не проблема, например. имейте durationTime из kCMTimePositiveInfinity и просто читайте достаточное количество образцов по мере необходимости… это первоначальный поиск, который проблематичен.   -  person Dan    schedule 06.11.2017


Ответы (2)


Одной из работающих процедур является использование AVAssetReader для чтения вашего сжатого AV-файла в сочетании с AVAssetWriter для записи нового необработанного LPCM-файла аудиосэмплов. Затем можно быстро проиндексировать этот новый файл PCM (или массив с отображением памяти, если необходимо), чтобы извлечь точные диапазоны с точностью до выборки, не вызывая аномалий размера декодирования VBR для каждого пакета или в зависимости от алгоритмов iOS CMTimeStamp вне контроля.

Это может быть не самая эффективная процедура по времени или памяти, но она работает.

person hotpaw2    schedule 06.11.2017
comment
Это определенно сработало бы — однако мне бы очень хотелось избежать промежуточного полного вывода всей исходной звуковой дорожки в память/диск. Использование, например. AVAssetExportSession и запись звуковой дорожки без перекодирования на диск (pass-through), а затем чтение того, что с использованием аудиофайла работают только API, но это затратный шаг. - person Dan; 06.11.2017

Я написал еще один ответ, в котором я неправильно утверждал, что AVAssetReader/AVAssetReaderTrackOutput не выполнял точный выборочный поиск, они это делают, но он выглядит сломанным, когда ваша звуковая дорожка встроена в файл фильма, поэтому вы нашли ошибку. Поздравляем!

Звуковая дорожка, сброшенная с проходом через AVAssetExportSession, как упоминалось в комментарии к ответу @ hotpaw2, работает нормально, даже когда вы ищете на непакетных границах (вы искали на границах пакетов, связанный файл имеет 1024 кадра на пакет - при поиске границ пакетов ваши различия уже не нулевые, а очень, очень маленькие/не слышимые).

Я не нашел обходного пути, поэтому рассмотрите возможность сброса сжатой дорожки. Это так дорого? Если вы действительно не хотите этого делать, вы можете самостоятельно декодировать необработанные пакеты, передав nil outputSettings: вашему AVAssetReaderOutput и пропустив его вывод через AudioQueue или (предпочтительно?) AudioConverter для получения LPCM.

Примечание. В последнем случае вам потребуется округление до границ пакета при поиске.

person Rhythmic Fistman    schedule 15.11.2017