【问题标题】:Writing video + generated audio to AVAssetWriterInput, audio stuttering将视频+生成的音频写入 AVAssetWriterInput,音频卡顿
【发布时间】:2012-08-29 21:59:59
【问题描述】:

我正在从 iOS 上的 Unity 应用生成视频。我正在使用 iVidCap,它使用 AVFoundation 来执行此操作。那边一切正常。本质上,视频是通过使用纹理渲染目标并将帧传递给 Obj-C 插件来渲染的。

现在我需要为视频添加音频。音频将是在特定时间发生的声音效果,也可能是一些背景声音。使用的文件实际上是 Unity 应用程序内部的资产。我可能会将这些写入手机存储,然后生成一个 AVComposition,但我的计划是避免这种情况并将音频合成为浮点格式缓冲区(从音频剪辑中获取音频为浮点格式)。稍后我可能会做一些即时音频效果。

几个小时后,我设法录制了音频并与视频一起播放......但它结结巴巴。

目前我只是在每一帧视频的持续时间内生成一个方波并将其写入 AVAssetWriterInput。稍后,我会生成我真正想要的音频。

如果我生成一个大样本,我就不会卡顿。如果我把它写成块(我更喜欢分配一个庞大的数组),那么音频块似乎会相互剪辑:

我似乎无法弄清楚这是为什么。我很确定我得到了正确的音频缓冲区的时间戳,但也许我做错了整个部分。还是我需要一些标志来让视频同步到音频?我看不出这是问题所在,因为在将音频数据提取到 wav 后,我可以在波形编辑器中看到问题。

编写音频的相关代码:

- (id)init {
    self = [super init];
    
    if (self) {
        // [snip]
        
        rateDenominator = 44100;
        rateMultiplier = rateDenominator / frameRate;
        
        sample_position_ = 0;
        audio_fmt_desc_ = nil;
        int nchannels = 2;
        AudioStreamBasicDescription audioFormat;
        bzero(&audioFormat, sizeof(audioFormat));
        audioFormat.mSampleRate = 44100;
        audioFormat.mFormatID   = kAudioFormatLinearPCM;
        audioFormat.mFramesPerPacket = 1;
        audioFormat.mChannelsPerFrame = nchannels;
        int bytes_per_sample = sizeof(float);
        audioFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsAlignedHigh;
        audioFormat.mBitsPerChannel = bytes_per_sample * 8;
        audioFormat.mBytesPerPacket = bytes_per_sample * nchannels;
        audioFormat.mBytesPerFrame = bytes_per_sample * nchannels;
        
        CMAudioFormatDescriptionCreate(kCFAllocatorDefault,
                                       &audioFormat,
                                       0,
                                       NULL,
                                       0,
                                       NULL,
                                       NULL,
                                       &audio_fmt_desc_
        );
    }
    
    return self;
}

