游戏引擎学习第143天

仓库:https://gitee.com/mrxiao_com/2d_game_3

回顾并规划今天的内容

目前,我们正在进行声音混合的开发。我们已经写好了声音混合器,并且已经实现了一些功能,比如声音流播放和音量插值。过去一周我们做了很多工作,进展非常快。不过,我们还没有实现的一点是:让请求或播放声音的人能够动态调整声音的频率。

接下来,我们希望能够实现这样的功能,让声音的频率可以动态调整。让我们讨论一下为什么可能会需要这样做。

讨论使用音量变化和音高偏移来生成丰富的音效景观,且仅使用少量音效资源

在游戏开发中,声音常常用于强化某些活动或事件的效果。比如,当角色跳起来时播放跳跃音效,或者当角色脚步踩到地面时播放脚步声。除此之外,还有一些环境音效,比如树枝的沙沙声或者风吹树叶的声音,这些是为了增强游戏的沉浸感。所有这些声音都会被传入声音系统中,声音系统负责播放与这些事件相关的声音。

然而,游戏开发的现实情况往往是有限的资源。可能会受到多种因素的限制,比如创建声音的费用、处理声音的时间、存储声音的空间、下载时占用的带宽等等。因此,很难为每种情况都录制独特且不同的声音。例如,可能只有五个不同的草地脚步声,播放的时候这些声音会反复出现,随着角色不断跑步,脚步声会频繁被触发。这种重复性会让声音显得单调,并且可能会让玩家感到厌烦,尤其是当目标是创建一个真实的户外环境时。

为了解决这个问题,可以通过调节音量来增加一些变化。我们可以利用声音混合器来实现音量的变化,使得每次播放时声音的强弱有所不同,但这并不能完全解决声音重复带来的单调感。更好的方法是使用频率调整,也就是对声音的音高进行微调。比如,我们可以将草地脚步声的音高稍微提高或降低一点,这样就能在不改变音效内容的前提下,为声音加入更多的变化。这种频率的微调不会对声音造成剧烈变化,而是通过细微的音高变化来避免声音的重复感。

另外,这种频率调整不仅可以增加音效的变化,还可以与游戏中的其他元素进行交互。例如,当玩家击中一个敌人时,可以通过调整音效的频率来反馈敌人受到的伤害,或者根据敌人血量的减少,调整音效的频率。这些变化不仅有助于打破声音的单调,还可以让玩家通过声音获得更多的反馈信息,提升游戏的互动性和沉浸感。

总的来说,音量和频率是非常实用的声音调节工具,它们可以大大提升声音系统的可变性,并且实现起来相对简单。因此,几乎所有游戏都能从音量和频率的调整中受益,尤其是在资源有限的情况下,这两种调整能够提供更多的声音变化,并让游戏声音更具活力和多样性。

如何进行音高偏移

我们之前讨论过,声音的波形中,峰与峰之间的距离,也就是声波的波长,决定了声音的音高。这个波长是衡量声波变化的标准,可以把波长理解为从一个峰到下一个峰的距离,或者从一个峰到一个波谷的距离。声波的频率,即每秒钟发生多少次完整的波动,决定了声音的音高。简单来说,波形中的振荡速度决定了音高的高低,无论波形的复杂程度如何,耳朵感知到的音高只是这些振荡速度的表现。

为了改变声音的音高,我们需要加速或减慢这些振荡的速率,也就是改变波长的长度。如果波长变长,音高就会降低;如果波长变短,音高就会提高。这就是声音变化的基本原理。在前几天介绍声音的那集里,我们也提到过这一点。

改变音高的方式有很多种,其中有些方法会非常复杂,通常用于那些需要在不改变声音时长的情况下大幅调整音高的场景。比如,假设我们有一个一秒钟长的声音,它有一个特定的音高,现在我们想提高这个音高,但又不改变声音的持续时间。这种操作就比较复杂。传统的信号处理方法或小波变换方法效果较差,虽然它们也能做到,但并不理想。现代一些更先进的方法,如圆柱重采样(cylindrical resampling),效果更好,但这些方法相对较少使用,且实现起来复杂。

幸运的是,针对我们现在的需求,这些复杂的方法并不是必须的。我们只需要对声音进行轻微的音高调整,而不必担心对时间持续性进行大规模的修改。因此,相对简单的音高调整方法就足够满足我们的需求,避免了复杂的计算和实现。

对小幅度音高变化的妥协

我们讨论的音高变化主要是轻微的音高调整,因此我们不需要使用复杂的算法。对于轻微的音高变化,我们并不关心声音的时长是否发生很大的变化。即便声音的时长稍微变长或变短,比如从一秒变成1.1秒,这样的变化对听感并不会有明显影响,我们感知到的只是音高发生了变化,而不会感到时长上的区别。所以,我们的目标是通过调整播放速度来改变音高。

数字化的声音其实是由一系列样本点组成的,这些样本点代表了声音波形在不同时间点的值。我们将这些值保存在一个样本缓冲区(sample buffer)中,每个样本通常是一个16位整数,表示该时间点波形的幅度。因此,我们实际上并没有原始的波形,而只是这些样本点,它们是波形的一个近似值。如果我们想要更改音高,实际上就是要改变播放这些样本点的速度,而不是直接重采样整个曲线。

我们不能简单地在任何采样率下重新采样这些样本,因为我们并不知道原始的波形是什么样的。我们只有这些样本点,它们是对波形的一个分段线性近似。只有在通过扬声器播放时,这些样本才会重建成波形,并发出真实的声音。然而,由于固定的采样频率,播放过程中会出现一些失真或伪影,这属于信号处理的范畴。虽然这些细节存在,但我们不需要深入讨论这些问题,特别是因为这些内容更适合信号处理专家去探讨。

