游戏引擎学习第142天

今天的计划

欢迎来到这个游戏开发项目,我们将从零开始编写一个完整的游戏,并且不会使用任何现成的库或引擎。整个开发过程中涉及的所有代码都会被完整展示,包括游戏运行所需的每一个细节。无论是哪款游戏,最终都需要有人编写底层代码,即使是使用商业引擎,这些引擎也是由开发者构建的。因此,这里将展示整个游戏开发的完整过程,而不仅仅是表层的功能实现,而是所有底层技术的细节。

今天的主要任务是对音频混合器(sound mixer)进行进一步完善。目前,该音频混合器已经具备一些基本功能,比如可以动态地从磁盘流式加载音频文件(streaming audio from disk),这是一个较为高级的特性。然而,它仍然相对简单,还没有提供足够的控制能力,例如,目前只能直接设置音量,但无法随着时间推移动态调整音量,比如实现淡入淡出(fade in/out)效果。因此,今天的目标是为音频混合器增加这些控制功能,使其能够更好地满足未来游戏开发过程中对音效环境的需求。

在之前的开发过程中,已经将音频处理相关的代码整理到一个独立的文件中。今天的工作将基于此文件进行扩展,继续完善音频系统的功能。

公告:有时候,自己写代码比复用别人的代码更快

在代码复用方面,确实有时候使用现成的库可能是一个选择,但前提是这些库的质量足够高,分发机制足够完善,开发人员能够始终保持稳定可靠的编程质量。然而,现实情况往往并非如此。很多时候,如果具备足够的开发经验,自己编写代码的效率往往比尝试让一个预先构建的库正常运行要高得多。

更关键的是,在项目接近完成时,如果发现所依赖的库存在难以修复的 Bug,或者出现了一些不可预测的行为,而自己对这部分代码完全不了解,这时就不得不花大量时间去研究这些外部代码,甚至需要重新实现部分功能。这样一来,使用外部库可能最终会导致更高的时间成本。因此,从这个角度来看,直接自己编写所需功能,反而可以避免这些潜在问题。

此外,如果编写代码时保持清晰的逻辑结构,避免陷入复杂冗余的面向对象架构或不必要的现代编程模式,那么实际需要的代码量通常会比想象中要少得多。只要专注于编写真正需要的代码,而不是引入额外的复杂性,许多功能的实现实际上并不需要太多代码。

举例来说,我们在过去两周里,每天仅用一小时,就完成了整个资源流加载(asset streaming)和音频处理相关的功能。这些代码总量相对较少,但已经能够实现完整的动态音频流处理。因此,值得注意的是,一旦掌握了必要的编程技巧,从零实现这些功能并不像外界想象的那样困难,反而可能比使用第三方库更加直接高效。

当然,如果完全不想深入学习这些底层技术,依赖外部库也是一种选择。但对于希望提升自身编程能力,并愿意在一定程度上掌控代码质量的开发者来说,亲自实现核心功能通常更具长期价值。从整体效率来看,编写自己的代码通常比花费时间去适应和调试外部库更加可行,尤其是当这些库并不能完全满足需求时。

因此,这里再次强调为什么选择从零开始编写所有代码。通过实际观察代码量的大小,可以清楚地看到,这些功能的实现远比预想的要简单。掌握一定编程能力后,从头开始编写自己的系统不仅是可行的,甚至可能是更优的选择。接下来,将继续深入分析现有的代码,并探讨下一步的改进方向。

音频与视频动画的区别及其对音频混合架构的影响

当前的音频播放系统已经支持基本的音量控制,但我们希望进一步扩展它,使音量可以随时间变化,实现动画效果。然而,音频动画和图像动画在本质上存在很大不同,因此在音频系统中实现动画需要采用不同的方法。

首先,图形渲染的更新频率相对较低。即使在 VR 环境下,最高帧率通常也不会超过 90 帧/秒,而在复杂游戏场景中,帧率甚至可能降至 30 帧/秒。常见的游戏通常采用 60 帧/秒的刷新率。然而,音频处理则完全不同,其采样率通常高达 48,000 次/秒,这意味着音频信号的更新频率远远高于图像。

由于这一显著的频率差异,音频系统的架构不能简单地套用图像渲染的处理方式。例如,在渲染系统中,通常会将静态元素批量存储为列表,并直接渲染这些列表,而不需要每一帧都计算动画信息。然而,音频处理不适用于这种模式。例如,在当前的音频输出循环中,每输出一帧图像,音频系统通常需要处理 800 个音频帧(即音频采样点)。这意味着,音频系统无法采用与图像系统相同的架构,即将大部分动画计算放在高层组件,而让底层仅负责最终合成。相反,音频动画的一部分核心逻辑必须直接嵌入混音器(mixer)中,因为音频帧的计算频率太高,必须在音频处理循环内部完成计算。