- (BOOL)beginRecordingSession {
    NSError* error = nil;
    
    isAborted = false;
    abortCode = No_Abort;
    
    // Allocate the video writer object.
    videoWriter = [[AVAssetWriter alloc] initWithURL:[self getVideoFileURLAndRemoveExisting:
                   recordingPath] fileType:AVFileTypeMPEG4 error:&error];
    
    if (error) {
        NSLog(@"Start recording error: %@", error);
    }
    
    // Configure video compression settings.
    NSDictionary* videoCompressionProps = [NSDictionary dictionaryWithObjectsAndKeys:
                                           [NSNumber numberWithDouble:1024.0 * 1024.0], AVVideoAverageBitRateKey,
                                           [NSNumber numberWithInt:10],AVVideoMaxKeyFrameIntervalKey,
                                           nil];
    
    // Configure video settings.
    NSDictionary* videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
    AVVideoCodecH264, AVVideoCodecKey,
    [NSNumber numberWithInt:frameSize.width], AVVideoWidthKey,
    [NSNumber numberWithInt:frameSize.height], AVVideoHeightKey,
    videoCompressionProps, AVVideoCompressionPropertiesKey,
    nil];
    
    // Create the video writer that is used to append video frames to the output video
    // stream being written by videoWriter.
    videoWriterInput = [[AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings] retain];
    //NSParameterAssert(videoWriterInput);
    videoWriterInput.expectsMediaDataInRealTime = YES;
    
    // Configure settings for the pixel buffer adaptor.
    NSDictionary* bufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithInt:kCVPixelFormatType_32ARGB], kCVPixelBufferPixelFormatTypeKey, nil];
    
    // Create the pixel buffer adaptor, used to convert the incoming video frames and
    // append them to videoWriterInput.
    avAdaptor = [[AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:bufferAttributes] retain];
    
    [videoWriter addInput:videoWriterInput];
    
    // <pb> Added audio input.
    sample_position_ = 0;
    AudioChannelLayout acl;
    bzero( &acl, sizeof(acl));
    acl.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
    
    NSDictionary* audioOutputSettings = nil;
    
    audioOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
    [ NSNumber numberWithInt: kAudioFormatMPEG4AAC ], AVFormatIDKey,
    [ NSNumber numberWithInt: 2 ], AVNumberOfChannelsKey,
    [ NSNumber numberWithFloat: 44100.0 ], AVSampleRateKey,
    [ NSNumber numberWithInt: 64000 ], AVEncoderBitRateKey,
    [ NSData dataWithBytes: &acl length: sizeof( acl ) ], AVChannelLayoutKey,
    nil];
    
    audioWriterInput = [[AVAssetWriterInput
    assetWriterInputWithMediaType: AVMediaTypeAudio
    outputSettings: audioOutputSettings ] retain];
    
    //audioWriterInput.expectsMediaDataInRealTime = YES;
    audioWriterInput.expectsMediaDataInRealTime = NO; // seems to work slightly better
    
    [videoWriter addInput:audioWriterInput];
    
    rateDenominator = 44100;
    rateMultiplier = rateDenominator / frameRate;
    
    // Add our video input stream source to the video writer and start it.
    [videoWriter startWriting];
    [videoWriter startSessionAtSourceTime:CMTimeMake(0, rateDenominator)];
    
    isRecording = true;
    return YES;
}

- (int) writeAudioBuffer:(float *)samples sampleCount:(size_t)n channelCount:(size_t)nchans {
    if (![self waitForAudioWriterReadiness]) {
        NSLog(@"WARNING: writeAudioBuffer dropped frame after wait limit reached.");
        return 0;
    }
    
    //NSLog(@"writeAudioBuffer");
    OSStatus status;
    CMBlockBufferRef bbuf = NULL;
    CMSampleBufferRef sbuf = NULL;
    
    size_t buflen = n * nchans * sizeof(float);
    // Create sample buffer for adding to the audio input.
    status = CMBlockBufferCreateWithMemoryBlock(
        kCFAllocatorDefault,
        samples,
        buflen,
        kCFAllocatorNull,
        NULL,
        0,
        buflen,
        0,
        &bbuf);
    
    if (status != noErr) {
        NSLog(@"CMBlockBufferCreateWithMemoryBlock error");
        return -1;
    }
    
    CMTime timestamp = CMTimeMake(sample_position_, 44100);
    sample_position_ += n;
    
    status = CMAudioSampleBufferCreateWithPacketDescriptions(kCFAllocatorDefault, bbuf, TRUE, 0, NULL, audio_fmt_desc_, 1, timestamp, NULL, &sbuf);
    if (status != noErr) {
        NSLog(@"CMSampleBufferCreate error");
        return -1;
    }
    BOOL r = [audioWriterInput appendSampleBuffer:sbuf];
    if (!r) {
        NSLog(@"appendSampleBuffer error");
    }
    CFRelease(bbuf);
    CFRelease(sbuf);
    
    return 0;
}

有什么想法吗?

我应该以不同的方式创建/附加示例吗?