简而言之,为了实现音高调整,我们将通过改变播放的速度来达到轻微的音高变化,而不需要重新构造复杂的信号处理方法。

决定在实时混音中使用线性插值

在处理声音时,所使用的音频数据是由一系列线性样本点组成的,我们并没有完整的波形信息。换句话说,实际处理的只是波形的一个线性近似。如果我们想要平滑地重新采样这些样本点,就需要对这些点之间的间隔做出一定的推测。虽然信号处理领域有许多更精确的重采样方法,但对于我们所说的微小音高变化,使用简单的线性插值就足够了。这种方法非常快速且简单,非常适合实时音频混音,特别是当我们不希望消耗太多 CPU 资源时。

因此,我们的目标是,除了像之前那样直接按照固定时间切片播放声音外,还希望通过插值来实现音高的平滑调整。具体来说,我们可以使用类似于我们在光栅化器中使用的双线性插值的方式,但由于这里只处理的是一维数据(音频样本),而不是二维图像数据,所以我们使用的是线性插值。回想一下,在图像处理时,我们需要在四个角之间插值,而音频数据只有两个相邻的点需要插值,因此这里的插值过程只是一个单阶段的线性滤波操作。

为了实现这一点,我们需要做的第一步就是在相邻的样本点之间进行插值,这样就可以在不同的播放速率下输出平滑过渡的声音,达到微调音高的效果。这种方法与图像处理中双线性插值的两个步骤相似,只不过在音频处理时,只有一个插值阶段,即通过两个样本点进行插值。

线性混合的重要性

在之前的系列中,我们讨论过线性插值的公式:p' = (1 - T) * p0 + T * p1,这个公式非常重要,它是许多编程中的基础构建块。它特别适用于游戏开发,因为它涉及到了人类感知的许多方面。在线性混合的过程中,我们不断使用这个公式来处理各种感知上的任务。这也是为什么这个公式在很多场合都被频繁使用,尤其是在音频处理时,它同样能发挥重要作用。

在音频处理中,我们希望能够在声音的样本之间进行插值,从而平滑地调整音高。这个操作实际上与图像处理中的双线性插值非常相似,但音频处理的插值只需要进行简单的线性插值即可。具体来说,我们希望能够通过调整样本点的位置,在播放过程中实现音高的变化。为了实现这一点,我们需要做的是首先计算出当前样本点的浮动位置,然后根据这个位置与其邻近的两个点之间的值,进行插值操作,从而得到精确的播放效果。

一旦我们掌握了如何在音频数据中进行插值,我们就可以自由地调整声音的播放速度了。通过改变播放的速率,我们就能够改变音高。这种方法非常有效,且不会引起太明显的听觉问题,因为我们对音频的时间长度变化做了很好的控制,确保音频不会在不被察觉的情况下发生过大的变化。

简单版:选择最近的样本

在之前的拉伸音高的实现中,我们采用了类似于栅格化中的处理方法。最初,我们并没有进行复杂的插值,而是简单地使用点采样,从最近的采样点获取数据,这样可以避免处理的复杂性,并且能迅速实现一个基础版本。随后,我们引入了双线性插值,通过这种方法使得效果更加平滑,避免出现明显的跳跃和不自然的边缘。

在音频处理中,我们可以采用类似的策略。首先,考虑到音高的变化,我们可以从浮动点位置读取声音数据,直接读取最接近的采样点,而不是进行复杂的插值。这种方法的好处是实现起来非常简单,也能在一开始快速验证功能是否有效。理论上,这种方法可能会导致一些听起来不太愉悦的伪影,特别是在某些音效上,可能会有些不自然的表现。希望通过这种简单实现,我们能听到这些伪影,进而判断是否需要进一步优化。

接下来,如果能够听到这些不理想的效果,我们将引入线性插值的方式来解决这些问题,从而实现更加平滑、清晰的音高调整效果。这种方法不仅可以解决伪影问题,还能进一步提高音频播放的质量。

实现简单版

我们首先对音频播放的代码进行了一些修改。原本,我们使用的是uin32类型来表示音频样本的计数,但现在我们将其升级为real32类型,这样可以更加精确地跟踪当前播放的位置。这是因为使用浮动点数可以让我们在播放音频时更加灵活,能够处理精确的位置,而不仅仅是样本的整数索引。

接下来,我们需要修正一些警告,特别是在样本剩余数的计算上,我们使用了RoundReal32ToUInt32(real32)来确保能够正确地将浮动点数转换回整数,这样可以在播放过程中避免出现错误。通过这样的调整,音频的播放能够更加精确地管理样本的读取位置。

为了进一步增强音频播放的灵活性,我们决定允许样本索引的改变不再局限于固定的增量值1。为了实现这一点,我们将SampleIndex改为real32类型,这样可以根据需要调整增量,进而控制播放的速度。这意味着我们不仅仅是读取样本,而是基于浮动位置来确定当前播放的音频位置。

在代码中,我们首先定义了一个名为dSample的变量,它初始设置为1.0,用来表示每次增量的值。然后,SampleIndex被重新设计成sample position,这意味着我们将更精确地控制音频播放的位置,而不仅仅是单纯地用整数索引去查找样本。

通过这些调整,我们可以通过浮动点数精确控制样本的播放位置,从而实现音高的变化而不影响音频播放的连续性和自然性。

回到实现简单版