因此,在音频系统中,音量的变化等动画效果必须直接集成到混音器中,而不能放到更高层的引擎组件中。这不仅是为了架构上的合理性,更是出于必要性。由于音频帧的计算速率极高,音量的变化必须在每个音频帧计算时实时调整,而不能依赖一个低频的外部控制系统来进行批量处理。

此外,这种情况在图形渲染中较为罕见,通常只有在某些特殊情况下,动画计算才会下沉至渲染器内部,例如某些实时光照或后处理特效。但在音频系统中,动画计算直接成为核心组件。例如,音量变化和音频频率调整是音频处理的基本功能,因此必须直接在混音器内部进行处理,而不是在更高层的逻辑中控制。

总结而言,由于音频的高采样率特性,音频动画的实现方式与图像动画不同,必须直接在音频处理循环内部完成计算,而不能依赖更高层的动画控制系统。因此,音量动画等功能将直接集成到混音器中,以确保音频处理的高效性和实时性。接下来,我们将进一步探讨如何具体实现这一功能。

我们希望随时间改变音量,同时避免不连续性

我们希望在音频系统中引入音量随时间变化的功能,而不是直接瞬间改变音量。因此,需要将音量概念拆分为两个部分:当前音量(CurrentVolume)和目标音量(target volume)。

未来,当我们调整音量时,不会直接将其设置为新值,而是会设置一个目标音量,并请求混音器逐步调整到该值,而不是立即发生不连续的音量变化。这样可以避免突然的音量跳变,使音频平滑过渡。

首先,我们需要在系统中引入这一概念,确保代码中涉及音量设置的部分可以接受目标音量的设定,同时不会丢失当前音量的信息。因为平滑调整音量需要知道当前音量的状态,因此我们将当前音量和目标音量分开存储。

接下来,在音频处理循环中,我们需要在每个采样点逐步调整音量,以确保音量变化是平滑的。这个过程需要在音频帧之间逐渐插值,从当前音量向目标音量过渡,而不是瞬间切换。

在实现这一功能时,我们可以采用不同的方法,每种方法都有其优缺点。不同的方法可能在计算复杂度、平滑程度和性能消耗上存在权衡,因此我们会先实现最直接的版本,并在完成后评估是否需要改进。此外,我们也可以参考 SIMD(单指令多数据)优化版本,以提高计算效率,进一步权衡不同方案的适用性。

总的来说,我们的目标是确保音量变化流畅,避免突兀的音量跳变,同时兼顾性能和实现的简洁性。接下来,我们将开始具体实现这一功能。

定义 ChangeVolume(改变音量)接口

我们需要扩展音量控制的概念,使得外部代码可以动态调整播放中的音量,而不仅仅是在播放开始时设定音量。因此,我们计划添加一个新的接口,比如 ChangeVolume,用于修改正在播放的声音的音量。

当调用 ChangeVolume 时,调用方需要传入以下参数:

  1. 正在播放的声音对象,用于识别需要调整的音频。Sound
  2. 音频系统的状态(如果有必要)。AudioState
  3. 目标音量值,用于指定左右声道(或多声道)的目标音量。
  4. 音量变化速率,用于控制音量变化的平滑程度,即音量调整所需的时间。

目前,我们的系统还没有存储音量变化速率的地方,因此需要增加一个变量来记录它。这个速率参数的作用是在当前音量和目标音量之间提供一个平滑过渡,而不是瞬间切换到新音量。

接下来,我们将在代码中加入相关的存储结构,并在音频混音处理过程中实现音量随时间变化的逻辑。

两种指定音量变化速度的方法:达到目标音量所需的时间 vs 变化率

我们希望在音频播放过程中支持动态音量变化,并提供平滑的音量过渡机制。为此,我们引入了 ChangeVolume 接口,该接口允许外部调用方修改当前播放音频的音量,并控制音量变化的速度,使其不会瞬间跳变,而是按照一定的时间渐变到目标音量。

音量变化方式的选择

我们需要确定音量变化的速率,而速率的计算方式可以有两种:

  1. 固定时长方式:指定音量变化所需的总时间(例如 1 秒内完成音量变化),在此时间内音量线性过渡到目标值。
  2. 固定变化速率方式:设定一个固定的音量变化速率,使音量按固定速率增加或减少,这意味着如果目标音量和当前音量的差距较大,音量调整时间会更长,反之则更短。

经过权衡,我们选择 固定时长方式,即用户直接指定音量变化所需的时间(如 2 秒内音量从当前值过渡到目标值)。这种方式更符合实际使用需求,比如音乐淡入淡出通常希望在特定的时间段完成,而不是按照固定速率变化。此外,这种方式更直观,用户无需关心当前音量值和目标音量值之间的差距,而是直接设定所需的时间。

