【问题标题】:AVAudioEngine multiple AVAudioInputNodes do not play in perfect syncAVAudioEngine 多个 AVAudioInputNode 不能完美同步播放
【发布时间】:2015-02-03 15:34:10
【问题描述】:

我一直在尝试使用 AVAudioEngine 来安排多个音频文件以完美同步播放,但是在收听输出时,输入节点之间似乎存在非常轻微的延迟。音频引擎使用下图实现:

//
//AVAudioPlayerNode1 -->
//AVAudioPlayerNode2 -->
//AVAudioPlayerNode3 --> AVAudioMixerNode --> AVAudioUnitVarispeed ---> AvAudioOutputNode
//AVAudioPlayerNode4 -->                                            |
//AVAudioPlayerNode5 -->                                        AudioTap
//      |                                                         
//AVAudioPCMBuffers    
//

我正在使用以下代码加载示例并同时安排它们:

- (void)scheduleInitialAudioBlock:(SBScheduledAudioBlock *)block {
    for (int i = 0; i < 5; i++) {
        NSString *path = [self assetPathForChannel:i trackItem:block.trackItem]; //this fetches the right audio file path to be played
        AVAudioPCMBuffer *buffer = [self bufferFromFile:path];
        [block.buffers addObject:buffer];
    }

    AVAudioTime *time = [[AVAudioTime alloc] initWithSampleTime:0 atRate:1.0];
    for (int i = 0; i < 5; i++) {
        [inputNodes[i] scheduleBuffer:block.buffers[i]
                                   atTime:time
                                  options:AVAudioPlayerNodeBufferInterrupts
                        completionHandler:nil];
    }
}

- (AVAudioPCMBuffer *)bufferFromFile:(NSString *)filePath {
    NSURL *fileURl = [NSURL fileURLWithPath:filePath];
    NSError *error;
    AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:fileURl commonFormat:AVAudioPCMFormatFloat32 interleaved:NO error:&error];
    if (error) {
        return nil;
    }

    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFile.processingFormat frameCapacity:audioFile.length];
    [audioFile readIntoBuffer:buffer frameCount:audioFile.length error:&error];

    if (error) {
        return nil;
    }

    return buffer;
}

我注意到这个问题只能在设备上感知,我正在使用 iPhone5s 进行测试,但我无法弄清楚为什么音频文件播放不同步,任何帮助将不胜感激。

** 答案**

我们最终使用以下代码对问题进行了排序:

AVAudioTime *startTime = nil;

for (AVAudioPlayerNode *node in inputNodes) {
    if(startTime == nil) {
        const float kStartDelayTime = 0.1; // sec
        AVAudioFormat *outputFormat = [node outputFormatForBus:0];
        AVAudioFramePosition startSampleTime = node.lastRenderTime.sampleTime + kStartDelayTime * outputFormat.sampleRate;
        startTime = [AVAudioTime timeWithSampleTime:startSampleTime atRate:outputFormat.sampleRate];
    }

    [node playAtTime:startTime];
}

这给了每个 AVAudioInputNode 足够的时间来加载缓冲区并修复了我们所有的音频同步问题。希望这对其他人有帮助!

【问题讨论】:

  • 好吧,你有多个播放器节点,你需要在每个节点上调用play 方法。我的猜测是这个调用立即开始处理并在音频线程中抛出一些东西。由于每个play 在不同的时间被调用,它可能会导致这种延迟。我的推理是否正确,是否可以批量启动播放器节点我不知道。我实际上正在使用 Apple 开发人员支持票,其中我提出了类似的问题。如果我得到任何明确的答案,我会回到这个问题。
  • 问题根本不在于 playAt: 方法。它恰好用于批量启动。您只需要使用一个具体的锚定时间。请参阅下面的答案...
  • 因此,尽管您在 ANSWER 中提供了解决方案,但如果设备需要超过 0.1 秒的时间才能完成操作,您的问题仍然存在。只要您没有正确安排它们,这些都是神奇的数字......
  • 嗨,丹尼,我的目标是从指定时间播放 AVAudioPlayerNode。我得到缓冲区,并在时间调度缓冲区,然后调用 playat time 方法。播放器未从指定时间开始播放。而是从头开始播放。
  • 每次玩都需要安排缓冲吗?

标签: ios objective-c iphone ipad core-audio


【解决方案1】:

我使用 Apple 开发人员支持票来解决我自己的 AVAudioEngine 问题,其中一个问题与您的问题完全相同。我得到了这个代码来试试:

