使用AVFoundation进行音频块的精确提取

11

问题

我正在尝试从视频文件中提取LPCM音频的样本精确范围。目前,我正在使用AVAssetReaderTrackOutput读取AVURLAsset并获得AVAssetTrack来实现此目的。

尽管使用AVURLAssetPreferPreciseDurationAndTimingKey设置为YES对资源进行初始化和准备,但在资源中寻找样本精确位置似乎不太准确。

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

例如,变量比特率编码的AAC流会表现出这种情况。虽然我知道VBR音频流在准确寻址方面会产生性能开销,但只要我能得到准确的样本,我愿意承担这个代价。

当使用Extended Audio File Services和ExtAudioFileRef API时,我可以实现样本精确的寻址和音频提取。同样,使用AVAudioFile也可以实现,因为它是建立在ExtAudioFileRef之上的。

然而,问题在于我还想从音频文件API拒绝但通过AVURLAsset支持的媒体容器中提取音频。

方法

使用CMTimeCMTimeRange定义样本精确的时间范围,并将其设置在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 给出的演示时间似乎与我所追求的相匹配,但由于它不准确,因此我没有机会校正和对齐检索到的样本。

有什么想法吗?

或者,是否有一种适应 AVAudioFileExtAudioFile 使用的方法来调整 AVAssetTrack

是否可以通过 AudioFileOpenWithCallbacks 访问音频轨道?

在 macOS 中是否有其他方式可以访问视频容器中的音频流?


1
需要注意的是,有时AVFoundation提供的样本数量不足以满足“durationTime”要求。例如,具有“kCMTimePositiveInfinity”的“durationTime”,只需按需读取足够的样本即可...问题在于初始搜索。 - Dan
2个回答

4

一种可行的方法是使用AVAssetReader读取压缩的AV文件,结合AVAssetWriter写入音频样本的新原始LPCM文件。然后,可以快速索引这个新的PCM文件(或者如果必要的话,内存映射数组)以提取精确的样本精确范围,而不会产生VBR每包解码大小异常或依赖于iOS CMTimeStamp算法超出一个人的控制。

这可能不是最高效的时间或内存操作,但它有效。


1
它肯定会起作用 - 但我真的想避免将整个源音轨的中间完整输出到内存/磁盘。使用例如AVAssetExportSession并在不重新编码的情况下将音轨写入磁盘(通过传递),然后仅使用音频文件API读取它是可行的,但这是一步昂贵的过程。 - Dan

0

我写了另一个答案,其中错误地声称AVAssetReader/AVAssetReaderTrackOutput不能进行样本精确的寻找,但实际上它们可以,但当您的音频轨道嵌入在电影文件中时,它看起来有问题,所以您发现了一个错误。恭喜!

如@hotpaw2的答案评论中提到的那样,通过AVAssetExportSession传递的音频轨道可以正常转储,即使您在非数据包边界上寻找(您碰巧在数据包边界上寻找,链接文件每个数据包有1024帧-在数据包边界之外寻找,您的差异不再为零,但它们非常小/不可听)。

我没有找到解决方法,因此请重新考虑转储压缩轨道。这很昂贵吗?如果您真的不想这样做,可以通过将niloutputSettings:传递给您的AVAssetReaderOutput并将其输出通过AudioQueue或(最好)AudioConverter来获取LPCM来自行解码原始数据包。

NB在后一种情况下,您需要处理四舍五入到数据包边界的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接