实现逻辑

  1. 参数设计
    ChangeVolume 方法接受:

    • 播放中的音频对象(用于标识需要调整的音频)。
    • 目标音量(左右声道或多声道音量值)。
    • 音量过渡时长(以秒为单位,如果设定为 0,则直接跳变至目标音量)。
  2. 计算音量变化速率

    • 如果 时长为 0,直接将当前音量设为目标音量,不进行过渡处理。
    • 否则,计算音量变化速度:
      D CurrentVolume = TargetVolume − CurrentVolume FadeDurationInSeconds D_{\text{CurrentVolume}} = \frac{\text{TargetVolume} - \text{CurrentVolume}}{\text{FadeDurationInSeconds}} DCurrentVolume=FadeDurationInSecondsTargetVolume−CurrentVolume
      这个速率表示音量在每秒应该变化的量。
  3. 音量变化存储结构

    • 当前音量 (CurrentVolume):音频当前的播放音量。
    • 目标音量 (TargetVolume):希望音量达到的目标值。
    • 音量变化速率 (dCurrentVolume):根据设定的时间计算出的音量变化速率。
    • 初始状态下,目标音量 = 当前音量 ,且 音量变化速率设为 0,确保音频在未主动更改音量时不会发生变化。
  4. 音量更新逻辑

    • 在音频混音处理的循环中,每次迭代时更新当前音量:
      CurrentVolume + = D CurrentVolume × time step \text{CurrentVolume} += D_{\text{CurrentVolume}} \times \text{time step} CurrentVolume+=DCurrentVolume×time step
    • 这样,随着时间推进,音量会逐渐过渡到目标值,而不会产生突兀的音量变化。
  5. 向量化优化

    • 由于音频可能是多声道的,因此可以将 CurrentVolume、target volume 和 D CurrentVolume 设计为 向量 (vector) ,避免为多个声道单独编写代码,提高代码可读性和可扩展性。例如:
      • 立体声 (左右声道):用 vec2
      • 5.1 声道:用 vec5
    • 这样,计算音量更新时可以直接使用向量运算,减少冗余代码,提高灵活性。

后续改进

当前实现是最基础的音量变化逻辑,后续可能还需优化:

  • 支持非线性渐变(如指数衰减、缓动函数等)以提供更自然的听觉体验。
  • 优化计算方式,减少浮点运算,提高性能。
  • 更复杂的音量动画控制,如关键帧动画或基于时间曲线的音量变化。

目前,我们的基础音量变化机制已经实现,可以开始测试其效果,并在后续根据实际使用需求进行进一步优化。

通过量纲分析(dimensional analysis)计算 dVolume(音量变化量)

我们希望对音量进行逐步调整,而不是瞬间改变音量,因此我们需要在每个采样点上进行变化。当前的音量变化率是以"每秒变化多少音量"来定义的,但我们希望将其转换为"每个采样点变化多少音量"。为了做到这一点,我们可以使用量纲分析的方法。

在这个上下文中,量纲分析的目的是将音量变化率从"每秒变化多少音量"转换为"每个采样点变化多少音量"。为了实现这一点,我们需要对单位进行分析和转换。

假设我们的目标是根据"每秒变化的音量"(即单位为"音量/秒")来计算每个采样点的音量变化量。音频处理系统的采样率是 48000 采样点/秒,因此每秒钟会有 48000 个采样点。

1. 初始情况:音量变化率为每秒多少变化

假设我们有一个音量变化率 V rate V_{\text{rate}} Vrate,单位是"音量/秒"(例如,单位为分贝 dB/s,或音量单位的其他标准)。这个变化率是我们想要逐步调整音量的速率。

d C u r r e n t V o l u m e = TargetVolume − CurrentVolume Duration = Δ V Δ t dCurrentVolume = \frac{{\text{TargetVolume} - \text{CurrentVolume}}}{{\text{Duration}}} = \frac{\Delta V}{\Delta t} dCurrentVolume=DurationTargetVolume−CurrentVolume=ΔtΔV

其中:

  • Δ V \Delta V ΔV 是音量的变化量
  • Δ t \Delta t Δt 是时间间隔,以秒为单位

这个表达式意味着我们每秒钟音量会变化 d C u r r e n t V o l u m e dCurrentVolume dCurrentVolume 的量。

2. 转换为每个采样点的变化量

但是在音频处理中,我们通常是按照每个采样点来处理音量的。因此,我们需要将"每秒变化的音量"转换为"每个采样点变化的音量"。我们知道音频系统的采样率是每秒 48000 个采样点。因此,1 秒钟内的每个采样点的时间间隔为:

Δ t sample = 1 SamplesPerSecond \Delta t_{\text{sample}} = \frac{1}{\text{SamplesPerSecond}} Δtsample=SamplesPerSecond1

为了将音量变化率从"每秒变化的音量"转换为"每个采样点变化的音量",我们需要做如下的单位转换:

d V o l u m e = V rate × Δ t sample dVolume = V_{\text{rate}} \times {\Delta t_{\text{sample}}} dVolume=Vrate×Δtsample