为了让音频的播放速度可以被动态调整,首先需要解决的一个问题是:在调整播放速度时,原本的样本剩余计算方式就不再适用,因为这假设了一个固定的采样速率。比如,如果播放速度翻倍,样本剩余计算就会出错。因此,我们需要一种新的方式来计算剩余的样本数量。

为了解决这个问题,我们决定引入一个新的浮动点类型变量,比如 float RealSamplesREmingInSound,来表示剩余的样本。我们首先计算如果音频按正常速率播放时,应该剩余多少样本,然后根据当前的播放速度调整这个值。播放速度通过 dSample 进行控制,dSample 定义了每个循环迭代时样本位置的增量,这使得样本播放的速度不再是固定的。

接着,我们在计算剩余样本时,不再是简单地加1,而是通过 dSample 的值来调整样本索引的步长。通过这种方式,可以控制播放的速度(加快或减慢)。例如,在播放速度加倍的情况下,dSample 值为2,意味着每次迭代样本索引增加2,从而使播放速度变得更快。

然而,调整样本索引并不是直接的,它需要确保样本位置始终在合法范围内,并且每次读取样本时都要根据当前的精确位置来选择最接近的样本。为了实现这一点,代码中使用了 RoundReal32ToUInt32() 函数来处理样本位置的四舍五入,从而保证索引正确。

在调试过程中,遇到了一些问题,尤其是当播放速度变化较大时,音频会出现一些异常的"断点",比如在1.4倍速度时,音频播放会出现奇怪的跳跃或失真。这提示我们可能在计算样本位置时,特别是接近四舍五入阈值时,存在某些问题。因此,需要进一步检查代码,看看是否在更新样本位置时遗漏了一些细节,或者在处理样本位置和播放速度之间的转换时产生了误差。

为了调试这些问题,首先我们会重新检查代码逻辑,确保在每次循环后正确更新样本位置。如果问题依旧存在,可以使用调试工具进行逐步跟踪,查看具体在哪一环节出现了问题,并据此进行修正。

最后,为了确保程序稳定运行,还需要考虑线程管理问题,特别是在使用多线程时,确保所有线程在重新加载动态链接库之前已经正常停止,这样可以避免潜在的崩溃或资源冲突问题。虽然这个问题在实际运行中不容易出现,但为了防止问题发生,最好还是加以注意。

添加样本的线性混合

接下来,我们决定加入线性插值(linear filtering)来平滑音频效果,尽管目前通过四舍五入已经能听出较好的效果,但我们还是希望加上插值以确保更好的音质。

实现方法与之前的做法类似,首先,我们需要取样本位置,并对其进行下取整(floor),从而得到一个整数样本索引。接下来,我们需要计算出样本位置的小数部分,这个小数部分会介于0到1之间,可以用来进行线性插值。

具体的步骤是,首先从样本索引中获取当前样本和下一个样本。然后,我们将这两个样本的值取出来,并转换为浮点数(real32),以便进行插值。之后,我们就可以利用线性插值来将这两个样本结合起来,生成一个平滑的结果。

我们用线性插值公式 lerp,其中 sample0sample1 分别是当前样本和下一个样本,而小数部分则是插值的权重。这样,就可以根据小数部分的值,在两个样本之间平滑过渡,产生更自然的声音。

为了测试,我们可以保留原来的代码路径,并引入新的插值路径,方便在需要时进行 A/B 测试,比较插值和非插值的效果。这将有助于我们确定是否需要使用插值,或仅仅依赖四舍五入的方式。

这样,当我们播放音频时,音频的质量应该会得到改善,尤其是在播放速度变化较大的情况下,线性插值能有效减少由采样间隔变化带来的音质失真。

比较使用和不使用线性插值的音效输出

经过测试,无法明显听出使用线性插值(linear filtering)与直接四舍五入(rounding)之间的区别。即便进行了对比,也难以分辨音频质量的变化。这可能是因为测试使用的音频不适合突出插值效果,因此很难察觉到实际的改进。

对于是否采用线性插值,目前看来可有可无,至少在当前的测试环境下,未发现明显的伪影(artifacts)或失真现象。这说明,四舍五入的方式已经足够处理当前的音频播放需求。或许需要换一组更合适的音频样本,才能真正感受到插值算法的作用。

总的来说,到目前为止,整个音频播放和处理逻辑已经基本完成。从处理播放速度的调整,到保证样本索引的正确性,再到尝试使用插值优化音质,所有关键环节都已经实现。除非后续发现更好的优化方式,否则当前的实现已经能够满足预期需求。

将 dSample 纳入 playing_sound

在当前的音频混合器中,我们已经具备了所有必要的功能,包括音高调整(pitch shifting)、实时音量调整(animated volume)以及流式音频播放(real-time streaming sound mixing)。其中,dSample 是我们用于控制播放速度的关键参数,我们可以直接从音频对象中读取这个值。为了方便管理,可以在音频对象(sound)内部存储 dSample,以便在播放音频时能够自由设定播放速度。当然,也可以在播放时动态传递这个参数,但目前的需求并不复杂,因此直接在音频对象中存储是较为简单直接的做法。

目前代码的实现已经非常简洁,实现了所有核心功能。唯一需要关注的是一些可能的边界情况(edge cases),这些可以在后续优化时再做进一步验证。当我们将代码下放到C级别(C implementation)时,可以进行更详细的检查,确保所有逻辑都正确无误。

从整体来看,这是一个相对简单的音频混合器实现,代码量不大,逻辑清晰,能够满足基本的音频播放、音高调整和音量调整需求。