这与 AAC 压缩有关吗?如果我尝试使用未压缩的音频(它会抛出),它就不起作用。

据我所知,我正在正确计算 PTS。为什么音频通道甚至需要这个?视频不应该同步到音频时钟吗?


更新

我尝试以 1024 个样本的固定块提供音频,因为这是 AAC 压缩器使用的 DCT 的大小。没什么区别。

在编写任何视频之前,我已经尝试过一次性完成所有模块。没用。

我已尝试将 CMSampleBufferCreate 用于剩余块,并将 CMAudioSampleBufferCreateWithPacketDescriptions 仅用于第一个块。没有变化。

我已经尝试过这些组合。还是不对。


解决方案

看来:

audioWriterInput.expectsMediaDataInRealTime = YES;

是必不可少的,否则它会扰乱它的思想。也许这是因为视频是用这个标志设置的。此外,CMBlockBufferCreateWithMemoryBlock 不会复制样本数据,即使您将标志 kCMBlockBufferAlwaysCopyDataFlag 传递给它。

因此,可以使用它创建一个缓冲区,然后使用CMBlockBufferCreateContiguous 进行复制,以确保您获得一个包含音频数据副本的块缓冲区。否则它会引用你最初传入的内存,事情就会变得一团糟。

【问题讨论】:

    标签: iphone objective-c ios avfoundation avassetwriter


    【解决方案1】:

    看起来不错,虽然我会使用CMBlockBufferCreateWithMemoryBlock,因为它会复制样本。您的代码是否可以不知道 audioWriterInput 何时完成?

    kAudioFormatFlagIsAlignedHigh 不应该是kAudioFormatFlagIsPacked吗?

    【讨论】:

    • 感谢您的回复!在创建样本缓冲区之前,我正在使用 CMBlockBufferCreateWithMemoryBlock。我确实想知道这是否与未复制缓冲区有关。我尝试先复制到 std::vector (因为数据来自.net)并等待编写器准备好,但它仍然有同样的问题。也许这里还有一个问题。我将查看标志,看看是否可以获取块缓冲区来复制数据。
    • 我正在使用 kAudioFormatFlagIsAlignedHigh 因为它是一个浮点缓冲区。我假设 kAudioFormatFlagIsPacked 用于整数缓冲区?
    • 我是否需要明确地创建电影以告知视频与音频同步? (并优先丢弃视频帧而不是音频?)
    • 找出这是否是您的问题的一种方法是故意泄漏样本缓冲区(而不是更改它们)。我认为您的数据是 kAudioFormatFlagIsPacked,这意味着您不需要 kAudioFormatFlagIsAlignedHigh。我认为您不需要将音频显式同步到视频,是的,它们具有不同的采样率,但这很好。实际上,您的问题可能与视频正交。您应该能够丢弃视频并生成一个重现问题的 m4a 音频文件。
    • 我试过故意泄露内存。不幸的是,它不能解决问题。在从 CMBlockBuffer 创建 CMSampleBuffer 后,我还尝试不泄漏而是直接清除内存。这最终没有音频,所以我只能假设它实际上并没有复制样本。我使用了 kCMBlockBufferAssureMemoryNowFlag | kCMBlockBufferAlwaysCopyDataFlag 执行此操作时,但它看起来仍然不像实际复制任何数据。无论如何,这看起来不像是我的问题的真正原因,但这绝对是要记住的事情。
    【解决方案2】:
    CMAudioSampleBufferCreateWithPacketDescriptions(kCFAllocatorDefault, bbuf, TRUE, 0, NULL, audio_fmt_desc_, 1, timestamp, NULL, &sbuf);
    

    应该是

    CMAudioSampleBufferCreateWithPacketDescriptions(kCFAllocatorDefault, bbuf, TRUE, 0, NULL, audio_fmt_desc_, n, timestamp, NULL, &sbuf);i made it.
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-31
      • 2012-11-29
      • 2014-05-06
      • 2012-11-16
      • 2019-08-14
      相关资源
      最近更新 更多