接下来,我们需要确保音量按正确的步长进行调整。我们会在音频处理循环中,根据计算出的步长,每次迭代调整音量。然而,仅仅这样处理会导致音量持续变化而不会停止,因此我们还需要判断音量是否已经到达目标值。如果当前音量已经达到目标音量,则停止调整,并将音量值直接设定为目标音量,确保不会超调。

为了进一步优化,我们还需要考虑多通道的情况。目前,我们的系统只支持左右声道(双声道),但为了扩展性,我们定义了 OutputChannelCount 变量,以便在未来支持更多通道。在遍历音量调整时,我们会根据每个声道的当前音量和目标音量来判断是否应该停止调整。

目前的实现仍然存在一个问题:由于循环是固定步长的,音量可能会略微超出目标值。因此,我们需要进一步优化,让循环在音量到达目标值的瞬间停止,而不是依赖额外的判断逻辑进行修正。这部分优化将在后续实现。

以在调试模式下保持合理的帧率,打开Win32RefreshRate = 60;

在进行音量渐变操作时,我们考虑到现在代码运行的速度足够快,可以在调试模式下进行编译并保持 30 帧每秒的更新速度。这使得在调试模式下进行开发变得更加方便。于是,决定打开Win32RefreshRate = 60;

接下来,在调试模式下进行音量渐变处理时,目标是将音量渐渐地降低。为了实现这一点,可以设定一个目标音量(例如将其设置为0,即淡出)以及一个递减的音量变化率(例如每秒递减 0.2)。这样,音量会在大约 5 秒内逐步降低至零。

但是,这时还需要注意几个细节。虽然我们能够设定目标音量和音量变化率,但为了确保音量变化能够生效,代码中必须在每次计算音量变化后将新的音量值写回到音频系统中。否则,音量变化将不会被应用,程序将无法正确地调整音量。

在这过程中,也出现了一些复杂性,比如要在每一帧结束时更新音量,如果要避免与每秒的采样频率(samples per second)产生依赖关系,可能需要额外的计算工作。然而,经过进一步思考,发现并不需要额外的复杂操作,只需要直接使用当前的音量计算值即可更新音量。

通过设置一个断点,可以调试音量的变化,并设置目标音量和当前音量,以便观察音量逐渐变化的过程。目标是测试音量渐变的效果,确保音量逐渐降低至目标值,最终达到预期的淡出效果。

这种音量渐变操作并不复杂,只需要确保每一帧的音量更新被正确地应用。

避免超过目标音量

在处理音量渐变时,问题的核心在于如何避免音量变化超出目标值。由于音量渐变过程通常涉及约800个步骤,所以在每个步骤中,需要确保音量变化不会超出预设的目标值。如果音量渐变达到目标音量的中间位置时,不希望继续改变音量,因此需要采取措施避免音量超过目标值。

为了解决这个问题,可以通过在每次处理音量变化时,计算出音量变化需要多少个采样步骤。在渐变过程中,当音量即将达到目标时,可以判断剩余的音频样本数(SamplesToMix)是否会超过目标音量所需要的样本数。如果会超出,可以将剩余的样本数截断,确保音量不会继续变化,达到目标音量时停止音量变化。

此外,可以引入一个标志位(VolumeEnded)来记录每个通道的音量是否已经达到目标。这样,音量渐变完成后,就可以避免继续进行不必要的检查。在处理音量变化时,如果音量正在变化,则需要计算出从当前音量到目标音量所需的样本数,然后将其四舍五入到最接近的整数值。通过这种方式,确保音量在达到目标时停止,不会发生过度变化。

为了实现这一点,可以通过将剩余样本数与目标样本数进行比较,判断是否需要截断当前的音量变化。若需要,便调整剩余样本数并记录音量变化结束。此时,音量变化会按照预期停止。

在调试过程中,可以设置一个测试案例,例如将目标音量设置为左声道0.0,右声道0.5,并将当前音量设置为0.1。这样,音量会在大约10秒内逐渐减小。通过断点和观察,我们可以确认音量变化是否按预期停止,并且逐步验证音量是否正确达到目标值。

最后,音量渐变操作得到了验证,音量变化的动画效果已经实现,并且可以处理音量变化、声道平移等操作。不过,下一步可能会涉及到音高的变化,这是一个与音量变化不同的问题,但可以在之后的工作中继续探讨。

当前方法的问题:浮点数重复加法可能导致误差

在使用逐步增加增量值的方式进行音量变化时,存在一些潜在的问题,主要是在精度方面。具体来说,当音量变化需要在多个采样步骤中逐步完成时,由于每个步骤只改变一个小的增量,这种方式可能导致音量无法精确地达到目标值,尤其是在大量的采样步骤(比如1600个采样步骤)中。虽然我们希望音量变化在一定的时间内完成,但如果我们通过多次小的增量(比如在1600个步骤中每次增加相同的量)来实现这个目标,可能会出现精度上的问题。