在接下来的时间里,我们计划优化实时代码编辑(live code editing)功能,以确保它能够顺利与音频混合器协同工作。目前的代码在进行动态重载(reload)时,可能会因为多线程的存在导致一些问题,尤其是在音频播放线程尚未完全退出时进行 DLL 重新加载,可能会导致异常。因此,我们需要实现一个线程清理(flush threads)机制,在重新加载 DLL 之前,确保所有音频线程已经正确退出,以避免潜在的崩溃或异常情况。

这项优化可以提升开发体验,使实时代码编辑更加稳定,从而提高调试效率,减少因线程问题导致的意外错误。

打开警告

在重新加载游戏代码之前清空队列

为了确保在重新加载 DLL 之前所有线程都已正确退出,我们考虑在 Win32UnloadGameCode 之前执行一个线程队列(queue)清理操作。我们当前有两个任务队列:高优先级队列(high priority queue)和低优先级队列(low priority queue)。为了让这些队列中的所有任务执行完毕,我们调用 Win32CompleteAllWork,分别对高优先级队列和低优先级队列进行处理,确保它们都已经完成所有剩余的任务。

最初,我们尝试将队列清理逻辑放在 Win32UnloadGameCode 之中,但这可能不是最佳方案,因此决定在加载游戏代码之前进行清理。我们希望通过调用 Win32CompleteAllWork 来确保所有任务都已完成,以避免在 DLL 重新加载时出现问题。然而,初步测试表明,这个方法似乎没有完全解决问题,因此我们需要进一步排查问题的根源。

目前的问题可能出现在多个方面:

  1. 任务未正确同步 :即使 Win32CompleteAllWork 被调用,仍然可能存在未完成的任务,导致某些线程仍然在运行。
  2. 某些线程未正确退出:即使所有队列都已清理完成,某些线程可能仍然在等待或执行其他任务,导致代码重载时发生冲突。
  3. 需要更彻底的线程管理 :可能需要引入更严格的线程同步机制,例如使用 EventSemaphore 来确保所有线程都已正确结束。

为了进一步调试,我们计划跟踪 Win32CompleteAllWork 的执行流程,并检查是否有遗漏的任务或线程未正确终止。同时,我们可能需要调整队列的管理方式,确保在 DLL 重新加载之前,所有任务都已安全完成并且所有相关线程都已正确关闭。

在程序运行时编译热重载还是有错误

将字符串从游戏代码块中复制出来,避免热重载后崩溃

遇到的问题实际上只是一个字符串处理问题,而并非之前所担心的线程队列问题。具体来说,文件名无效,导致某些字符串无法正确解析。这一问题的根源在于,我们在实现 LiveCode 编辑功能时,已经意识到嵌入在代码中的字符串(即静态字符串)可能无法正确使用。然而,在当前的测试代码中,这些字符串仍然是直接定义的,因此在尝试使用它们时会遇到问题。

在实际的游戏运行中,这通常不会成为问题,因为这些文件名通常会从数据中加载,而不是直接嵌入代码。但由于目前的测试代码中,字符串是直接嵌入的,它们在 DLL 重新加载时可能会变得无效,进而导致文件名解析错误。

为了修复这一问题,我们计划在 AddSoundInfoAddBitmapInfo 这两个函数中,确保所有的字符串都被正确存储到内存分配区域(arena)中,而不是依赖代码中的静态字符串。具体实现方法如下:

  1. 使用 PushString 方法 :我们计划创建一个 PushString 函数,用于将字符串复制到适当的内存区域,而不是直接使用字符串字面量指针。
  2. AddSoundInfoAddBitmapInfo 处理字符串 :在 fileName 赋值时,我们不再直接使用字符串指针,而是调用 PushString,确保字符串存储在正确的内存区域中。

目前,我们还没有实现 PushString,但可以立即编写它,以确保所有字符串都被正确存储,并在 DLL 重新加载时不会丢失或变得无效。这样,我们可以避免因字符串指针无效而导致的问题,从而确保 LiveCode 编辑功能的稳定性。

实现 PushString

我们实现了 PushString 这个函数,它的作用是在指定的内存分配区域(arena)中存储字符串,以避免字符串指针在 DLL 重新加载后失效的问题。

首先,我们定义了 PushString,它接受一个 const char* 作为输入,同时需要提供一个内存 arena 来存储这个字符串。这个方法主要用于测试阶段,在生产环境中可能不需要,但目前我们希望能够确保字符串存储在我们的数据存储区域,而不是代码段,以避免 DLL 重新加载导致字符串指针无效的问题。

实现细节

  1. 确定字符串的大小

    • 由于字符串是以 \0 结尾的,因此需要遍历整个字符串来计算大小。
    • 我们需要包含 \0 终止符的大小,因此在计算时默认 size = 1
  2. 内存分配

    • 使用 PushSizearena 中分配相应大小的内存。
    • 这里的 alignment 维持默认值。
  3. 复制字符串

    • 通过 for 循环遍历字符串的每个字符,并将其复制到 arena 分配的内存中。
    • 目标内存 dest 通过 PushSize 申请的区域存储,源数据为 source,直接按字节复制。
  4. 优化思路(减少双重遍历)

    • 目前 PushString 需要两次遍历:
      1. 第一次遍历计算字符串大小。
      2. 第二次遍历复制字符。
    • 优化方案
      • 由于内存 arena 是单线程环境,可以直接在计算大小的同时,将字符复制到 arena 里。
      • 这样避免了双重遍历,提高效率,但在当前测试阶段,这个优化可能不是特别关键。

