Choose your operating system:
Windows
macOS
Linux
音频渲染中的精确计时和延迟问题
在音频引擎中,出于对CPU性能的考量,音频样本是在 缓冲区(buffer) 中渲染的,然后分别提交到输出硬件——即数字音频转换器(DAC)。由于多种原因,这是在CPU上实时渲染音频的唯一可行方式——CPU缓存一致性、硬件API开销等。
这些缓冲区通常一次包含数百甚至数千个样本。必须理解的一点是,音频引擎渲染命令通常在音频缓冲区渲染开始时使用。例如,某个命令是播放新音效,停止旧音效,或更改音量或音高等音效参数。
因此,渲染的音频缓冲区大小直接决定着执行新命令的速度。此速度也决定了任何已发出的命令的可感知听觉延迟。例如,如果要触发爆炸VFX并在相同的游戏线程更新函数上触发爆炸声音,那么看到VFX爆炸和听到声音之间的延迟由此缓冲区大小决定。
这意味着,一个包含2048个样本并以每秒48k样本(kHz)速度进行渲染的缓冲区,将导致最高43毫秒(ms)的听觉延迟。
除了这种由于渲染缓冲区大小造成的内在音频渲染器延迟,由于游戏线程和音频线程更新函数以及一般线程通信开销,从游戏线程发布的音频命令也需要时间才能到达音频引擎。如果线程或游戏延迟为13毫秒(切实可行的数字),并且我们使用处理速度为48 kHz的2048样本大小的缓冲区,以及如果命令刚好错过了当前正在渲染的缓冲区的启动,将会出现累计延迟56毫秒的最糟糕情况。
让事情变得更加复杂的因素还包括,游戏线程更新函数高度可变(至少从音频渲染器的角度看是如此),并且容易受到任何卡顿(在垃圾回收、加载资产等情况时)的影响。
游戏线程更新函数还会为游戏和图形渲染计时,而不适用于音频渲染和音频计时。如果你需要在一个精确的时间点播放音效(正当不同音效结束或开始之时),从游戏线程调用该命令将不可行,因为游戏线程和音频渲染线程事件计时被解耦。
这些 可变延迟(variable latency) 和 依赖于游戏线程的音频事件计时(game-thread-dependent timing of audio events) 对于大部分音频应用程序来说都不算大问题。根据游戏的CPU负荷或特定平台的约束,通常可以提前将缓冲区大小和渲染的缓冲区数量调整到合适的大小,让大部分计时问题都低于可感知范围的阈值。
但是,对于某些音频应用程序,例如交互式音乐、精确计时的重复武器开火或者其他依赖于节奏的音频,这些小问题就变成了大问题。
Quartz和样本精确度
样本精确度(Sample accuracy) 能够让音效在音频渲染器中的任意样本上启动(例如缓冲区中心),而不是在缓冲区边界处启动。
Quartz 是一个能够解决可变延迟和游戏线程计时不兼容性问题的系统,它提供了一种可以精确播放任何音效样本的方法。样本精确度指的是能够让音效在音频缓冲区的任意样本(时间点)上渲染音频,,而不是在缓冲区开始时渲染。
Quartz不是在音频缓冲区开始时渲染音效,而是指示音效在所需的音乐值(小节或拍子)或时间值(秒)上播放,无论缓冲区大小、游戏线程计时或其他可变延迟来源如何。
这就产生了很多的应用——从创建动态音乐系统,到控制依赖于节奏和计时的音效的播放,例如冲锋枪。
Quartz的工作方式
利用Quartz,你可以使用 PlayQuantized()
调度音效,此参数可以在音频渲染中的特定样本上执行命令,以提前向音频引擎发出警告。通过提前安排时间,你可以消除延迟,让音效可以在准确计算出的样本上进行渲染,使队列中的音效似乎在无延迟播放。
缓冲区大小也称为缓冲区回调大小,或简称为 回调大小(callback size)。
Quartz还可以和 蓝图 搭配使用。可以使用蓝图来应用这些量化的命令和计时事件,而这些命令和事件又会与音频混合器中发生的量化操作同步触发游戏逻辑。
Quartz中的主要组件
Quartz时钟
时钟(clock) 是负责在音频渲染线程上调度和触发事件的对象。时钟是使用 Quartz子系统(Quartz Subsystem) 创建的,并使用 时钟句柄(Clock Handles) 通过蓝图进行修改。每个时钟都有一个 Quartz节拍器(Quartz Metronome)。
Quartz节拍器
节拍器(metronome) 是音频渲染线程对象,可以根据用户提供的信息(例如BPM(每分钟的拍子数)和)记录时间的流逝并决定何时需要执行接下来的命令。
游戏逻辑可以订阅节拍器上的事件,以在发生音乐时长时得到通知。
Quartz时钟句柄
时钟句柄(Clock Handle) 是 活动时钟 的游戏端代理。 这是通过 Quartz子系统(Quartz subsystem) 获得的,用于控制音频混合器中运行的时钟。
Quartz子系统
时钟句柄提供对特定时钟上功能的访问权限,而Quartz子系统提供对与特定时钟无关的功能的访问权限。这包括创建和获取时钟,查看时钟是否存在,以及查询延迟信息。
音频组件:PlayQuantized()
这是添加到音频组件的新函数,能够利用指定的 量化边界(Quantization Boundary) 在特定时钟上播放音效。
创建和初始化Quartz时钟
以下是示例工作流:
要创建时钟,在 Quartz子系统(Quartz Subsystem) 上调用 创建新时钟(Create New Clock)。
提供 时钟名称(Clock Name) 和 节拍记号(Time Signature)。
将返回值存储为蓝图变量。(如果已存在具有相同名称的时钟,这会向现有时钟返回一个句柄。)
通过调用 时钟句柄(Clock Handle) 上的其中一个函数,设置时钟的 滴答速率(Tick Rate):
每分钟拍子数(Beats per Minute) (显示在下面)
每次滴答的毫秒数(Milliseconds per Tick)
每秒滴答次数(Ticks per Second)
每分钟三十二分音符数(Thirty-second Notes per Minute)
步骤4中的调用在技术上是可互换的;使用各种变体是为了方便,但需要注意的是,每个变体都有自己相应的 getter。
在量化边界上播放音效
要在量化边界上播放音效,请在 音频组件(Audio Component) 上调用 Play Quantized
,并提供时钟句柄和量化边界。
Quartz量化边界(Quartz Quantization Boundary) 是你告诉音频混合器音效确切播放时间的方式。此参数在其他一些与调度相关的Quartz函数中使用。以下是此结构体中的一些选项。
量化(Quantization) 是要用于调度的时间值。可能的时长包括:
时长 |
说明 |
---|---|
小节 |
由节拍记号自动确定。 |
拍子 |
默认为节拍记号的分母,但可以改写。 |
1/32 |
三十二分音符是可用的最小时长。 |
1/16,(附点)1/16,1/16(三连音) |
十六分音符,附点十六分音符和十六分音符三连音 |
1/8,(附点)1/8,1/8(三连音) |
八分音符,附点八分音符和八分音符三连音 |
1/4,(附点)1/4,1/4(三连音) |
四分音符,附点四分音符和四分音符三连音 |
半,(附点)半,半(三连音) |
半分音符,附点半分音符和半分音符三连音 |
全,(附点)全,全(三连音) |
全音符,附点全音符和全音符三连音全音符是可用的最大时长。 |
滴答 |
滴答声是最小的值,与三十二分音符相同。 |
乘数(Multiplier) 是截止音效应该播放时的量化时间值的数值。这是所选择的量化时间值的乘数。
计数参考点(Counting Reference Point) 是用于对事件进行调度的参考点。不同的用例需要使用不同的参考点。
相对小节(Bar Relative)—从当前小节开始时计算。例如,对于电子鼓:
将"量化"设置为 拍子(Beat),将"乘数"设置为 2.0 会为下一个发生的 拍子2(Beat 2)(测量过程中的第二个拍子)调度事件。
如果在 4 beats to the bar(@@@) 的 拍子1(Beat 1) 上设置了时钟,则事件将在下一个拍子上触发。
如果在 4个拍子中的第3个(Beat 3 of 4) 上设置了时钟,则事件将在下一个小节的拍子2上触发。
相对传输(Transport Relative)—从时钟开始时计算,或者从上次重置传输时计算。例如,在4小节边界上触发音乐符干:
这用于根据传输(歌曲计数器)来触发剪辑/符干。
如果时钟在小节6上,则事件将在小节9上触发(传输计数从小节1开始计数)。
相对当前时间(Current Time Relative)—从当前时间开始计算。例如,为了在触发针刺的同时提供音乐感:
将"量化"设置为"拍子"并将"乘数"设置为2.0,将会把事件安排在下一个拍子之后的强拍上。
如果时钟在4个拍子中的第1个拍子上,则事件将在第3个拍子上触发。
如果时钟在4个拍子中的第3个拍子上,则事件将在下一个小节的拍子1上触发。
在Quartz中,小节并非始终是完整的音符,拍子并非始终是四分音符。如需更多信息,请参见下面的节拍记号和节拍覆盖。
订阅命令事件
当你想要将游戏逻辑与量化事件同步时(例如在安排播放的音效的开头处),你可以在特定命令生命周期的不同阶段收到通知。
为了达到此目的,从这类函数的 委托中(In Delegate) 参数中拖出一个引脚,并创建 自定义事件(custom event):
根据不同的需要,可以多次调用自定义事件。在图中的示例中,你可以中断事件类型 EQuantizationCommandDelegateSubType
枚举来响应以下事件:
事件类型 |
说明 |
---|---|
排队失败 |
命令未能进入音频渲染线程(Play Quantized很可能无法通过并发性检查)。 |
已排队 |
音频渲染线程已收到命令,正在等待执行(通过了并发性检查) |
已取消 |
命令在执行之前已在音频渲染线程上取消(很可能在开始播放之前就已经在音频组件中叫停)。 |
即将开始 |
命令即将执行(当前是在与"已开始(Started)"相同的游戏帧上调用该命令,,但将来应尽早调用该命令,以补偿线程之间的延迟。)。 |
已开始 |
已在音频渲染线程上执行了命令。 |
订阅节拍器事件
除了订阅 量化命令(Quantized Command) 生命周期的各个阶段以同步游戏和音频渲染,你还可以在每次发生 量化时长(Quantization Duration) 时得到通知。
在下例中,我们订阅了 小节(bar) 通知。 每当我们的事件触发时,都会安排一个音效在以下小节上播放:
如果你希望访问单个事件中的所有量化时长,可以立刻订阅所有 量化事件(Quantization Events) 并开启 量化类型(Quantization Type) 枚举:
节拍记号和节拍覆盖
Quartz支持对节拍记号和仪表进行更加精细的控制。如上文创建和初始化Quartz时钟中所述,在创建时钟时,你将需要提供 节拍记号(Time Signature)。
节拍记号具有三个字段。前两个表示任意节拍记号:
拍子数(Num Beats):每次测量的拍子数("拍子类型"下指定的类型),或是节拍记号的 分子(numerator)。
拍子类型(Beat Type):枚举表示用于获取拍子的时长,或者节拍记号的 分母(denominator)。可能的值为:
枚举
数值
/2
半分音符
/4
四分音符
/8
八分音符
/16
十六分音符
/32
三十二分音符
可选节拍覆盖(Optional Pulse Override) 是第三个字段,可用于指定让哪个时长在整个小节中获取拍子。如果未提供"可选节拍覆盖"则默认为节拍记号中的 拍子类型(Beat Type)。
小节量化边界(Bar Quantization Boundary) 是由节拍记号直观控制的。在4/4节拍中,小节持续4个四分音符,而在12/8节拍中,小节将持续12个八分音符。
"节拍覆盖"参数实际上是节拍覆盖的数组。每个条目都可以按照指定的 时长(Duration) 来指定 节拍数(Number of Pulses),然后再移动到数组中的下一个条目。请参阅以下示例:
节拍记号为7/8。 如果不提供可选节拍覆盖,拍子量化边界将与/8(八分音符)量化边界相同。
但是利用Quartz,你可以指定拍子(或节拍)可以持续的确切时长。本例中,数组中有两个条目,显示在详细信息(Details )面板的默认值( Default Value)下:
对于 0值(0 value):
节拍数:2(Number of Pulses:2)
节拍时长:1/4(Pulse Duration: 1/4)
这意味着我们的前两个拍子的时长将是一个四分音符。
对于 1值(0 value):
节拍数:1(Number of Pulses:1
节拍时长:(附点)1/4(Pulse Duration: (dotted)1/4)
这意味着最后一个拍子的时长将是一个附点四分音符。
这会强制执行(1, 2) (1, 2) (1, 2, 3)的计数,其中加粗的 1 是节拍所在的八分音符。如果你的蓝图订阅了 拍子(Beat) 定量边界,这是触发事件的时间。
如果循环切换条目,则计数将变成(1, 2, 3) (1, 2) (1, 2)。
如果值提供的时长短于一个小节,将复制最后一个条目以达到正确的长度。
如果值提供的时长超过一个小节,则列表将会截断,并且将从列表开头重新开始计数,首个计数点在下一个小节的强拍上。