这种做法在理论上是可以接受的,但实际操作中,由于浮点数运算的精度问题,会引入一些误差。浮点数运算本身在计算机中并不是完全精确的,每次小的增量可能会带来误差的积累,从而导致音量的最终值与目标值之间出现细微的偏差。这种误差的积累可能会导致音量值没有精确地达到预期的目标,进而可能影响到最终的音效表现,尤其是在高精度音频处理的场景下。

因此,虽然这种逐步增加增量的方法在理论上是可行的,但它可能会导致在音量变化的过程中出现精度问题,尤其是在处理大量步骤时。

另一种方法:在起始和结束音量之间进行线性插值(linear interpolation)

另一种处理音量变化的方式是直接设定一个目标音量,并通过每次将当前音量乘以一个比例来逐步达到目标音量。具体做法是,可以使用当前的采样索引,将其除以总的采样数来计算当前音量变化的进度。然后,利用线性插值计算从初始音量到目标音量的变化。这样,每次都会根据进度计算出一个新的音量值。

然而,这种方法并不是最佳选择,因为它需要在每一步中进行一次倒数运算(如计算倒数或除法)。虽然这种方法从理论上来说是准确的,但每次都进行倒数运算会增加计算的复杂性,特别是如果音量变化步骤很多时,这种计算可能会影响效率。

不过,随着现代处理器的性能提升,计算速度已经不再是瓶颈,尤其是执行除法运算的速度大幅提高。因此,有时反而不必过于在意避免这种运算,特别是在需要更精确和稳健的计算时。处理器现在执行这类数学运算的速度已经足够快,甚至可以考虑使用更直接、更精确的方式进行计算,而不是为了效率而削减精度。

另一种方法:反向求解(backsolving),确保在循环结束时达到目标音量

另一种方法是在音量变化的过程中,不调整循环的步伐,而是调整音量变化的增量(Delta volume),以确保在循环结束时正好达到目标音量。如果音量在循环结束之前已经达到目标值,那么我们可以通过减少Delta的增量,来确保音量变化在循环的末尾刚好达到预期的音量。也就是说,目标是通过反向推算出需要的Delta音量值,而不是通过计算采样点来调整进度。

这两种方法在理论上并没有明显的优劣,关键在于是否能解决实际问题,或者代码中是否有特定的需求需要采用某种方式。如果没有实际的音频问题或者性能上的需求,可能就不需要在这方面做过多调整。现在考虑到当前的实现方式已经能够正常工作,可能就暂时不去深入改进,尤其是在现阶段进行简单的测试时。

不过,随着代码的优化,尤其是在处理更复杂的循环时,可能会需要考虑这些调整。例如,如果在优化后的循环中,可能会同时处理多个样本(例如每次处理四个样本),那么此时可能需要重新评估这些调整的方式,以确保更高效的计算。所以,目前还不需要急于改变方法,未来可能会根据实际需求和问题做进一步的调整。

交互式测试 ChangeVolume

我们现在来测试音量变化的代码,发现目前并没有调用音量变化的功能,因此需要做一些修改和测试。首先,在播放音乐的地方,我们将音频播放的句柄保存下来,这样就可以方便地控制音量变化了。在代码中,可以在game_state中先设置一个音频对象,如"playing_sound *Music",这样方便后续的测试和调用。

接下来,我们修改代码以便在音频系统中调用ChangeVolume函数,实际操作时可以选择将音量淡入或淡出,这取决于按钮的按下状态。例如,按下某个按钮时,音量会逐渐增大(淡入),而按下另一个按钮时,音量逐渐减小(淡出)。对于这种变化,我们还设置了一个淡入淡出的时间参数(比如10秒),以便进行更为直观的测试。

在测试中,发现一个问题:代码没有设置目标音量,因此音量变化功能并没有按照预期工作。需要确保设置了目标音量(Sound->TargetVolume = Volume;),并且确保在计算音量变化时,增量(Delta)已经按要求进行调整。

通过修复了这个问题后,代码开始正常工作。可以看到,当设置了目标音量后,左声道的音量变化成功,音频的淡入和淡出功能也能正常运行。此外,还可以通过代码实现音频的左右声道平移,即在左声道和右声道之间移动声音,效果良好。

最后,去除了一些调试用的断言,确保音频的播放和音量调整在正常的代码流程中进行。测试完成后,计划接下来实现频率变化的功能,这是下一个步骤,准备开始频率变化的代码实现。

你能再展示一次如何跳过失败的断言(assertion)吗?通过查看反汇编代码的方式?

我们可以通过跳过断言来继续执行程序,下面展示了如何在调试过程中跳过一个断言。首先,如果使用的是系统的断言机制,一般可以选择"ignore"来跳过断言而不引发问题,但如果没有使用系统的断言机制,就无法利用"ignore"功能。因此,可以采取其他方法来绕过断言。