测试

在测试过程中,我们发现 PushString 之前并未被调用,后来发现是命名问题导致的调用错误。修正后,我们进行了以下验证:

  • 确保 PushString 正常调用,并正确计算字符串大小(包含 \0)。
  • arena 里正确分配并存储字符串。
  • 通过调试工具检查 dest,确保字符串正确复制。

作用与必要性

  1. 避免 DLL 重新加载导致的字符串指针失效

    • 在动态重载 DLL 时,存储在代码段(.rdata)的 const char* 可能失效。
    • 通过 PushString,字符串存储在 arena 里,使其在 DLL 重新加载后仍然有效。
  2. 支持 Live Code Editing(实时代码编辑)

    • 代码热重载时,不能依赖代码段中的静态数据。
    • 由于测试代码暂时没有从资产文件加载字符串,而是直接在代码里定义 const char*,所以目前仍然需要 PushString 进行内存管理。
  3. 测试代码的必要性

    • 在开发过程中,测试代码是不可避免的,而测试代码中的字符串可能导致临时问题。
    • 通过 PushString,可以在测试代码中安全使用字符串,而不会受到 DLL 重新加载的影响。
    • 这虽然增加了一些调试工作,但从长远来看,测试代码能够确保系统的稳定性,提升开发效率。

总结

  • PushString 作用 :在 arena 中存储字符串,避免 DLL 重新加载导致指针失效。
  • 实现方式 :计算字符串长度,分配内存,复制字符串,避免存储在 .rdata 段。
  • 优化思路:可以在计算字符串长度时,直接进行内存复制,避免双遍历。
  • 测试结果 :修正命名问题后,PushString 运行正常,成功存储字符串并通过调试验证数据正确性。
  • 长期作用 :在资产系统完整运行之前,PushString 是必需的;但未来字符串会从资产文件加载,这个问题将自然消失。

如何将一个样本的音量稍微调至左或右?

我们实现了基于鼠标位置的音量左右声道平移(panning)功能。具体来说,我们通过检测鼠标的 X 坐标来调整左右声道的音量比例,使得当鼠标位于屏幕左侧时,左声道音量较高,右声道音量较低;而当鼠标位于屏幕右侧时,右声道音量较高,左声道音量较低。

实现步骤

  1. 获取鼠标位置

    • 通过 PlatformMouseX 获取鼠标的 X 坐标。
  2. 计算音量比例

    • 计算鼠标位置在屏幕中的相对比例,即 鼠标X / 屏幕宽度
    • 这个值介于 0(最左)到 1(最右)之间。
  3. 设置左右声道音量

    • 右声道音量 = 鼠标X / 屏幕宽度(即 RightVolume)。
    • 左声道音量 = 1 - 右声道音量(即 LeftVolume)。
    • 这样,当鼠标在屏幕左端时,LeftVolume = 1RightVolume = 0;当鼠标在屏幕右端时,LeftVolume = 0RightVolume = 1;当鼠标在中间时,LeftVolume = 0.5RightVolume = 0.5,实现平滑过渡。
  4. 应用音量设置

    • 将计算出的 LeftVolumeRightVolume 赋值给音乐的音量控制参数。

代码实现

cpp 复制代码
float RightVolume = SafeRatio0(MouseX, ScreenWidth);
float LeftVolume = 1.0f - RightVolume;
ChangeVolume(&GameState->AudioState, GameState->Music, 0.01f, MusicVolume);
  • SafeRatio0(a, b) 是一个安全除法函数,防止 b0 造成除零错误。
  • SetMusicVolume 负责调整音量。

测试验证

  • 移动鼠标时音量变化

    • 当鼠标移到左侧,左声道音量最大,右声道静音。
    • 当鼠标移到右侧,右声道音量最大,左声道静音。
    • 在屏幕中间,左右声道音量均为 50%。
  • 验证平滑过渡

    • 通过实时调整 MouseX 位置,检查音量变化是否平滑,是否出现突然断音或失真。

不知道为什么这个地方会段错误

如果你进入一个时间扩展区域,音高是否会在几秒钟内变化,当你穿越障碍时?

在音频处理方面,我们探讨了是否需要在进入时间膨胀区域或穿越某个屏障时进行 音调(Pitch)偏移

是否需要动态音调偏移?

  • 目前没有明确的需求来实现 大幅度音调变化,因此没有必要在主音频循环中加入这类处理。
  • 我们的目标是实现 较为微妙的音调偏移,而不是大范围的音调变化。
  • 时间膨胀(Time Dilation) 本身已经可以自然地导致音调变化,因此如果有类似需求,可以通过时间膨胀机制来实现。

音调变化的影响

  • 微小的音调变化 可以增加声音的多样性,使其听起来不那么单调。
  • 但如果音调变化过于剧烈或频繁,可能会对听感造成干扰。
  • 由于我们调整的音调偏移范围较小,动态调整(如渐变过渡)的实际意义不大,因为人耳不会明显察觉这种缓慢的变化。

为什么不使用音调渐变?

  • 渐变(Ramping) 适用于从一个极端变化到另一个极端(如从 低音 变为 高音),这样才有明显的听觉效果。
  • 但在我们的场景中,音调调整只是为了增加 轻微的音频变化,而不是大规模的音高转换,因此不需要渐变处理。
  • 目前的做法是 设定一个音调后保持稳定,这样既能增加多样性,又不会带来额外的计算负担。

