Quartz概述

用于对样本进行精确调度的音频引擎系统

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毫秒的最糟糕情况。

Quartz.png

如果音频请求在缓冲区已经开始渲染之后才传入( 1 ),则该请求将在不播放( 2 )的情况下计入下一个周期,直至下一个缓冲区( 3 )开始。

让事情变得更加复杂的因素还包括,游戏线程更新函数高度可变(至少从音频渲染器的角度看是如此),并且容易受到任何卡顿(在垃圾回收、加载资产等情况时)的影响。

游戏线程更新函数还会为游戏和图形渲染计时,而不适用于音频渲染和音频计时。如果你需要在一个精确的时间点播放音效(正当不同音效结束或开始之时),从游戏线程调用该命令将不可行,因为游戏线程和音频渲染线程事件计时被解耦。

这些 可变延迟(variable latency) 依赖于游戏线程的音频事件计时(game-thread-dependent timing of audio events) 对于大部分音频应用程序来说都不算大问题。根据游戏的CPU负荷或特定平台的约束,通常可以提前将缓冲区大小和渲染的缓冲区数量调整到合适的大小,让大部分计时问题都低于可感知范围的阈值。

但是,对于某些音频应用程序,例如交互式音乐、精确计时的重复武器开火或者其他依赖于节奏的音频,这些小问题就变成了大问题。

Quartz和样本精确度

样本精确度(Sample accuracy) 能够让音效在音频渲染器中的任意样本上启动(例如缓冲区中心),而不是在缓冲区边界处启动。

Quartz 是一个能够解决可变延迟和游戏线程计时不兼容性问题的系统,它提供了一种可以精确播放任何音效样本的方法。样本精确度指的是能够让音效在音频缓冲区的任意样本(时间点)上渲染音频,,而不是在缓冲区开始时渲染。

Quartz1.png

Quartz提供了一方法,可以安排在缓冲区中间进行音频调用,而不必将音频渲染推迟到下一个缓冲区开始。

Quartz不是在音频缓冲区开始时渲染音效,而是指示音效在所需的音乐值(小节或拍子)或时间值(秒)上播放,无论缓冲区大小、游戏线程计时或其他可变延迟来源如何。

这就产生了很多的应用——从创建动态音乐系统,到控制依赖于节奏和计时的音效的播放,例如冲锋枪。

Quartz的工作方式

利用Quartz,你可以使用 PlayQuantized() 调度音效,此参数可以在音频渲染中的特定样本上执行命令,以提前向音频引擎发出警告。通过提前安排时间,你可以消除延迟,让音效可以在准确计算出的样本上进行渲染,使队列中的音效似乎在无延迟播放。

缓冲区大小也称为缓冲区回调大小,或简称为 回调大小(callback size)

Quartz2.png

A 表示游戏线程,用X轴界限表示游戏帧更新函数,而 B 表示音频渲染线程,用X轴界限表示渲染的音频缓冲区。它们各自的X轴在现实世界时间中垂直排列。 ( 1 ) 当游戏线程调用 PlayQuantized() 时,它会请求在给定的量化边界(例如下一个四分音符)上播放音效,此边界的计算方式为音频渲染线程上最接近的音频输出样本。( 2 ) Quartz以常规的 量化命令 形式发出此请求,并将音效排入队列,以便在将来的时间点对音频进行渲染。此请求会等待并通过音频渲染线程 ( 3 ) 上的多个音频缓冲区,具体视计算出的量而定。到了应该在计算出的缓冲区中渲染该音效时,Quartz将按照一个整数延迟来馈送音频,以防止在缓冲区 ( 4 ) 开始时进行渲染,而是在需要准确地在四分音符 ( 5 ) 上播放的样本上的输出缓冲区开始一段时间后进行渲染。

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时钟

以下是示例工作流:

  1. 要创建时钟,在 Quartz子系统(Quartz Subsystem) 上调用 创建新时钟(Create New Clock)

  2. 提供 时钟名称(Clock Name) 节拍记号(Time Signature)

  3. 将返回值存储为蓝图变量。(如果已存在具有相同名称的时钟,这会向现有时钟返回一个句柄。)

  4. 通过调用 时钟句柄(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)下:

OptionalPulseOverride_Values.png

对于 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)。

如果值提供的时长短于一个小节,将复制最后一个条目以达到正确的长度。

如果值提供的时长超过一个小节,则列表将会截断,并且将从列表开头重新开始计数,首个计数点在下一个小节的强拍上。

欢迎帮助改进虚幻引擎文档!请告诉我们该如何更好地为您服务。
填写问卷调查
取消