一种常见的方法是使用手动编写的断言,像在某些系统中会使用"野蛮断言"(poor man's assert)。我们通过向空指针写入数据来触发内存故障,操作系统会认为这是一次非法的内存操作,从而终止程序。这时,程序会中断,进入调试状态。在调试器中,我们可以查看到由于非法内存操作导致的故障,从而找到相关的汇编代码。

在调试过程中,如果希望跳过断言并继续执行程序,可以通过修改汇编代码的控制流来实现。比如,在汇编代码中,断言检查会设置一个条件标志(比如通过"test"指令),如果条件满足就跳过当前的代码行。我们可以直接右键点击相关代码行,选择"set next statement"(设置下一条语句),这样程序就会跳到断言检查后面,继续执行剩余的代码。

对于某些断言,如果不想每次都跳过,可以在代码中引入一个"ignore"变量。当这个变量为真时,断言就会被忽略。在调试时,可以修改这个变量的值,设置为"true"后,程序在后续执行中就会跳过断言,直到程序重新启动。

这种方式虽然算是比较简单的方法,但也能解决调试时的断言问题。如果需要更高效、更规范的断言机制,最终还是需要实现一个更完善的断言函数,能做更复杂的错误处理,而不仅仅是触发内存故障。

通过这种方式,我们可以灵活地跳过不需要的断言,继续程序的调试工作。如果遇到更加复杂的调试需求,可以进一步优化这类断言机制。

有没有情况需要以非线性方式增加音量?

在音量控制方面,通常我们可以使用线性方式增加音量,但有时也可以考虑非线性增加。然而,通常来说,耳朵对这种变化的感知并不像其他事物那么敏感,所以线性变化通常已经足够。大多数情况下,控制音量变化的速度比控制音量变化的曲线形状更为重要。也就是说,虽然音量变化的形状是可以调整的,但通常并不需要对其进行过多的调整。

但这并不意味着所有情况都适用这个标准。如果游戏的核心是声音,比如音乐游戏,可能就需要更为复杂的声音系统和控制。比如《摇滚乐队》这类游戏,其玩法依赖于音效和声音的复杂性,而不像大多数游戏那样,声音仅仅是附加在游戏体验中的一部分。因此,对于一款不以声音为核心的游戏来说,使用简单的线性音量变化就已经可以达到专业质量了。

需要注意的是,在当前这样的游戏中,我们做的是基本的音频处理,并且确保其质量达到专业水平,但这并不意味着在其他情况下,某些更复杂的音效系统不必要。每个项目根据其目标的不同,可能会需要不同层次的音频控制。如果游戏的核心是声音,开发者就需要深入研究和设计音频系统,否则简单的音量控制就足够了。

所以,通常情况下我们不需要做得过于复杂,但这并不意味着在某些特定情况下,复杂的音效设计不是必需的。

目前只能改变单个声音的音量。我们是否应该能够调整主输出(Master Output)的音量?

对于音量控制,除了可以调整单个声音的音量外,还可以考虑控制主输出的音量。实现这一功能是完全可行的,通常可以采用一种简单的方式。比如,我们可以设定一个主音量值,然后在音频状态中默认这个主音量等于默认的音量值。接下来,在处理音频数据时,我们会在每个音量的计算中,提前将主音量乘进去。

具体来说,当获取音量时,可以通过在音频数据处理的循环中直接将主音量乘进去,而不必在进入和退出时都进行计算。这样做可以避免在每次处理时都涉及到复杂的计算。如果这种做法带来的性能消耗不是很大,通常可以接受。

如果后续发现这种做法对性能有显著影响,也可以考虑优化,比如不在每次循环中都乘以主音量,而是进行其他的调整。但总体来说,添加一个主音量控制其实是相当简单的,主要是在音频处理的过程中将主音量乘进去即可。

会支持空间化音效(spatialization)吗?比如:如果声音来源在屏幕左侧,音效主要在左声道,右侧则主要在右声道?

空间化音效的意思是根据声音的位置将其分配到不同的音频通道。例如,如果声音位于屏幕的左侧,那么它主要通过左声道播放;如果声音位于右侧,则通过右声道播放。这种功能在混音器中已经得到了支持,但是否在实际游戏中使用这一功能,取决于我们自己。具体是否启用这一功能,我们需要等游戏开发进展到一定阶段后,实际听听游戏的音效混合效果,看看这种空间化的声音表现是否合适,是否会让人觉得太分散或干扰,而我们是否更倾向于让声音听起来更集中一些。

关于实现这一功能的难度,其实并不算太高,主要是在声音播放时,确定其位置并将相应的音量或声道分配给对应的左右声道。这是一个技术上可行且比较常见的做法。不过,最终是否使用这个功能,还需要根据游戏整体的音效表现来决定。

当窗口失去焦点/重新获得焦点时,实现音乐淡入淡出(fade-out/fade-in)有多难?

在窗口失去焦点或重新获得焦点时实现音乐的淡入淡出效果,目前在混音器中已经很容易实现,因为我们已经在混音器中实现了这个功能,所以操作起来非常简单。然而,处理Windows系统的音频时,可能会更复杂,因为Windows系统会限制我们在应用不活跃时访问音频设备。

目前,音频设置是基于应用程序处于活动状态时才能访问音频设备,而当应用程序不活跃时,则停止播放声音。为了实现窗口失去焦点或重新获得焦点时的淡入淡出效果,需要修改当前的音频设备设置。这意味着我们需要更改请求音频缓冲区的方式,修改音频设备的初始化代码,告诉Windows即使应用程序不活跃,也希望能够继续播放音频。

虽然这样做会涉及一些更改,但这是可以实现的。如果这个功能非常重要,可以根据需要进行调整。

如果最终方案不是线性插值(linear interpolation),那 dVolume 是怎么处理的?

在实现音量变化时,使用的是线性插值(linear interpolation),即每次在音量的起始值和目标值之间做等分变化。这意味着音量在设定的时间内会均匀变化,按固定的步长从一个值平滑过渡到另一个值。如果需要将音量的变化控制得更加精细或不均匀,可以使用不同的插值方法,但目前的做法是使用线性插值。

在音量调整的过程中,关键点是要根据每次变化的步长来调整音量的过渡,这样可以确保音量按预期的方式进行变化,并且不会因为浮动误差导致不精确的音量效果。

如果用 ?:(三元运算符)来实现 Assert,就可以在表达式或 if 语句不能使用的地方调用它了

使用三目运算符(? :)代替 if 语句来实现断言的想法,通常是为了在某些情况下能够将断言表达式嵌入到其他地方,尤其是在无法使用 if 语句的上下文中。例如,想要在一个复杂的表达式中做检查并且确保断言的结果依然有效。但使用断言的主要目的是在调试时检查程序的内部状态,断言会在发布版本中被编译掉,不会影响最终程序的行为。

通常情况下,断言用于确保程序在运行时状态符合预期。在调试版本中,如果断言失败,程序会中止,以便开发人员能及时发现问题。然而,断言会在发布版本中被剔除,因此它不应该影响程序的正常执行。

在某些情况下,开发者可能会希望通过三目运算符来实现更简洁的逻辑表达式,但通常来说,这种方式并不常见,因为它可能使代码更加复杂且难以维护。实际上,断言本身就是用来在开发和调试阶段检查条件,而不是在生产环境中执行复杂的逻辑检查。

使用三目运算符来做断言检查,并不会带来太多额外的好处,反而可能让代码更加难以理解,尤其是考虑到断言会在发布版本中被编译掉,可能会产生不必要的复杂性。

你已经实现音频削波(clipping)了吗?

目前还没有实现音频剪辑(audio clipping)功能。如果指的是音频的动态范围控制,比如限制音频信号的最大音量,我们还没有在系统中加入这种功能。到目前为止,音频的处理没有进行任何的"夹紧"(clamp)操作,以避免音量超过一定范围。如果需要的话,我们会在SIMD操作中加一个音频clamp操作,但不会涉及像压缩器这种复杂的音频处理技术。我们当前的目标是确保音频的音量在合理的范围内,而不会做过多的复杂操作。

指的是管理音频动态范围(dynamic range)------是的

目前没有实现音频压缩器(audio compressor),也不打算加入这个功能。虽然不能完全排除未来可能需要这样的功能,但目前不认为这个游戏会需要音频压缩器。这个决定主要基于对游戏的整体需求的预估,认为音频压缩器对这个项目来说并不必要。

关于 Assert,我不确定为什么要在表达式中使用它,但这似乎是通常的做法......应该没什么坏处吧?

关于assert的实现,讨论了为何不希望将assert放入某些表达式中。这样做可能引发潜在的问题,因为如果有人不小心在代码中放入了不合适的表达式,可能会导致错误。因此,不希望在没有明确理由的情况下启用这种用法。理论上,如果编译器能够捕捉到这种错误并导致编译错误,那就不应允许这种写法。除非确实能想到一种合理的用途,否则不会启用这种用法。

在代码开发中,并不喜欢限制代码的灵活性,以防止所有可能的错误,特别是那些不太可能发生的错误。但如果不能想到任何合法的原因来支持这种写法,并且如果编译器能够帮助检测这些错误,那么就会将这类写法视为非法。这是为了让编译器能够更好地帮助捕捉潜在的错误。

你不能直接对音频做硬削波(hard-clip)。如果不使用压缩器(compressor)或限制器(limiter),至少需要使用 tanh 之类的软削波(soft-clipping)方法

关于音频处理的问题,讨论了是否需要使用压缩器或限制器来防止硬性削波。实际上,很多游戏都采用了硬性削波处理,这在实际应用中并不常见,也不是一个大问题。虽然软削波可以减少音频失真,但硬削波在许多情况下并不会带来显著问题,尤其是在游戏中。大多数游戏中,硬削波的发生频率非常低,因此并不需要过度担心。

你会实现音频文件压缩(audio file compression)吗?

关于音频压缩的问题,讨论表示不打算在项目中实现专用的音频压缩器,因为这将花费大量时间,并且不会带来很多教育价值。实现压缩器类似于从规范中实现Vorbis音频解码器,过程繁琐且缺乏深入的见解,因此不愿投入过多精力。更倾向于通过基本的压缩器处理音频,接受文件尺寸较大的事实,认为压缩优化可以留到游戏完成后再做,而当前不想将大量时间浪费在这方面,因为觉得它并不是项目中最重要或最有意义的部分。

音频压缩器(audio compressor)是做什么的?

音频压缩器是一种尝试以非线性方式压缩音频范围的工具。通常,音频的音量变化是线性的,也就是说,音量范围从0到1,如果将某个音频样本乘以0.5,音量就会减半。问题在于,当音频中有非常大的声音和非常小的声音时,类似于HDR成像的效果,最终会因为线性压缩的方式导致没有足够的空间容纳所有声音。

为了解决这个问题,音频压缩器采用一种非线性的方式来映射声音的音量。这意味着随着声音变得越来越响,音量的增加幅度会逐渐变小。例如,较小的声音变化会显著增加音量,而较大的声音变化则不再增加太多音量。这种方式通过对声音进行曲线映射,而不是简单的线性放大,帮助音频的整体范围适应更小的空间。

具体来说,音频压缩器将较高音量的部分压缩,使其音量变化不再那么剧烈,从而平衡音频的整体范围。最终效果是,声音在达到一定响度后,不会无限制地变大,而是会趋于平稳,从而避免了音频中极大音量和微弱音量之间的失衡。

音频混合器(mixer)需要进行多少优化?

关于混音器的优化,其实需求可能非常少,甚至可能不需要做任何优化。混音器的当前实现已经相当高效,可能可以保持原样。而且与图形处理部分不同,图形部分因为需要做大量的优化来保证性能,所以我们开发了自己版本的GPU,但混音器并不像图形处理那样需要特别多的优化。混音器在其当前的实现下,可能已经非常高效,即使没有多线程处理,也不会造成太大性能问题。因此,不太可能需要对混音器进行深度优化。

限幅器(Limiter)和压缩器(Compressor)有什么区别?

总结上述内容:关于音频压缩器与限制器的区别,实际上,由于没有深入了解音频技术,所以无法给出确切的解释。一般来说,音频压缩器和限制器都是用于处理音频动态范围的工具,但其具体功能和实现方式有所不同,最佳的答案应该请教音频方面的专家。

对于音频的优化,混音器的优化需求其实非常低,目前的实现方式已经足够高效,可能不需要做任何大的优化。混音器不需要像图形部分那样进行复杂的优化,因此它的性能已经可以满足需求。

最后,关于下一步的内容,开发者计划继续进行音频编程工作,比如实现频率变化的功能,以便可以对声音进行小幅度的音调调整(例如小的音高变化),虽然这对当前的游戏并非必需,但能为音效增加一些多样性。这也是音频工程的一部分,可以让音频设计更具灵活性。

相关推荐
君莫愁。8 分钟前
【Unity】搭建基于字典(Dictionary)和泛型列表(List)的音频系统
数据结构·unity·c#·游戏引擎·音频
蓑衣客VS索尼克1 小时前
无感方波开环强拖总结
经验分享·单片机·学习
肥肠可耐的西西公主2 小时前
前端(AJAX)学习笔记(CLASS 4):进阶
前端·笔记·学习
云上艺旅2 小时前
K8S学习之基础十五:k8s中Deployment扩容缩容
学习·docker·云原生·kubernetes·k8s
亭墨3 小时前
linux0.11内核源码修仙传第五章——内存初始化(主存与缓存)
linux·c语言·驱动开发·学习·缓存·系统架构
凡人的AI工具箱3 小时前
PyTorch深度学习框架60天进阶学习计划第14天:循环神经网络进阶
人工智能·pytorch·python·深度学习·学习·ai编程
傍晚冰川3 小时前
【江协科技STM32】ADC数模转换器-学习笔记
笔记·科技·stm32·单片机·嵌入式硬件·学习
知识分享小能手4 小时前
Html5学习教程,从入门到精通, HTML5 新的 Input 类型:语法知识点与案例代码(16)
开发语言·前端·学习·html·html5·web·java开发
小呀小萝卜儿4 小时前
2025-03-08 学习记录--C/C++-PTA 习题10-2 递归求阶乘和
c语言·学习
虾球xz6 小时前
游戏引擎学习第143天
学习·游戏引擎