结论

  • 目前不需要 大幅度的音调变化,因此不在主循环中实现动态音调调整。
  • 时间膨胀 机制本身可以自然地带来音调变化,无需额外的 Pitch Shift 处理。
  • 由于我们的音调变化幅度较小,不适合使用 渐变(Ramping) 过渡。
  • 如果需要调整音调,可以直接修改 播放速率音频频率,但通常只在特殊场景下使用。

如果你想在不改变速度的情况下进行音高偏移,难道不可以通过当前样本和下一个样本之间快速而简洁的线性插值来实现吗?

在音频播放和处理方面,我们探讨了一些关键概念,特别是 线性插值(Linear Interpolation) 在音频播放速率调整中的作用,以及如何调整音量以避免剪切(Clipping)问题。

关于线性插值(Linear Interpolation)

  • 能否使用线性插值来调整音调,而不改变播放速度?

    • 不行 ,因为音频播放速度的变化是由于 播放指针(Play Cursor) 以不同于 1:1 速率读取样本导致的。
    • 线性插值的作用仅仅是 减少失真(Artifacts) ,确保在变速播放时不会产生明显的音频失真,而 不会改变播放速度本身
    • 例如,在 升高音调 时,播放指针会 更快 地前进,从而导致音频整体变短,即播放速度加快。
  • 为什么线性插值不能单独用于改变音调?

    • 线性插值仅用于在两个样本之间进行平滑过渡,以 减少音频失真,但它本身不会影响音频的时长或音调。
    • 如果不改变播放速率,仅靠线性插值是无法做到 提升音调但保持播放速度 的。
    • 真正的解决方案通常涉及更复杂的 音高变换(Pitch Shifting) 算法,如 相位声码器(Phase Vocoder)Granular Synthesis

关于音量和剪切(Clipping)

  • 音量过高会导致剪切(Clipping)
    • 当音频信号超过了允许的最大幅度(通常是 [-1,1] 或 16-bit PCM 的 [-32768, 32767]),就会发生剪切,导致失真。
    • 例如,当我们在调试音频时,如果音量过高,可能会出现 音频削波(Clipping) 的现象,使声音听起来失真。

你有没有讲解过适合做这类混音工作的测试音频文件是什么样的?

在音频混音中,使用纯正弦波进行测试是非常有效的。纯正弦波的声音简单而清晰,能够帮助我们在调整音频时准确地听到不同频率的细节。它们通常被认为是检查和调整音频质量的好工具,因为其波形不包含复杂的谐波成分,能够较为纯粹地展示音频设备的响应。

对于那些进行高质量音频处理的工作,纯正弦波特别有用,因为它能帮助识别低频和高频的任何失真或不准确之处。在混音过程中,尤其是在做细节调整时,纯正弦波能帮助清楚地听出音频设备的性能,确保音质的纯净。

另外,纯正弦波不仅适用于音频的测试,也能够在频率响应调整时提供很好的参考。通过在多个频段上测试音频的表现,可以更精确地确定设备的性能和音频信号的传输质量。这是确保音频最终呈现出最佳效果的重要手段。

总之,纯正弦波在音频测试中是一个非常实用的工具,它帮助工程师们更加精确地检测和优化音频的质量。在进行音频调整、设备检测和音频分析时,纯正弦波提供了一个清晰、稳定的基础,能够帮助我们辨别音频信号中的细微问题。

你是否曾使用过 C 标准库中的 memcpy?

在实际的编程工作中,我们有时会依赖于标准库中的函数,而不是自己编写的库。尽管在某些项目中,我们还没有开发出自己的CC(C语言编译器)库,但这并不妨碍我们在实际项目中使用标准库的功能。标准库提供了许多已经经过优化和广泛使用的工具,能够有效地帮助我们完成常见的编程任务,而不必重新发明轮子。

尽管如此,在日常的开发过程中,我们并不总是依赖于这些库,而是根据具体需求选择是否使用。有时候,基于效率、性能或特殊需求的考虑,可能需要开发自定义的库来满足特定的编程要求。这样可以确保程序更加符合特定的应用场景,或者提高代码的执行效率。

至于"黄色矩形"这一提法,可能是指某种图形界面的组件或工具,在上下文中可能与调试或显示有关,但具体细节尚不明确。通常在开发过程中,界面中的颜色和形状会起到某种视觉引导的作用,帮助开发者或用户识别信息。

黄色矩形是什么?

黄色矩形只是一个占位符,因为我们还没有为楼梯部分准备好艺术素材。因此,我们在测试一些缩放代码时使用了它。实际上,这一切只是测试用的内容,我们正在进行一些实验性的工作,测试不同的功能和效果。至于为什么使用黄色矩形,这个问题其实并没有特别的解释,它只是作为一个临时的视觉标识出现。

目前有很多不同的测试内容在进行中,虽然这些内容并不完整或者看起来有些不合常理,但它们是为了验证一些特定功能的有效性。我们正在进行一些内部的实验和调试,这些测试会随着工作进展逐步整理和完善。所以,看到这些不寻常的测试内容也是可以理解的,它们都是开发过程中暂时的产物。

总的来说,黄色矩形只是一个临时工具,用于处理开发过程中的一些具体需求,未来会被替换成实际的艺术资源。在此之前,这些测试内容将继续存在,直到相关功能被完成并最终整合进项目中。

你计划在游戏中加入哪些音效选项?比如玩家能否暂停游戏并静音音乐,但保留音效?还是音效将是固定的,就像控制器设置一样?