AudioTimeStamp myAudioQueueStartTime = {0};
UInt32 theNumberOfSecondsInTheFuture = 5;
Float64 hostTimeFreq = CAHostTimeBase::GetFrequency();
UInt64 startHostTime = CAHostTimeBase::GetCurrentTime()+theNumberOfSecondsInTheFuture*hostTimeFreq;
myAudioQueueStartTime.mFlags = kAudioTimeStampHostTimeValid;
myAudioQueueStartTime.mHostTime = startHostTime;
AVAudioTime *time = [AVAudioTime timeWithAudioTimeStamp:&myAudioQueueStartTime sampleRate:_file.processingFormat.sampleRate];

除了在天网时代安排播放而不是未来 5 秒,它仍然没有同步两个 AVAudioPlayerNodes(当我切换 GetCurrentTime() 以获得一些任意值以实际管理播放节点时)。

因此,无法同时同步两个或更多节点是一个错误(由 Apple 支持部门确认)。一般来说,如果您不必使用 AVAudioEngine 引入的东西(并且您不知道如何将其翻译成 AUGraph),我建议您改用 AUGraph。实现起来有点开销,但你可以更好地控制它。

【讨论】:

  • 我们最终使用以下代码对问题进行了排序:AVAudioTime *startTime = nil; for (AVAudioPlayerNode *node in inputNodes) { if(startTime == nil) { const float kStartDelayTime = 0.1; // sec AVAudioFormat *outputFormat = [node outputFormatForBus:0]; AVAudioFramePosition startSampleTime = node.lastRenderTime.sampleTime + kStartDelayTime * outputFormat.sampleRate; startTime = [AVAudioTime timeWithSampleTime:startSampleTime atRate:outputFormat.sampleRate]; } [node playAtTime:startTime]; }
  • 我没有将所有的 AVInputNode 一起播放,而是将它们安排在 0.1 秒后,以便有足够的时间让缓冲区在每个通道上加载。这解决了问题。
  • 问题根本不在于 playAt: 方法。它恰好用于批量启动。您只需要使用一个具体的锚定时间。请参阅下面的答案...
  • @DannyBravo - 如果设备需要超过 0.1 秒的时间才能完成动作,您的问题仍然存在。只要您没有正确安排它们,这些都是神奇的数字......
【解决方案2】:

问题:

好吧,问题是您在 playAt: 之前的每次 for 循环运行中检索您的 player.lastRenderTime

因此,您实际上会为每位玩家获得不同的现在时间!

这样做的方式,你不妨用 play:playAtTime 启动循环中的所有玩家:nil !!!如果失去同步,您会遇到相同的结果...

出于同样的原因,您的播放器在不同的设备上以不同的方式运行不同步,具体取决于机器的速度;-) 您的 现在-次是随机的幻数 - 所以,如果它们恰好在您的场景中工作,不要假设它们总是有效。即使是因为繁忙的运行循环或 CPU 造成的最小延迟也会让你再次失去同步......

解决方案:

您真正需要做的是获得 now = player.lastRenderTime 之前的 ONE 离散快照循环并使用这个相同的锚点,以便为所有播放器获得批量同步开始。

这样您甚至不需要延迟播放器的启动。诚然,系统会剪掉一些前导帧 - (当然每个玩家的数量相同 ;-) - 以补偿您最近设置的现在之间的差异(实际上已经过去了)和实际的 playTime(在不久的将来仍然存在),但最终会完全同步启动所有播放器就好像您过去真的在 现在 开始使用它们一样。这些剪裁的帧几乎从不引人注意,您可以放心地响应响应...

如果您碰巧需要这些帧 - 因为在文件/段/缓冲区开始时会听到咔哒声或伪影 - 那么,通过启动您的 现在玩家延迟。但是当然你会在点击开始按钮后得到一点延迟 - 虽然当然仍然完美同步......

结论:

这里的重点是为所有玩家提供一个单一的参考 now-time 并调用您的 playAtTime:nownow-reference 后尽快使用 /strong> 方法。间隙越大,截断的前导帧部分就越大 - 除非您提供合理的开始延迟并将其添加到您的现在-时间,这当然会导致点击开始按钮后出现延迟启动形式的无响应。

并且始终注意这样一个事实 - 无论音频缓冲机制在任何设备上产生什么延迟 - 它不会影响任何数量的同步性播放器如果以正确的方式完成,上述方式!它不会延迟您的音频!只是让您听到音频的窗口稍后会打开...


请注意:

  • 如果您选择无延迟超级响应开始选项 和 无论出于何种原因,都会产生很大的延迟( 捕获现在和播放器的实际开始),您将 剪掉你的主要部分(最多约 300 毫秒/0.3 秒) 声音的。这意味着当您启动播放器时,它将立即启动 但不是从您最近暂停的位置恢复,而是 (最多约 300 毫秒)稍后在您的音频中。所以声学感知是 pause-play 会在旅途中剪掉一部分音频,尽管一切都完美同步。
  • 作为您在 playAtTime:now + 中提供的开始延迟 myProvidedDelay 方法调用是一个固定的常量值(不会得到 动态调整以适应缓冲延迟或其他变化 重系统负载时的参数)甚至适用于延迟选项 提供的延迟时间小于约 300ms 会导致 如果依赖于设备的准备,则剪辑领先的音频样本 时间超过了您提供的延迟时间。
  • 最大剪裁量(按设计)不大于 这些〜300毫秒。为了得到证明,只需强制进行受控(样本准确)的削波 领先的框架,例如现在添加负延迟时间 并且您将通过增加这个来感知不断增长的剪辑音频部分 负值。每个大于〜300ms的负值都会得到 整流到~300ms。因此,提供的 30 秒负延迟将 导致与负值 10、6、3 或 1 相同的行为 秒,当然也包括负0.8,下0.5秒 到 ~0.3

此示例很好地用于演示目的,但不应在生产代码中使用负延迟值。


注意:

在多人游戏设置中最重要的是让您的 player.pause 保持同步。截至 2016 年 6 月,AVAudioPlayerNode 中仍然没有同步退出策略。

只需在两个 player.pause 调用之间进行一点方法查找或将某些内容注销到控制台,就可能会强制执行后一个甚至是比前一个晚更多的帧/样本。因此,您的播放器实际上不会及时停在相同的相对位置。最重要的是 - 不同的设备会产生不同的行为......

如果您现在以上述(同步)方式启动它们,那么您上次暂停的这些不同步的当前玩家位置肯定会强制同步到您的新现在 - 在每个 playAtTime 的位置: - 这实质上意味着您正在将丢失的样本/帧传播到未来随着您的播放器的每次新开始。这当然会随着每个新的开始/暂停周期而增加,并扩大差距。这样做五十或一百次,您已经获得了不错的延迟效果,而无需使用效果音频单元;-)