在游戏中,声音设置的选项可能会是固定的,而不是可自定义的。考虑到很多游戏的体验是由开发者精心设计的,声音和音乐是整体音效的一部分,因此我们更倾向于保持这种设计,而不是让玩家根据个人喜好随意更改。我们认为,音乐和音效是游戏氛围的重要组成部分,就像电影中的配乐一样,观看电影时并不会让你自己选择更换配乐,尽管在某些情况下,也许这会是一个更好的选择,但这并不是我们的首选。

虽然如此,考虑到作为一个教育项目的目标,每个游戏副本都会包含源代码。如果玩家有兴趣替换音乐,我们并不会提供一个简单的菜单来修改音乐,而是希望鼓励玩家通过学习一些编程知识,自己进入源代码并去除音乐部分。这不仅是一个很好的编程入门项目,而且能让玩家在实践中学习如何修改和定制游戏。

我们希望通过这种方式,玩家能够更深入地理解游戏的制作过程,从而激发他们自己动手修改游戏的兴趣。这种方式不仅能够帮助学习编程,还能增强玩家的参与感和成就感,让他们更有动力去探索游戏的其他方面,比如控制器设置等。

对于这个功能,你难道不会想使用另一种方式来控制音量的动画吗?通过设置一个速率,而不是时间长度?

在实现音量控制时,不希望使用基于速率(rate)的动画方式,而是希望音量的变化能够根据当前的鼠标位置精确地进行调整。具体来说,音量的变化应该与鼠标的位置变化直接关联,而不是通过设置一个固定的速率来控制变化的速度。

我们希望音量能够平滑地从当前的值过渡到新的值,而不是突然发生变化(即"爆音"现象)。为了避免这种情况,音量变化将通过插值来实现,插值的过程会在合理数量的采样点之间平滑过渡。这种方式确保了音量变化的平稳性,并且能够精确地反映鼠标位置的变化,而不会出现突兀的音量波动。

这种方法可以确保音量的过渡既自然又精确,符合我们对音频控制平滑度和精确度的要求。

如果玩家在跑步,你会把玩家的速度与音高挂钩,还是会添加更多的声音效果,比如草地上更多的脚步声?

在设计玩家角色的脚步声时,主要的调整是增加不同的声音效果,而不是直接将玩家的速度与音调(音高)相连接。具体来说,玩家移动速度较快时,脚步声的频率会根据步伐的频率发生变化,例如更频繁地触发草地上脚步声的音效。然而,玩家的移动速度也可能根据不同的地面类型影响音调的变化。

例如,玩家跑得更快时,可能会更用力地踩到地面或产生其他动作,这可能导致音调的变化。因此,虽然速度会影响音调(特别是根据表面类型),但音调变化并不是最主要的因素。最重要的变化是脚步声的触发频率,即在特定时间内脚步声的播放次数。通过根据玩家的速度来调整触发频率,能够更真实地模拟玩家的脚步声。

总的来说,增加不同的脚步声音效果是首要的,而通过速度调整脚步声的频率和音调则是为了增加声音的真实感。

一旦你发布这个源代码,是否意味着别人可以在此基础上进行修改、构建自己的游戏?

这款游戏的源代码将允许玩家在此基础上进行修改、构建并创作自己的游戏。实际上,源代码已经可以获取,只要玩家预购了游戏,就可以开始修改和开发源代码。不过,需要注意的是,在游戏正式发售之前,玩家不能将修改后的源代码用于发布商业游戏,因为在发售前会有一个时间限制。

这个时间限制会持续到游戏正式发布后的两年。两年过后,源代码就没有时间限制了,玩家可以自由地使用它来制作任何商业产品,进行商业发布。这种开放源代码的方式,旨在鼓励玩家学习和创作,同时也为那些希望将这些修改用于商业用途的人提供了机会。

你可能已经讲过这个问题,但操作系统是否自动为每个核心分配线程?那么是否无法强制让单个线程在其独立核心上运行?

操作系统负责决定每个线程在哪个核心上运行,因此程序员不能直接选择线程运行的核心。然而,由于性能优化的需求,特别是在某些缓存是特定核心本地的情况下,有时可能需要对线程的核心分配进行控制。为了满足这种需求,大多数操作系统(包括Windows)提供了机制,让开发者可以向操作系统指示希望某个线程运行在哪些核心上,甚至是指定某个核心。

这种机制被称为"亲和性掩码"(affinity mask)。通过亲和性掩码,开发者可以告诉操作系统某个线程只能运行在某些特定的核心上,或者只运行在一个特定的核心上。虽然操作系统会尽力将线程调度到指定的核心,但并不一定能够保证线程永远不会被调度到其他核心。操作系统通常会尽量遵循这种偏好,但在某些情况下可能还是会发生线程在其他核心上运行的情况。

因此,尽管操作系统决定了线程的核心分配,程序员仍然可以通过设置亲和性掩码等方法,间接地影响线程的分配。这种方法使得程序员能够在一定程度上控制线程的分布,确保性能优化。Windows 操作系统还提供了一些额外的调度选项,比如用户模式调度等,使得开发者在调度线程时可以有更多的选择和控制。

编程是否就是记住一堆命令并按顺序排列它们?

编程不仅仅是将一堆命令按顺序排列。更准确地说,它是通过建立计算机工作方式的心理模型,并根据你编程的计算机类型来理解这些模型。程序员有一套方法可以影响计算机的操作,然后需要通过这些方法找出一组最合适的操作步骤,以达成目标。因此,编程其实是理解计算机如何工作并用适当的操作实现预定结果的过程。

当提到编程和音效相关的问题时,也会涉及到很多类似的思考过程。比如在声音设计中,我们需要了解不同的技术如何影响音频效果,进而选择合适的方式来达到预期的声音效果。

而对于最后的问题,即是否加入低频和高频音效的复杂性,它的难易度取决于具体的实现方式和技术要求。一般来说,低频和高频的音效设计并不复杂,但需要考虑音频的整体平衡和表现形式,确保它们与其他音效元素搭配得当。

添加低通/高通滤波器是否过于复杂?

通过代码实现滤波器并不是特别复杂。尽管我自己对声音编程没有太多经验,但基本原理还是比较简单的。当输出声音样本时,可以使用滤波器来处理这些样本。这种滤波器通常是一个简单的内联处理,它在每次输出一个样本时会对其进行一些数学运算,并使用累加器来跟踪一些状态。

实现滤波器时,最大的挑战通常是额外的记录工作。因为滤波器需要查看一个样本窗口,以便了解它需要过滤的振荡情况,所以在实现时需要一些额外的书籍管理和计算。这种额外的管理是处理滤波器时的主要难点,但一旦完成了这些管理工作,接下来的数学操作就相对简单了。滤波器本质上只是一组简单的数学运算,通过这些运算来更新累加器并处理新的样本。

如果你确实需要一个低通滤波器,实际实现起来也并不是一个很大的问题,处理过程并不会特别复杂。总的来说,滤波器的实现需要一定的记录和计算,但一旦这些管理工作完成,滤波的运算过程就相对简单。

低通滤波器(Low-pass filter)和高通滤波器(High-pass filter)是两种常见的信号处理滤波器,用于控制信号中的不同频率成分。

低通滤波器(Low-pass filter):

低通滤波器允许低频信号通过,而抑制高频信号。它的作用是去除信号中频率高于某一特定值(称为截止频率)的成分。这样,低频的部分(如音频信号中的低音)能够顺利通过,而高频部分(如噪音或高频噪声)会被削弱或去除。

  • 应用场景
    • 音频处理:去除音频中的高频噪声,保留低频的声音。
    • 图像处理:去除图像中的细节噪声。

高通滤波器(High-pass filter):

高通滤波器则允许高频信号通过,抑制低频信号。它的作用是去除信号中频率低于某一特定值的成分,使得高频成分(如高音、锐利的声音或细节信息)能够通过,而低频成分(如低音或慢速波动)会被削弱或去除。

  • 应用场景
    • 音频处理:去除音频中的低频噪声,例如风声或低音过重的部分。
    • 图像处理:增强图像的边缘细节,去除模糊的低频信息。

总结:

  • 低通滤波器 允许低频 信号通过,去除高频信号。
  • 高通滤波器 允许高频 信号通过,去除低频信号。

两种滤波器通常根据应用场景和需要去除的频率范围来选择使用。

如果一棵树倒下,而英雄不在场听到,难道它就不会发出声音吗?

这个问题类似于哲学中的"树倒了有没有声音"这一悖论,但在这里它变得更加简化。我们关心的不是"英雄"是否在场,而是"玩家"是否在场,因为声音最终是要被玩家在他们的世界中听到的。英雄所在的世界是没有声音的,所以英雄无法听到任何声音。声音的发生与否,实际上是发生在玩家的世界中的。

用一种类似于"树倒了,没人听见"的比喻,如果一个声音发生在游戏中,但没有人(在玩家的世界中)在场听它,这个声音就不会被"听到"。所以,虽然声音在游戏中的虚拟世界内发生了,但其意义与存在感依赖于玩家在现实世界中是否听到它。这种情形使得哲学上的"树倒了有没有声音"问题变得相对简单,关键点在于声音的存在与感知是否发生在玩家的世界。

你接下来要做的事是将声音代码放入 SIMD 吗?

声音代码将被转换为SIMD(单指令多数据)指令集,以确保它在多个核心上高效工作。然而,转换过程中并不会过多地优化,主要是确保其能够正常运行。对于目前所需的声音功能,基本就这些,后期可能会在开发周期的后期再考虑增加更多的声音处理功能。如果以后确实需要其他声音特性,会根据游戏的实际需求进行调整,而目前并不会过于复杂地添加额外的声音功能,直到明确知道具体需要哪些功能为止。

你会使用复数吗?我听说复数在声音处理上很有用。

这段内容讲述了在数字信号处理(DSP)中如何使用复数,特别是在声音处理方面。复数在傅里叶变换等算法中非常重要,这使得它们成为处理声音信号时的一种基础工具。尽管复数在传统的声音处理代码中广泛使用,尤其是在做滤波等处理时,但对于基础的声音处理工作,如本次讨论的内容,并不一定需要用到复数或数字信号处理技术。只有当涉及到更复杂的信号转换时,例如傅里叶变换时,才会需要复数。

虽然数字信号处理的确是传统的声音处理方法,但他个人并不特别喜欢它,认为它有些"陈腐"。如果想深入了解传统的声音处理方法,需要学习数字信号处理。

相关推荐
茯苓gao1 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾1 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT2 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa2 小时前
HTML和CSS学习
前端·css·学习·html
看海天一色听风起雨落3 小时前
Python学习之装饰器
开发语言·python·学习
speop4 小时前
llm的一点学习笔记
笔记·学习
非凡ghost4 小时前
FxSound:提升音频体验,让音乐更动听
前端·学习·音视频·生活·软件需求
ue星空4 小时前
月2期学习笔记
学习·游戏·ue5
萧邀人5 小时前
第二课、熟悉Cocos Creator 编辑器界面
学习
m0_571372825 小时前
嵌入式ARM架构学习2——汇编
arm开发·学习