由于我们无法(通过提供的系统)控制这个因素,唯一的补救措施是在其他在它们之间没有任何东西的紧密序列,就像您在下面的示例中看到的那样。不要将它们放入 for 循环或类似的东西中 - 这将保证在播放器的下一次暂停/开始时结束不同步...

将这些调用保持在一起是 100% 完美的解决方案,还是任何大 CPU 负载下的运行循环都可能会偶然干扰并强制将暂停调用彼此分离并导致丢帧 - 我不知道 - 在至少在几周内弄乱了 AVAudioNode API,我绝不会强迫我的多人游戏设置不同步 - 但是,我仍然对这种不同步的感觉不太舒服或安全, random-magic-number 暂停解决方案...


代码示例和替代方案:

如果您的引擎已经在运行,您将在 AVAudioNode 中获得 @property lastRenderTime - 您的播放器的超类 - 这是您实现 100% 样本帧准确同步的门票。 .

AVAudioFormat *outputFormat = [playerA outputFormatForBus:0];

const float kStartDelayTime = 0.0; // seconds - in case you wanna delay the start

AVAudioFramePosition now = playerA.lastRenderTime.sampleTime;

AVAudioTime *startTime = [AVAudioTime timeWithSampleTime:(now + (kStartDelayTime * outputFormat.sampleRate)) atRate:outputFormat.sampleRate];

[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];

[player...

顺便说一句 - 您可以使用 AVAudioPlayer/AVAudioRecorder 类实现相同的 100% 样本帧准确结果...

NSTimeInterval startDelayTime = 0.0; // seconds - in case you wanna delay the start
NSTimeInterval now = playerA.deviceCurrentTime;

NSTimeIntervall startTime = now + startDelayTime;

[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];

[player...

如果没有 startDelayTime,所有播放器的前 100-200 毫秒将被剪掉,因为尽管播放器已经在 开始(嗯,已安排)100% 同步,但 start 命令实际上需要时间进入运行循环现在。但是有了 startDelayTime = 0.25,你就可以开始了。并且永远不要忘记提前prepareToPlay您的播放器,这样在开始时无需进行额外的缓冲或设置 - 只需启动它们即可;-)

【讨论】:

  • 我使用您的快照技术(单个lastCurrentTime 值)同步启动多个AVAudioPlayerNodes。然而,播放似乎延迟了约 250 毫秒。有什么想法吗?
  • 首先,很好的答案。你还在 Stack Overflow 上活跃吗?我在这个确切的问题上遇到了困难。您的回答使我朝着正确的方向前进,但是我需要前约 200 毫秒的音频,但它们仍然被切断。你能帮忙吗?
  • 你所说的“仍然被切断......”是什么意思 - 尽管是什么,“仍然”?尽管设置了 kStartDelayTime?
  • 但是startTime对于每个玩家来说都是不同的,因为每个玩家都有自己的lastRenderTime。我不明白的另一件事是deviceCurrentTime — 找不到这样的属性。
  • 很好的答案!谢谢你!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多