游戏引擎学习第140天

回顾并为今天的内容做准备

目前代码的进展到了声音混音的部分。昨天我详细解释了声音的处理方式,声音在技术上是一个非常特别的存在,但在游戏中进行声音混音的需求其实相对简单明了,所以今天的任务应该不会太具挑战性。

今天我们会编写一个非常基础的声音混音器,首先确保它能正常工作,接着思考一下它的接口应该是什么样的。接下来可能会在下周进行一些优化调整,并使其更加符合实际需要。我们可能会进行一些简化,以确保它在运行时的表现良好,但不会对其进行过多优化,因为我们更关注的是实现它能够正常运行。

回顾一下我们上次停下的地方。昨天我们详细讨论了声音处理,但在之前的日子里,我们加载了声音文件,并能够播放我们选择的任何一个文件。现在代码中,声音播放的是一段钢琴音频采样。

现在,我们有一个当前的设置方式,可能不是最好的选择。我们平台代码的设置是,如果没有达到目标帧率,音频会发生撕裂现象。举个例子,如果没有达到帧率,音频会跳过一些帧,在调试模式下,我们就能看到音频中的跳过,然而如果在优化模式下,音频播放就平滑了。问题的原因是,我们目前没有做任何处理来避免这些问题。我们假设总是能保持目标帧率,并尽可能填充音频样本以补偿任何可能的帧率缺失。

当前如果屏幕刷新频率是165Hz的话
在这里插入图片描述

让从电脑获取刷新频率来计算
在这里插入图片描述

(debug模式运行程序音频有撕裂的现象)
(release模式正常)

接下来,我们要考虑做的工作是:我们现在有一个播放音频的循环,功能很简单,就是将加载的声音样本复制到输出缓冲区中。而我们的目标是将这个简单的操作扩展成可以在任意时间播放多个声音的系统,同时将它们混音成一个合理的音频输出,这样我们就能够同时播放多个声音而无需担心它们之间的冲突。

为了实现这一目标,首先要清楚我们目前的代码结构。在当前的设置中,播放音频样本是通过单独的调用来实现的,之所以这么做是因为我们最开始想让它与游戏状态的其他部分分开,考虑到将来可能会将其单独放到线程中运行。虽然目前还没有决定是否将其放在不同的线程中,但我们首先假设,声音样本的生成和游戏的其他部分是同步进行的。

当然,未来可能会考虑将它拆分到独立线程中,但现在我们假设声音和游戏的其他部分同步运行,先不考虑线程和锁的问题,之后再看是否需要对这一点做出改变。

game.h: 引入 playing_sound 并在 game_state 中添加一个指针

首先,我们需要了解当前正在播放的所有声音,因此在 game.h 文件中,我们可能需要在 game_state 中添加一些内容,来记录当前播放的所有声音。这里有多种方式可以实现这一点。一种合理的方法是使用链表,我们可以将正在播放的声音添加到链表中,然后假设链表中的每个节点代表一个正在播放的声音。另一种方法是使用固定数量的槽(数组),我们只允许在这些槽中播放声音。如果所有槽都已满,新的声音就无法播放。

为了解决这个问题,首先我决定使用链表,因为它是最简单直观的方式。我们可以创建一个名为 playing_sound 的结构体,并使它具有链表功能,使每个声音都有一个指向下一个声音的指针。然后,我们会在 game_state 中添加一个指针,指向链表的开头,命名为 PlayingSounds

playing_sound 结构体中,我们希望每个声音有以下信息:

  1. 声音ID:每个声音都会有一个唯一的标识符,这是为了区分不同的声音,类似于我们之前在渲染中使用的ID。通过这个ID,我们可以从音频资源中提取正确的声音样本进行播放。

  2. 音量:我们需要一个音量值,表示当前声音的音量大小,这个值介于0和1之间,0表示静音,1表示原始声音的最大音量。这个值可以控制声音的强度。

  3. 左右声道音量:因为我们使用的是立体声音频,我们还需要分别控制左右声道的音量。因此,我们可能需要存储左右声道的音量值,允许分别调整左右声道的音量。

  4. 播放位置:我们还需要跟踪当前声音播放的位置,也就是已经播放了多少个音频样本。这是为了确保在播放声音时,我们知道已经播放了多少,以免重复播放同样的样本。每个声音会有一个“播放游标”来记录当前播放到的样本位置。

基于这些需求,我们的 playing_sound 结构体大致会包含以下字段:

  • sound_id ID:当前播放的声音的ID。
  • real32 Volume[2]:声音的整体音量(0到1之间)。
  • left_volumeright_volume:分别控制左右声道的音量。
  • SamplesPlayed:记录已播放的音频样本数量。

这些信息可以帮助我们管理音频的播放,并确保能够正确地混合多个声音。如果需要支持声音的循环播放或更多功能,后续可以继续扩展。

接下来,我们将通过链表管理这些 playing_sound 实例,并在每个更新周期遍历链表,播放当前的声音。
在这里插入图片描述

game.cpp: 设置 PlayingSound 循环

在这个循环中,首先需要做的是从 game_state 中获取到第一个正在播放的声音。接着,检查这个声音是否有效。如果有效,就对这个声音进行处理。每次处理完当前声音后,就移动到链表中的下一个声音,继续处理所有正在播放的声音,直到遍历完所有声音。

除此之外,还可以考虑提前排队一些样本数据。例如,我们可以允许设置已播放的样本数为负数,这样就能够实现延迟播放的功能。通过这种方式,可以将某个声音的播放推迟一段时间,比如延迟半秒钟再开始播放。虽然这种延迟播放功能不一定是必要的,但如果需要实现,可以通过负值来标记推迟的播放时间。

接下来,对于每个需要输出的音频样本,我们的目标是将这些声音样本输出到最终的音频缓冲区。最初的循环仅仅是将16位的样本数据复制到16位的输出数据中,这种方式虽然简单,但可能会导致音频样本值的裁剪(clipping)问题。如果我们直接使用16位的值作为中间缓冲区来累加音频样本数据,可能会遇到溢出或裁剪问题,因为累加的音频值可能超出了16位的表示范围。

为了避免这种问题,我们可以考虑使用更高精度的缓冲区(比如32位或更高位深的缓冲区),来存储音频样本的中间累积值。这样做可以减少裁剪的概率,并且在最终将这些累积值转换成16位输出时,能够更好地控制音频的动态范围,避免失真。

总结来说,在输出音频样本时,需要确保处理链表中的所有播放中的声音,并在处理过程中考虑延迟播放、避免裁剪等问题。最终的目标是通过更高精度的缓冲区避免音频失真,并将处理后的样本正确输出。
在这里插入图片描述

在这里插入图片描述

Blackboard: 在32位深度混音缓冲区中工作

在进行声音混合时,如果我们使用16位音频进行处理,可能会遇到裁剪(clipping)问题。假设有两个声音,第一种声音的波形第二种声音的波形当将这两种声音相加时,结果会超出16位的表示范围,导致裁剪发生。即使将两种声音叠加,它们的值可能会超出16位的最大值,从而发生失真。

然而,如果我们再加入第三种声音,假设第三种声音当我们将它与前两个声音相加时,第三个声音的波形可能会减弱前两个声音的影响,使得最终的结果回到16位范围内。这样就避免了裁剪问题。也就是说,多个声音的叠加并不一定总是会导致裁剪,因为不同声音的波形可能会互相抵消,从而将总和控制在可表示的范围内。

因此,通常在处理16位音频时,为了避免裁剪问题,应该在32位精度的空间中进行混合处理。这样能够确保即使多个声音叠加,最终结果也不会溢出16位的范围。

在实现过程中,使用浮点数(float)是一个不错的选择,因为浮点数能够更方便地进行动态范围调整和其他的调制操作。在混合过程中,我们可以将16位音频数据转换为浮点数进行处理,完成混合后再将结果转换回16位,这样能够确保音频的质量并避免裁剪。

总结来说,处理音频时,最好使用32位浮点数作为中间缓冲区,先将16位数据转换为浮点数进行混合,最后再将混合后的结果转换回16位数据输出,这样能够避免音频裁剪并保持较好的音质。

game.cpp: 引入 RealChannel0 和 RealChannel1

在这个过程中,我们希望能够创建一个真实的声音缓冲区,并且这个缓冲区是针对每个声道的。对于立体声来说,我们会有两个声道,即左声道和右声道。所以,我们需要为每个声道分别处理缓冲区的数据。

为了实现这一点,我们将为每个声道分配一个浮点数类型的缓冲区,因为使用浮点数有助于我们在混音过程中避免裁剪问题。每个声道的缓冲区将用来存储混合后的音频数据。

具体步骤如下:

  1. 初始化缓冲区:我们将为每个声道(例如左声道和右声道)创建一个缓冲区。每个缓冲区中的数据将以浮点数的形式存储,这样我们可以避免16位音频格式可能带来的裁剪问题。

  2. 数据转换:从加载的声音文件中提取每个样本,首先会将其转换为浮点数格式。然后,使用浮点数表示的缓冲区进行混合操作。

  3. 混合处理:我们将逐个采样地处理每个声音的样本,并将它们写入到相应的声道缓冲区中。通过这种方式,我们在混音过程中能够保持较高的音质,并且避免在处理多个声音时出现裁剪现象。

  4. 最终输出:混合操作完成后,我们将得到一个包含所有声音数据的缓冲区,它们已经被转换为浮点数格式,并进行了合适的混合。接下来,我们可以将这些数据转换回16位音频格式,准备输出到音频硬件。

在这里插入图片描述

game_asset.h: 引入 GetSound

在这个过程中,需要先确保有一个获取音频数据的功能。首先,代码中并没有直接实现获取声音的函数,因此需要添加一个新的函数来获取声音。

步骤如下:

  1. 创建获取声音的函数:目前代码中并没有 GetSound 函数,因此需要编写这个函数。该函数的作用是根据声音的 ID 获取对应的音频数据。

  2. 调用获取函数:在处理播放的声音时,需要通过该 GetSound 函数获取到对应的声音数据。通过声音的 ID 提取出声音文件数据,为接下来的混音操作提供数据来源。

  3. 使用声音 ID:每个播放中的声音都有一个唯一的声音 ID,通过声音 ID 可以访问到对应的声音资源。这个 ID 存储在每个播放中的声音对象里,作为获取该声音数据的关键。

在这个步骤中,最重要的是确保获取音频数据的功能能够正常工作,以便后续对音频进行混音和处理。
在这里插入图片描述

game.cpp: 调用 GetSound 并在循环中对样本进行求和

首先,加载声音的过程需要从资源中获取。资源会存储在 ttransient_state(瞬态状态)中,具体来说是它的 groupsTransientStorage 中,存储了所需的音频资产。获取声音时,需要检查LoadedSound是否已加载声音,如果未加载,便无需继续处理。若未能获取到声音数据,则可以跳过相关处理。

为了确保声音在播放时不出现突兀的响声,建议在加载音频时调用 LoadSound 函数,这样可以确保音频尽快准备好播放。另一方面,为了避免声音出现“爆音”现象,可以在播放前设置音量渐入效果,逐渐将音量调到正常水平。实际上,最好的方法可能是延迟播放,直到音频文件加载完毕,避免音频未准备好就开始播放。

处理过程中,对于未LoadedSound,使用一种方法延迟音频播放,直到所有数据准备完毕。通过调整播放进度和(播放样本数),可以避免音频处理错过任何样本。这个策略可以确保音频的顺畅播放,而无需担心遗漏或不准确的播放。

在声音加载完成后,需要遍历音频的样本并进行加总。每个样本的处理方式是,首先将该样本的值与当前通道的Volume音量值相乘,再进行加总。每个通道的音量(例如 Volume0 和 Volume1)应该提前提取并清晰标记,以便确保音量的控制在正确的通道中应用。

要从加载的声音样本中提取特定样本值时,可以通过计算样本的索引位置来获取该样本。每个音频样本的位置由 SampleIndexSamplesPlayed(播放样本数)决定,因此需要将它们相加得到准确的样本索引。

另外,在处理音频时,必须考虑到边界情况。例如,可能需要处理立体声的特殊情况(stereo)。这种特殊情况会影响到如何处理样本值,特别是在多个通道或样本位置不一致时。

处理声音加总的过程时,如果多个声音同时播放,我们需要保证清除旧的音频数据,否则加总结果可能不准确。特别是在每次新的音频播放开始前,需要先清空音频通道中的原有数据,确保加总过程从零开始。这样,音频加总将不会受到先前数据的干扰。

为了简化初步实现,避免对第一次播放的声音进行特殊处理,可以在每次处理时统一方法,即对每个声音的处理方式一致,而无需对首个声音进行特别优化。为了确保加总结果正确,可以在加总前将所有的通道数据清零。

以上步骤可以确保音频的加载、播放、加总和音量处理能够平稳进行,从而避免音频播放时出现卡顿、爆音或其他异常情况。
在这里插入图片描述

game.cpp: 循环遍历求和后的声音,从中读取并将其写入 SampleOut 缓冲区

接下来,我们需要完成一个循环来处理所有的音频数据。具体来说,现在已经有了所有声音的加总结果,并且这些加总后的音频数据已经存储在内部缓冲区中。接下来的任务是从这些缓冲区中读取出加总后的值,并将其写入输出缓冲区。

首先,我们需要确保音频数据从内部缓冲区正确地转换并输出为16位的音频数据。具体来说,就是从每个通道的源缓冲区中读取数据,然后进行四舍五入,确保数据格式正确。四舍五入后的值将转化为16位整数,这就是最终要写入输出缓冲区的音频数据。

在左声道和右声道中,分别从源缓冲区(如源0、源1)读取数据,并进行四舍五入。这一步是为了确保输出的音频数据精度符合16位的要求,并避免任何意外的截断或误差。

在数据处理过程中,原先的测试索引已经不再需要,因为现在不再依赖于这些索引来处理音频,而是直接通过内存中的加总数据进行处理。

接下来,需要确保处理过程的区域是有效的。我们需要约束处理的音频样本范围,避免越界或无效的内存访问。为此,可以从瞬态内存(transient memory)中分配临时内存,以确保内存管理的正确性。通过从瞬态内存中分配内存块,确保了我们在混音过程中有足够的内存来存储数据,并且内存释放也会在混音完成后自动进行。

为了管理这些临时内存,可以创建一个临时的音频混音缓冲区,并在其中进行样本的处理。这些缓冲区将用于存储混音过程中所需的数据。每当需要使用内存时,从瞬态内存中申请相应的内存块,处理完后再释放回去。

在此基础上,清理缓冲区中的音频数据,以便开始新一轮的混音操作。然后对所有声音进行加总,最后将加总后的数据转换为16位格式,确保音频数据符合输出要求。

总结一下,这个过程的核心包括:

  1. 从源缓冲区读取音频数据并进行四舍五入处理;
  2. 将四舍五入后的数据转换为16位整数;
  3. 在处理过程中管理内存,确保临时内存的正确分配和释放;
  4. 对所有声音进行加总,并将结果写入输出缓冲区。

这些步骤确保了音频的正确处理,避免了数据溢出或内存错误,并确保最终输出的音频数据格式正确。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game.cpp: 引入 SamplesToMix 和 SamplesRemainingInSound,以处理音频的有限性

目前,混音器的主要部分已经基本完成,接下来需要处理一些细节问题,特别是关于声音时长的管理。

首先,需要考虑声音的时长问题。当进入音频处理循环时,每个加载的声音LoadedSound都具有特定的样本数量(SampleCount)。在处理过程中,如果样本索引超出了这个样本数量,意味着已经到达了声音的末尾。如果继续读取超出范围的数据,可能会访问到错误的内存区域。虽然不会导致程序崩溃(因为内存通常是连续分配的),但会导致音频流中出现错误的数据,从而产生杂音或其他不良效果。因此,需要一种机制来判断声音是否已经播放到结尾,以防止这种情况的发生。

为了解决这个问题,可以引入“待混合样本数”(SamplesToMix)的概念。初始时,待混合的样本数量默认等于输出缓冲区的大小。但在计算时,需要检查实际可用的样本数,即当前声音剩余的样本数。具体计算方式如下:

  1. 计算当前声音已经播放的样本数 SamplesPlayed
  2. 计算该声音的总样本数 SampleCount
  3. 通过 SampleCount - SamplesPlayed 得到该声音还剩余多少样本可以被混合SamplesRemainingInSound。

如果待混合样本数大于该声音剩余的样本数,则需要调整待混合样本数,使其不会超出实际范围。然后,在处理循环时,就可以使用这个调整后的 SamplesToMix,避免访问超出范围的内存。

调整播放样本数后,还需要进一步处理播放列表中的声音。当混音完成后,需要更新 SamplesPlayed,即增加已播放的样本数量。如果发现某个声音已经播放到了末尾,就应该从播放列表中移除它。否则,它会一直留在列表中,导致播放列表不断膨胀,占用不必要的资源,甚至可能影响程序性能。因此,需要在适当的时机清理已播放完毕的声音。

这部分处理的核心步骤包括:

  1. 计算剩余样本数:确定当前声音还有多少样本可以播放。
  2. 调整待混合样本数:确保不会超出声音的实际数据范围,避免访问无效内存。
  3. 更新已播放样本数:记录已经混合的样本数量,确保下一帧的计算正确。
  4. 移除播放完毕的声音:如果某个声音已经完全播放完毕,就将其从列表中删除,以保持播放列表的整洁和高效。

这一机制保证了混音过程的稳定性,同时优化了资源管理,使得播放列表不会无限增长,提高整体性能。
在这里插入图片描述

game.h: 在 game_state 中添加 playing_sound *FirstFreePlayingSound

为了管理播放列表,需要在播放结束时从列表中移除已播放完毕的声音,并将其加入一个空闲列表(free list),以便后续复用。当需要播放新的声音时,可以直接从空闲列表中取出一个已释放的声音对象,而不是重新分配新的内存,从而提高资源利用率和性能。

具体来说,在移除声音时,首先要确保它不会再被使用,然后将其指向空闲列表的头部(例如 FirstFreePlayingSound)。这样,下一次需要播放新声音时,可以直接从空闲列表中取出一个已有的结构,而不必重新创建新的实例。这种做法减少了内存分配和释放的开销,提高了播放管理的效率。
在这里插入图片描述

game.cpp: 在 PlayingSound 循环中使用 FirstFreePlayingSound

为了优化声音播放管理,在移除已播放完毕的声音时,需要将其加入空闲列表,以便后续复用。具体实现方式是利用链表结构,将移除的声音节点添加到空闲列表的头部,这样可以快速回收并复用内存,而无需频繁申请和释放新内存。

在执行这一操作时,需要使用 PlayingSound 结构中的 Next 指针。具体步骤如下:

  1. 取出当前游戏状态中的 FirstFreePlayingSound,即当前空闲列表的头部。
  2. 将即将被释放的 PlayingSound 节点的 Next 指向 FirstFreePlayingSound,将其链接到空闲列表的最前端。
  3. 更新 FirstFreePlayingSound 指针,使其指向新的空闲节点,即刚刚释放的 PlayingSound

通过这种方式,可以确保被移除的声音不会被遗忘,而是被回收到空闲列表中,等待下一次需要播放新声音时直接复用。这不仅减少了内存分配和释放的开销,还提高了声音管理的效率,使得播放列表不会无限增长,同时减少了系统资源的浪费。

在这里插入图片描述

game.cpp: 提前固定 Next 指针,以防止其被释放时影响循环迭代

在遍历播放列表时,如果在循环过程中移除当前正在处理的声音对象,会导致迭代出现问题。因为 playing sound 被移除后,其 next 指针指向的内容可能已经改变,导致后续遍历发生错误。因此,需要在移除之前先保存 next 指针,以确保迭代能够正确进行。

具体实现方式如下:

  1. 提前获取下一个播放声音节点
    在处理当前 PlayingSound 之前,先将 Next 指针保存到一个独立的变量 NextPlayingSound,这样即使当前 PlayingSound 被移除,其 Next 仍然可以被访问。

  2. 在循环结束时使用已保存的 Next 指针
    在完成当前节点的处理后,使用预先存储的 NextPlayingSound 作为新的迭代对象,而不是直接访问 PlayingSound->Next,这样就不会受到节点被释放的影响。

这种方法在需要从链表中移除元素的迭代过程中非常常见,主要目的是防止因节点删除导致的访问错误,提高代码的稳定性和可维护性。

此外,还添加了一个断言,确保 PlayingSound->SamplesPlayed 始终大于等于 0。这样如果未来需要支持声音延迟播放,代码会提醒需要对混音逻辑进行额外调整,以正确处理延迟情况。目前暂时不支持延迟播放,但为后续扩展留出了可能性。
在这里插入图片描述

game.h: 引入 MetaArena 概念

当前代码中存在一些需要解决的问题,尤其是在内存管理方面。

1. 现存的问题:游戏状态的内存管理

目前在 game_state 中存在两种不同的内存分配区域:

  • 世界(World)相关的内存区域
    这个区域用于存储与当前世界(游戏场景)相关的数据,当世界被销毁时,该区域的所有数据都会被释放。
  • 长期存活(Meta)区域
    这个区域存储一些需要在多个世界之间持久存在的数据,例如音乐或音效。因为当玩家从一个世界退出回到主菜单时,背景音乐不应该停止,因此音效不应与世界的内存区域绑定,而应该属于一个更持久的区域。

目前并没有正式区分这两种内存区域,但未来可能需要引入 “世界内存”(会随世界销毁而释放)和 “元内存”(会跨世界持续存在)来更好地管理游戏状态中的不同数据。虽然现在可以暂时不处理这个问题,但之后应该在架构层面进行改进,以适应更复杂的游戏需求。


2. 声音加载逻辑尚未完成

当前的 load_sound 相关代码似乎尚未完全实现,存在以下问题:

  • load_sound 函数被调用后,尚未正确地在 asset_system 中完成音效的加载。
  • 具体来说,SoundInfos 变量的 FileName 是否正确赋值仍存疑,可能导致加载逻辑无法正确执行。
  • SoundInfos(音效信息)数组似乎没有被正确填充,导致声音数据实际上并未正确加载到内存中。
  • sound first 变量目前也没有正确指向任何数据,这意味着音效数据结构的初始化可能未完成。

3. 可能的解决方案

  • 内存管理优化

    • 需要在 game state 中正式引入 长期存活的内存区域(Meta Arena),将跨世界存活的音效存储其中。
    • 目前的 World Arena 仍然保留,用于存储随世界创建和销毁的临时数据。
    • 未来可能还需要引入 动态管理机制,例如在 Meta Arena 中进行手动释放,以避免长期运行导致的内存泄露。
  • 改进声音加载逻辑

    • 需要确保 load sound 代码能够正确解析 sound info 并填充相应的数据结构。
    • 应该检查 SoundInfos 是否在 asset system 中正确初始化并存储音效数据。
    • 如果 Info 变量的 FileName 未被正确赋值,需要找到 LoadSound 的具体调用流程,并确保 FileName 的来源正确。

4. 结论

目前的代码在内存管理和音效加载方面仍然有一些未完成的部分:

  • 内存管理方面 需要区分 World ArenaMeta Arena,以确保音效数据不会因世界的销毁而被错误释放。
  • 声音加载方面 需要检查 LoadSound 相关代码,确保 SoundInfos 被正确初始化并存储音效数据。
  • 未来需要进一步完善 资源管理系统,包括加载、释放、回收等机制,以提高整体稳定性和扩展性。
    在这里插入图片描述

在这里插入图片描述

game_asset.h: 将音频资源添加到 asset_type_id

目前的代码正在处理游戏中的音效资源,并尝试将它们纳入系统中进行管理。

1. 组织音效资源

在现有的资源管理中,已经有了一个存放 图片(bitmaps) 的区域,现在需要以类似方式管理 音效(sounds)。因此,代码中新建了一个 sounds 目录,并开始往其中填充一些初步的音效资源。这些音效文件包括:

  • bloop //轻微的弹跳声
  • drop //物体掉落
  • glide //滑行
  • music //背景音乐(BGM)
  • pup //获取道具、能力提升(power-up)

这些音效文件可能只是测试用的资源,而非最终游戏中的正式音效,但目前会先将它们纳入系统进行管理和测试。


2. 加载音效

  • 代码正在尝试从 sounds 目录中读取音效文件。
  • 其中一个音效文件体积较大(大约 30MB),但目前先默认接受它,不对大小进行特殊处理。
  • 具体的加载过程暂时未详细展开,但整体目标是确保所有列出的音效都能被正确读取,并在游戏中使用。

3. 未来的优化方向

  • 音效管理系统完善

    • 可能需要为音效创建一个 缓存系统,避免重复加载相同的音效文件,提高加载效率。
    • 需要考虑 音效格式的转换或压缩,特别是大体积的音效,可能会影响加载速度和内存占用。
  • 音效的分类和使用

    • 目前的音效名称(bloopdropglide 等)可能只是测试用,最终需要根据游戏需求重新设计音效库。
    • 未来可能会引入 不同类型的音效(背景音乐、环境音效、UI 交互音效等),并对其进行分类管理。
  • 动态音效加载

    • 如果游戏存在大量音效,可能需要动态加载和卸载,而不是一次性全部加载到内存中,以优化资源管理。

4. 结论

当前代码已经初步引入了 音效资源管理,并将部分测试音效纳入系统。虽然目前只是简单地将目录中的文件加载进来,未来仍需要对 音效格式、加载方式、内存管理 等方面进行优化,以提升游戏的音效系统稳定性和性能。
在这里插入图片描述

game_asset.cpp: 扩展资源加载,处理音频

当前的任务是将音频资源的处理方式扩展,使其与之前的资产系统保持一致。现阶段,我们尚未定义完整的资源包文件格式和目录结构,因此暂时通过代码直接构造音频资产,后续会改为从磁盘加载。

目前,我们已经有了一些临时音效文件:

  • bloop
  • drop
  • glide
  • music
  • pup

这些音频文件暂时通过手动方式加入到资源系统,并按照索引进行管理,例如 bloop_00.wavbloop_01.wav 等。同样地,Puhp 也有多个变体,如 Puhp_00.wavPuhp_01.wav。此外,music 作为背景音乐资源,也被加入到资产系统中。

由于当前还没有正式的资源文件格式,因此这些数据的加载逻辑只是临时的。目的是为了先验证音频系统的可行性,确保整个流程符合需求,而不是先花大量时间去设计一个完整的资源打包格式。如果后续发现当前方式符合需求,就会基于现有逻辑创建正式的资源文件格式,并删除这些手写的临时代码。

在这个过程中,我们的目标是:

  1. 模拟资源加载 —— 先手动构造数据,模拟最终的加载方式。
  2. 验证功能 —— 通过手动数据确保音频系统正确运行。
  3. 优化设计 —— 在功能稳定后,再进行资源文件格式和加载流程的优化。

最终,所有的音频资源都会存储在一个标准化的资源包文件中,而不是硬编码在代码中。
在这里插入图片描述

在这里插入图片描述

game_asset.cpp: 引入 AddSoundAsset

为了扩展当前的音频资产管理系统,需要创建类似于“添加位图资产” (AddBitmapAsset) 的功能,但用于音频资源。具体来说,需要实现一个 AddSoundAsset 的功能来将音频资产加入到资源管理中。

这个过程大致如下:

  1. 添加音频资产:我们将创建一个类似于 AddSoundAsset 的函数来处理音频资源的加入。这里不涉及对齐(alignment),因此不需要额外处理对齐的细节。
  2. 调试信息:与之前位图资产的调试方式类似,我们需要添加调试信息来追踪加载的音频资产。比如创建一个 DEBUGAddSoundInfo 函数,来记录音频资源的加载状态,帮助我们跟踪音频的加载和使用情况。
  3. 音频计数管理:需要引入一个调试函数 DEBUGUsedSoundCount,用于管理和显示当前使用的音频资源数量。通过这个函数,我们能够查看音频资源的使用情况,确保没有遗漏或错误。
  4. 索引管理:在加载音频时,类似于位图资源的索引,我们需要维护一个 SoundCount 来确保音频资源的正确加载和访问。这些索引将确保音频资源不会被错误地覆盖或者未正确加载。

为了确保所有的音频资源在加载时不会出问题,音频数据的加载系统会在这里进行调试和优化,确认加载过程没有问题。需要注意,可能有些类型的索引(如样本索引)需要处理为正确的类型(比如在代码中处理为32位整数),避免发生不兼容的类型错误。

总之,现在添加音频资源的功能已经准备好,理论上应该能顺利地加载音频资源,而不会出现问题。
在这里插入图片描述

在这里插入图片描述

game.cpp: 使新系统模拟流中的初始内容

现在的目标是确保新系统能有效模拟最初开始时的效果,确保在继续开发其他功能之前,已经验证了当前实现是否正常运行。因此,我们首先会在游戏开始时,模仿之前的音效加载方式,做一些初始化工作。

具体的步骤如下:

  1. 分配音效资源:在游戏开始时,我们需要分配一个音效资源,这个资源会被设置为正在播放的音效。在初始阶段,将其分配到 WorldArena 中,这样可以保证它在正确的位置,并且是临时使用的,不会长期保留。

  2. 将音效资源连接到播放列表:将这个音效资源赋值给 FirstPlayingSound,这表示当前正在播放的音效。这样做可以验证音效的加载和播放是否正常。

  3. 测试加载和播放音效:现在我们测试的是新实现的功能是否正常。我们希望通过简单地加载一个音效并播放,来确认我们之前实现的音效系统是否能够正常工作。

  4. 改进和简化代码:代码中出现了一些重复和不太清晰的部分,比如在加载时需要获取第一个音效的 ID。为了避免代码冗余,我们可以优化这些函数,确保它们能够处理不同类型的资源(如位图和音效)。本质上,我们希望这些函数能够通用,不仅仅局限于处理位图,还可以处理音效等资源。

  5. 类型安全性:虽然可以让这些函数接受任何类型的资源,但为了增强类型安全性,可以在必要时使用包装函数。这些包装函数将为不同类型的资源提供更多的类型检查,使得在整个游戏开发过程中,能更有效地处理和确保类型的正确性。

总之,当前的目标是确保音效系统能够正确加载和播放,并且让代码更加简洁和具有通用性,以便后续扩展和维护。
在这里插入图片描述

game_asset.cpp: 引入 GetFirstSlotID 和各种获取函数,用于位图和声音

接下来,我们希望对音效和位图的处理代码进行一定的改进和重构。具体来说,就是通过对现有的函数进行封装,使它们能够处理不同类型的资源,而不仅限于位图。以下是我们要进行的一些改动和改进的步骤:

  1. 封装函数:我们将对现有的 GetFirstBitmapID 这类函数进行封装,创建一个 GetFirstSlotID 的函数,这个函数不再与位图直接相关,而是通用地处理所有类型的资源。我们希望这些函数能够在不考虑资源类型的情况下返回对应的插槽 ID,保持通用性。

  2. 修改相关函数:类似地,我们还需要对 GetRandomBitmapGetRandomSlot 等函数进行相应的修改,使它们能够支持不同类型的资源处理,而不局限于位图。例如,我们可以创建 GetRandomSound 函数,这样可以通过相同的逻辑来处理音效和其他资源。

  3. 代码重构:通过这种方式,原本与位图相关的函数会变得更加通用,能够支持音效和其他资源的操作。虽然这些修改看起来很简单,但它们实际上提高了代码的可维护性和扩展性,避免了代码冗余。

  4. 改进命名:为了提高代码的可读性和明确性,我们还需要改进一些函数的命名。例如,将 GetFirstBitmapID 改为 GetFirstSoundFrom 或者 GetRandomFrom,这些命名更加符合实际功能。

  5. 测试和验证:在修改了大量的代码后,我们需要确保这些修改不会引入新的问题。我们将在调试模式下检查代码,确保所有的资源加载和播放操作都按预期工作,特别是在资源管理方面。

  6. 临时处理:在实际操作中,我们先将音效资源添加到 WorldArena 中,并临时分配播放的音效资源。然后,通过 FirstPlayingSound 来跟踪当前正在播放的音效。虽然这些修改是临时的,但它们有助于验证代码的有效性。

  7. 调试:经过上述改动后,我们在调试时遇到了一些问题,比如访问冲突等错误。为了排查问题,我们需要深入分析和逐步调试,确保所有的资源都得到了正确的分配和使用。

总结来说,当前的目标是通过对现有函数的封装和修改,使得代码更加通用,并且能够灵活地处理音效和其他类型的资源,同时确保新代码能够顺利运行,不引入新的错误。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

调试器: 步进查看资源加载代码

我们开始分配资源并设置标签范围。在这过程中,遇到了一些问题,比如声音计数有时小于预期的声音数量。这时,我们调整了声音资源的计数器,以确保它不会低于实际需要的数量。

具体步骤如下:

  1. 分配资源:首先,我们分配了音效资源,并开始设置相关的标签范围。这一步是为了确保音效能够被正确加载和使用。

  2. 设置标签范围:标签范围是用来标识不同类型资源的位置和顺序。通过设置这些范围,系统能够快速访问和管理资源。

  3. 调整计数器:遇到的问题是声音计数有时低于预期的声音数量。这可能是因为在设置或分配资源时,计数器的值没有正确更新。因此,我们调整了声音计数器的值,以确保它与实际资源数量一致,避免了资源使用上的错误。

总结来说,通过分配和调整音效资源,以及修正计数器的设置,我们确保了资源管理系统能够正确处理音效资源,避免了计数器值过低导致的问题。
运行发现段错误
在这里插入图片描述

game_asset.cpp: 将 SoundCount 设置为 256 * Asset_Count

显然,这样的做法是行不通的。但这其实并不需要太过担心,因为这些问题在当前阶段并不重要。我们只需要确保它在现阶段的功能是足够的,毕竟,最终这些设置不会硬编码在程序中,而是会通过包文件来设置。我们在打包时会准确知道包文件中有多少个声音资源,因此在那个时候一切都会正常运作。
在这里插入图片描述

调试器: 检查 GameState->FirstPlayingSound

现在,我们可以查看一下游戏中的第一个播放声音,看看它被设置成了什么。实际上,它已经有了一个有效的ID,这说明我们成功地找到了一个真实的声音资源。希望这意味着我们确实成功地定位到了一个实际的声音资源,并且它现在已经在系统中可用。
在这里插入图片描述

game_asset.cpp: 将音量调到最大

现在考虑到音量的问题,显然它不应该是零,因为如果我们想听到声音,我们必须给它一个非零的音量。因此,暂时将音量设置为最大。接下来,在其他代码部分,我们可以查看混音器,确保现在有声音数据实际传递给它。这样做是为了确保系统能够正常处理并播放音效。
在这里插入图片描述

调试器: 步进进入 LoadedSound

需要加载音频,因为它还没有被加载。接下来,要确保获取音量和目标通道,这些通道应该已经被清空。我们需要确认这一点,并查看混音器实际处理了多少样本。检查音频的数据流,确保样本值正确地积累并处理。如果音频数据有值,它应该会被正确地累积。接下来,我们会继续播放声音并更新状态,确保播放过程顺利进行。所有这些步骤都看起来合理,接下来将继续处理输出缓冲区。
在这里插入图片描述

build.bat: 切换到 -O2 并运行游戏

现在的目标是创建一个简单的功能,允许我们轻松地触发声音。在之前的代码中,我们已经为播放声音做了相关的内存分配。现在的计划是将这些分配代码移回到合适的位置。虽然目前有一些关于内存分配的问题,比如是否应该进行内存分区,或是从操作系统动态分配内存,但现在并不是讨论这些问题的时机。

我们现在的重点是能够在“World Arena”中随意分配声音,暂时不考虑其他复杂的内存管理问题,直接假设能够从World Arena 中分配所需的声音资源。

game.cpp: 引入 PlaySound

接下来,要实现一个新的功能,允许通过 PlaySound 函数播放声音。这个函数将接受一个声音 ID 和一个游戏状态,用来决定在哪个状态下播放声音。函数的工作流程是,首先创建一个新的播放声音对象,并初始化它,就像之前的初始化过程一样。然后,把这个播放声音对象放到播放列表的顶部。

在此过程中,如果存在一个空闲的播放声音对象,则使用它;如果没有空闲的,则会创建一个新的播放声音对象,并把它添加到空闲列表中。同时,每个新的播放声音对象的 next 指针会设置为零,确保它不会被其他地方使用。

这段代码的目的是确保每次播放声音时,都能正确地管理播放声音对象。如果没有空闲对象,系统会自动创建一个新的并插入到空闲列表中。最后,这个操作应该可以启动音乐并使其在游戏中播放。
在这里插入图片描述

运行游戏并听到我们的声音

game.cpp: 在触发剑时调用 PlaySound

为了测试音频混合的功能,计划在特定事件发生时播放声音,比如当角色发动攻击或其他动作时。在代码中,考虑通过调用 PlaySound 函数来实现这一点。具体来说,将从一个已定义的资产列表中随机选择一个声音,并播放它。目标是确保能够通过随机选择的声音来测试音频播放的过程。

在实现时,发现 PlaySound 函数并不接受两个参数,这与预期的调用方式不符。因此需要检查这个函数具体需要多少个参数,并弄清楚应该传入什么类型的参数。此外,考虑到可能还没有定义一个用于选择随机声音的系列,也需要检查是否已经创建了一个随机选择声音的序列。如果没有,需要相应地进行处理。
在这里插入图片描述

game.h: 向 game_state 中添加 random_series GeneralEntropy

在这个阶段,发现系统没有实现随机序列功能,因此需要添加一个随机序列来实现音效的随机播放。具体来说,要在系统中引入一个随机种子(random_series)机制,以便生成不同的随机数序列。通过对代码的检查,发现游戏中已经有了随机种子的实现方式。因此,可以利用现有的机制来生成随机数。

为此,首先需要将随机种子设置到游戏状态中,并且确保该种子能影响到音效播放中的随机行为。完成这些步骤后,应该就能顺利地进行声音的随机播放测试。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

触发剑时候段错误

必须得在播放声音吧已经播放的声音的样本数设置为0才行
当播放新声音时,需要重置所有相关参数,但 SamplesPlayed(已播放的采样数)没有被正确重置为零。这导致在后续的播放过程中,声音的状态可能不正确,进而影响音频的播放逻辑。要修复这个问题,需要在 PlaySound 函数中确保 SamplesPlayed 被正确初始化为 0,以保证声音从头开始播放而不会出现异常行为。
在这里插入图片描述

在这里插入图片描述

game.cpp: 调查 bug

在检查代码时,发现存在一个问题。具体来说,在播放声音时,未正确更新“下一个播放声音”的指针。应当在播放完成后,将“PlayingSound->Next”指向游戏状态中的“FirstFreePlayingSound”。这个指针没有正确更新,导致播放声音的处理没有按照预期进行。

Blackboard: 链表

在代码中使用了一个链表来管理播放的声音。链表中的每个节点代表一个正在播放的声音,包含指向下一个声音的指针。问题出在链表中节点的连接处理上。当播放声音结束后,链表的“next”指针需要正确地跳过已完成的声音,指向下一个正在播放的声音。然而,在实现过程中,只有链表中的一部分得到了处理,忽略了更新指向新节点的指针。这导致了链表的指针没有正确连接,影响了声音播放的管理。

game.cpp: 正确构建这个链表

在管理播放声音的链表时,需要跟踪每个节点的指针。每个“playing sound”都指向一个声音,并且通过链表的“Next”指针连接下一个播放的声音。为了解决指针更新的问题,应该在遍历过程中保持对当前节点的指针,并在需要时通过更新“PlayingSoundPtr”来正确指向下一个节点。

具体来说,处理播放声音时,需要确保在移除当前节点时,前一个节点的指针能够指向下一个节点。这个操作的关键在于在移除一个节点时,将当前节点的“next”指针赋值给上一个节点指针,从而保持链表的完整性。这样一来,不需要额外的操作来更新链表,只需要确保每次移除节点时,正确更新指针,链表自然会自动推进。

通过这种方式,链表的管理变得更加高效,并且避免了不必要的重复操作。
在这里插入图片描述

调试器: 步进查看 playing_sound 链表

在这个过程中,目标是简化播放声音的循环。最开始时,操作的是指向“playing_sound”的指针,而现在是操作指向“PlayingSoundPtr”的指针的指针,即查看指针所在的位置。在这种方法中,首先需要获取第一个播放声音的指针,并从这个位置获取当前的播放声音。然后,其他的操作就按正常流程进行。

如果播放的声音已经完成,应该进行一些额外的操作。首先,要确保记住当前节点的“前驱”指针,即指向当前节点的指针。然后,往前推进时,只需取出当前节点的“下一个”指针来替代当前节点的指针,继续处理下一个节点。完成后,将当前节点放回空闲列表。

通过这种方式,链表的结构得到更新,并且指针的操作变得更加清晰、简化。完成后,循环会自动继续到下一个声音,无需额外复杂的操作。

之前的一个错误

在这里插入图片描述

梳理一下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

运行游戏并听到我们的声音

我们确认了一些内容,调整了一些设置,现在一切看起来都已经恢复正常。

在回顾的过程中,我们重新设置了一些参数,并进行了测试。在播放音效时,出现了一个声音,并不完全是我们希望在游戏中射击时听到的声音。这意味着当前的音效可能不太符合预期,或者需要进一步调整,以确保射击音效听起来更符合游戏的氛围。

此外,还听到了一些额外的声音,包括一些无关的声音元素。这表明当前的音频处理可能还存在一些问题,需要进一步优化或筛选,以避免多余或不恰当的音效混入最终的输出。

虽然已经完成了主要的内容,但仍然有一些额外的工作需要进行,例如进一步调整音频参数、优化音效播放机制,或者确保所有的声音都符合游戏设计的要求。不过,总体来说,核心部分已经完成,只是一些细节仍需优化。

由于时间关系,接下来需要转向其他任务,后续可以再继续优化这些细节,以确保音效的最终效果符合预期。

声音缓冲区是对数还是线性(分贝值还是线性值),我们是否需要在构建缓冲区的和时考虑到这一点?

我们讨论了声缓冲(sound buffer)的特性,主要关注其是线性的(linear)还是对数的(logarithmic),以及在计算混音总和时是否需要考虑这一点。

在分析过程中,我们确认了声缓冲是线性的,而不是对数的。这意味着在进行音频混合时,样本的数值是以线性方式进行叠加的,而不需要进行对数运算或额外的非线性处理。这一点与某些音频系统(如人耳感知的响度)不同,因为人耳对声音的感知通常是对数的,但声缓冲本身依然是线性存储的。

由于声缓冲是线性的,在进行混音时,可以直接相加各个声音的样本值,而不需要考虑对数缩放或其他复杂的变换。这使得音频混合的实现更加直观和直接,同时也符合一般的数字音频处理方式。

最终,我们得出结论:声缓冲是线性的,不是对数的,因此在构建混音总和时,无需额外考虑对数缩放的问题

你们已经有人在为游戏创作原声带了吗?

我们讨论了游戏的音乐和艺术资源的版权问题,以及未来可能的规划。目前,游戏的配乐是通过授权获得的,因此在版权方面受到了限制,无法随意发布或修改。这意味着,我们无法将当前的音乐作为游戏源代码的一部分发布,也无法将其归入公共领域(public domain)。

未来是否会重新制作配乐,取决于游戏的销售情况。如果游戏的收入足够,可能会考虑聘请作曲家专门创作音乐,以便能够完全掌控其版权,并将其作为游戏的一部分发布。这样的话,音乐可以像游戏的源代码一样,被自由分发,甚至有可能进入公共领域。但目前尚不确定是否会有足够的资金支持这个计划。

相比之下,游戏的美术资源是完全自主创作并拥有全部版权的,因此可以随时以任何方式发布。例如,可以选择将部分美术资源公开,让任何人都可以在自己的游戏中自由使用,就像游戏的代码计划进入公共领域一样。不过,目前还没有明确的计划,只是希望保留这样的可能性,以便未来可以自由决定如何处理这些资源。

提醒一下 InterlockedIncrement 中的参数顺序及其掩盖的 bug

我们一直想修复这个问题,但不确定这是否就是要修复的那个问题。之前已经注意到 InterlockedIncrement 相关的问题,但不确定这是否就是讨论中的问题。

关于 InterlockedIncrement 中的参数顺序,可能掩盖了某个 bug。我们一直有意修复这个问题,但现在才真正开始处理。当前遇到的疑问是,是否在错误的地方复用了 InterlockedIncrement,特别是在 Queue->CompletionCount 上的使用方式可能存在问题。

在检查时,发现 InterlockedIncrement 可能被重复使用,但不确定这是否就是导致问题的原因。为了进一步确认,需要回溯到之前讨论的具体内容,以确保正在处理的是正确的问题。

game_asset.cpp: 处理 LoadBitmap 中 BeginTaskWithMemory 失败的情况

我们不确定具体指的是哪个 bug,因此需要更具体的描述。我们其实是在考虑另一个问题,主要是在进行原子比较交换(atomic compare exchange)时会遇到的情况。问题出现在任务开始时,如果任务开始失败了,我们希望能够确保将资源的状态恢复为“未加载”状态。这样做是因为,如果没有恢复到“未加载”状态,那么资源将进入队列,但永远不会被分配任务,因为队列中没有任务可以处理,结果资源就永远无法加载。

这个问题虽然今天没有讨论到,但它确实是我们在论坛上曾经提到过的一个 bug,需要修复。我们需要确认是否有在相关代码中加上这个修复。
在这里插入图片描述

在这里插入图片描述

将单声道声音混合到立体声时,每个通道的音量应该是中间声道的 50%

当缺少单声道声音时,如果每个声部的音量设置为50%,那么它们会被放在中间声道(center panned)。这个做法的原因似乎是,如果我们想让声音移动到另一边,可以更容易地调整音量。例如,通过在两个声道之间平滑过渡,可以实现声音逐渐向另一侧推送。

然而,这样的设置是否合理则取决于我们想要的效果。一个可能的默认设置是这样做,因为在中间声道时,两个声部的音量合并成一个,而50%能避免两个声音叠加时过于吵杂。虽然我可以理解这种方式的合理性,但我不确定是否完全认同这一点。因为如果想要声音在中间时达到全音量,可能需要不同的调整方式。可能我更倾向于让声音在向其他声道移动时逐渐变得较安静,而不是保持一定的音量。

因此,虽然这可以作为一个默认设置,我仍然不确定是否完全适合,可能还需要进一步探讨。

这个音频代码是否允许你同时播放相同的声音(即,如果在第一次播放结束之前再次启动相同的声音)?

这段音频代码的目的是让同一个声音可以同时播放,也就是在第一次播放还没有结束之前,启动同一个声音的第二次播放。从描述来看,代码可能是通过允许在音频还未完全播放完时再次启动同一音频来实现这个功能。

不过,这是否能够实现取决于代码具体的实现方式。一般来说,如果音频播放系统允许同一个声音实例同时多次播放,就应该能够成功。但如果系统有一个限制,比如不允许同一个音频文件在播放完成前被再次触发,那么代码就不会实现预期的效果。

因此,是否能成功播放同一个音频文件取决于音频播放机制的设计。假如允许多次播放同一声音而不会覆盖或冲突,那么就能实现并行播放;但如果音频播放器在处理时会拒绝重复播放,或者音频资源没有正确管理,就可能会导致问题。

比较和交换中的第二和第三个参数

关于提醒的代码,讨论的是比较和交换(compare and swap)中的第二个和第三个参数。我们最初可能混淆了函数,误以为是在讨论另一个函数。实际上,我们谈论的是原子比较交换(atomic compare exchange)。

现在,问题是在比较和交换的过程中,这两个参数的顺序是否正确。需要检查一下代码,看这两个参数是否被放置在了错误的顺序里。具体来说,原子比较交换操作的顺序应该是:首先是预期值(expected),然后是新的值(new)。这个顺序非常重要,因为它决定了交换操作的正确性。

game_intrinsics.h: 交换 AtomicCompareExchangeUInt32 中的参数,然后再交换回来,并改变其调用的函数

我们发现,之前的比较和交换(compare and swap)操作中,参数顺序确实错了,这导致了某些行为不符合预期。虽然代码似乎能够正常工作,但实际操作可能存在其他隐藏的 bug。我们决定进一步调查并确保这部分的实现是正确的。

为了避免混淆,包括自己在内的开发者更容易理解,我们打算将代码调整为与 Windows 中的实现保持一致,因为 Windows 是采用这种方式的。这种做法可以避免产生混淆,特别是参数的顺序。我们查阅了相关代码,确认原子比较交换操作中的顺序应该是:预期值(expected)放在最后一个位置,新的值(new)放在前面。

在我们期望的场景中,任务的状态应该是“未加载”(unloaded),但我们发现系统意外地将状态设为了“排队中”(queued)。这显然是由于参数顺序错误导致的,然而,系统还是能够返回“未加载”状态,这其实是因为交换操作从未真正执行过。由于条件设置问题,操作总是会返回“未加载”状态,这解释了为何代码在错误的实现下仍然能够“正常工作”,但这种行为并不符合预期。

因此,调整代码的参数顺序后,应该能够避免混淆,并且确保原子比较交换操作的正确性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

目前音频似乎依赖于帧率。是否可以将其放在单独的线程中或使用中断(如果可以的话),使其不再依赖于帧率?

音频的播放似乎是与帧率相关的。如果将音频放在一个单独的线程中,或者使用中断,可能有助于让音频不再依赖于帧率。

然而,在 Windows 上使用中断并不太现实。不过,确实可以将音频处理放到一个独立的线程中,这样可以减少音频对帧率的依赖。然而,这样做有一个问题,就是必须确保该线程不会被“饿死”,因为操作系统无法保证一定会及时唤醒这个线程。

通常的解决方案是,虽然帧率可能不稳定,但可以通过在音频缓冲区中加入更多的音频数据,确保下一个帧率周期的音频数据足够。如果帧率保持正常,那么可以将新音频数据覆盖到缓冲区中,如果没有达到预期的帧率,则音频可能会出现“跳跃”的现象,但这并不会对整体效果造成太大影响。

目前,这个问题不太需要担心,因为理想情况下,游戏应该总是能够保持稳定的帧率。在发布的游戏中,错过帧率是非常罕见的,这是不应该发生的。然而,考虑到一些外部因素,例如后台程序占用资源,偶尔可能会错过某一帧。在这种情况下,可能需要采取一些措施来确保音频处理不受影响,虽然这些措施暂时并不紧急。

你能为 gcc / clang 添加一个 __sync_val_compare_and_swap(Value, Expected, New) 吗?

我们讨论了是否需要添加一个 __sync_val_compare_and_swap,并且提到需要考虑 GCC 和 Clang 编译器的支持。虽然可以完全实现这个功能,但目前不太记得具体的实现细节。

同时,我们还考虑是否已经在代码中定义了其他编译器相关的宏或者标识符。如果没有,我们可能需要再检查一下是否需要为其他编译器添加相应的支持。

game_intrinsics.h: 添加 tfnw 的建议

讨论的重点是关于添加 __sync_val_compare_and_swap,以及如何在 GCC 中处理相关的问题。首先,回忆起上次的实现,记得曾经用过某些特定的指令来处理这个问题。对于 GCC,我们需要确定使用哪种“屏障”指令来实现同步。

具体来说,提到如果想在 GCC 中实现这个功能,我们需要知道应使用什么指令来保证内存的同步。如果使用 volatile 关键字,它可以起到一定的屏障作用,这符合我们记得的做法。尽管这个方法有些不寻常,但它似乎是有效的。

接下来,我们计划将 __sync_val_compare_and_swap 的功能添加到代码中,并测试其是否按预期工作。我们会进行一些微调,确保它能正确地执行,然后将代码提供给其他人下载和测试,看看实际效果如何。

如果还有其他问题,可以进一步调整和完善。

game.cpp: 播放音乐而不是 bloop,以演示同时播放相同的声音

我们决定回答关于是否能够在同一时间播放多个声音的问题。为了演示这个功能,我们打算不播放平常的声音,而是选择一个非常响亮且持续时间很长的声音进行测试。通过这个方法,可以清楚地展示系统是否能够处理多个声音同时播放的情况,确保音频不会被覆盖或中断。

那为什么它能工作?

之所以能实现多个声音同时播放,是因为播放声音和实际的声音数据是两个完全独立的概念。我们采用了将播放中的声音与实际加载的声音分开的方法,创建了一个单独的播放声音列表。这样,我们可以在列表中添加任意多个播放条目,而这些条目可以都指向同一个底层的声音缓冲区。

换句话说,播放的声音是基于播放列表的数量,而不是加载的声音数量。播放的循环是针对当前正在播放的声音进行的,而不是针对所有已加载的声音。这种方法使得可以同时播放多个声音,即使它们指向相同的声音数据。

Blackboard: 将资产数据与实例数据分开

我们实现了一个完全分离的系统,具体来说,加载的声音和正在播放的声音是两个独立的部分。加载的声音存放在一个地方,而正在播放的声音则在另一个地方。当我们加载一个声音并将其添加到播放列表时,可以根据需要堆叠任意数量的播放声音,这些播放声音都指向同一段音乐。

每个播放声音都保存了已经播放的样本数量,从而能够记录每个播放实例在不同时间点的位置。这一点非常重要,因为它说明了每个播放实例的数据是独立的,即使它们指向相同的声音数据。

这里的核心概念是,应该始终将资源数据(例如声音的定义)与实例数据(例如播放声音的状态)分开。这就像定义一个结构体类型,然后可以创建多个该类型的对象。我们也为每个播放声音定义了一个结构体,并可以创建多个实例。同样,对于声音资源系统也是如此,我们加载了声音文件并可以随时播放它。通过这种方式,可以在不干扰其他播放实例的情况下,反复播放同一声音。

这样,系统就能支持同时播放多个相同的声音实例,并且每个实例的播放位置和状态都能独立管理,确保系统的灵活性和高效性。

为什么使用链表而不是其他数据结构,比如vector?

使用链表而不是其他数据结构(如vector)的原因是,链表在随机添加和移除元素时表现得更高效。vector在这方面存在一些局限性。具体来说,当你往vector中添加元素时,如果vector满了,你可能需要重新分配整个vector,这样会带来较大的性能开销。而当你删除元素时,如果没有做额外的空闲列表跟踪,通常需要将整个vector压缩,重新整理元素。

而链表则避免了这些问题,因为它在随机添加和移除元素时非常高效,尤其是在不需要进行随机访问的情况下。在这种情况下,链表能够很好地工作,因为它专门为频繁的插入和删除操作设计,不需要像向量那样进行整个结构的重新分配或压缩。

总的来说,如果不需要对数据进行随机访问,只需要频繁地添加和删除元素,链表是一个非常合适的数据结构,能够提供更好的性能。

但是你能拿到声音求和机制的输出并播放它吗?

如果需要,完全可以将声音合成机制的输出播放出来。没有任何理由不能这么做。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/982813.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Goby 漏洞安全通告| Ollama /api/tags 未授权访问漏洞(CNVD-2025-04094)

漏洞名称:Ollama /api/tags 未授权访问漏洞(CNVD-2025-04094) English Name:Ollama /api/tags Unauthorized Access Vulnerability (CNVD-2025-04094) CVSS core: 6.5 风险等级: 中风险 漏洞描述: O…

Python----数据分析(Matplotlib五:pyplot的其他函数,Figure的其他函数, GridSpec)

一、pyplot的其他函数 1.1、xlabel 在matplotlib中, plt.xlabel() 函数用于为当前活动的坐标轴(Axes)设置x轴的 标签。当你想要标识x轴代表的数据或单位时,这个函数非常有用。 plt.xlabel(xlabel text) 1.2、ylabel 在matplotl…

构建python3.8的docker镜像,以便解决: dlopen: /lib64/libc.so.6: version `GLIBC_2.28‘

1、简介 在使用pyinstaller打包工具打包应用为二进制的时候,出现了一个“”: dlopen: /lib64/libc.so.6: version GLIBC_2.28”的问题 2、解决方案 2.1、问题原因 由于使用了官方提供的镜像,而官方提供的镜像编译的机器上、glibc的版本过高&#xff…

音频3A测试--AEC(回声消除)测试

一、测试前期准备 一台录制电脑:用于作为近段音源和收集远端处理后的数据; 一台测试设备B:用于测试AEC的设备; 一个高保真音响:用于播放设备B的讲话; 一台播放电脑:用于模拟设备A讲话,和模拟设备B讲话; 一台音频处理器(调音台):用于录制和播放数据; 测试使用转接线若…

MATLAB程序介绍,三维环境下的IMM(交互式多模型),使用CV和CT模型,EKF作为滤波

本文所述的MATLAB代码为三维的交互式多模型(IMM)滤波器,结合了匀速直线运动(CV模型)和匀速圆周运动(CT模型)的状态估计。使用扩展卡尔曼滤波(EKF)来处理状态更新与观测数…

upload-labs详解(1-12)文件上传分析

目录 uploa-labs-main upload-labs-main第一关 前端防御 绕过前端防御 禁用js Burpsuite抓包改包 upload-labs-main第二关 上传测试 错误类型 upload-labs-env upload-labs-env第三关 上传测试 查看源码 解决方法 重命名,上传 upload-labs-env第四关…

第一:goland安装

GOPROXY (会话临时性),长久的可以在配置文件中配置 go env -w GOPROXYhttps://goproxy.cn,direct 长久的,在~/.bashrc文件中添加: export GOPROXYhttps://goproxy.cn,direct ----&#xff0d…

ASP使用EFCore和AutoMapper添加导航属性数据

目录 一、不使用自增主键 (1)下载AutoMapper的nuget包 (2)配置映射规则 (3)配置MappingProfile文件 (4)控制器编写添加控制器 (5)测试 二、使用自增主…

什么是Jmeter? Jmeter工作原理是什么?

第一篇 什么是 JMeter?JMeter 工作原理 1.1 什么是 JMeter Apache JMeter 是 Apache 组织开发的基于 Java 的压力测试工具。用于对软件做压力测试,它最初被设计用于 Web 应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源…

汽车零部件厂如何选择最适合的安灯系统解决方案

在现代制造业中,安灯系统作为一种重要的生产管理工具,能够有效提升生产线的异常处理效率,确保生产过程的顺畅进行。对于汽车零部件厂来说,选择一套适合自身生产需求的安灯系统解决方案尤为重要。 一、安灯系统的核心功能 安灯系统…

Ubuntu20.04双系统安装及软件安装(七):Anaconda3

Ubuntu20.04双系统安装及软件安装(七):Anaconda3 打开Anaconda官网,在右侧处填写邮箱(要真实有效!),然后Submit。会出现如图示的Success界面。 进入填写的邮箱,有一封Ana…

为解决局域网IP、DNS切换的Windows BAT脚本

一、背景 为解决公司普通人员需要切换IP、DNS的情况,于是搞了个windows下的bat脚本,可以对有线网络、无线网络进行切换设置。 脚本内容 echo off title 多网络接口IP切换工具:menu cls echo echo 请选择要配置的网络接口: echo echo 1. 有线网络&am…

【OMCI实践】wireshark解析脚本omci.lua文件(独家分享)

引言 omci.lua文件是Wireshark的OMCI协议解析插件的核心组件。它配合BinDecHex.lua,可以解析OMCI协议的数据包,提取出消息类型、受管实体标识、受管实体属性等关键信息,并以人类可读的形式显示在Wireshark的解码视图中,方便研发人…

JPA编程,去重查询ES索引中的字段,对已有数据的去重过滤,而非全部字典数据

一、背景 课程管理界面,查询前,需要把查询元数据给出。 学科列表、学段列表和分类列表,我们把它定义为查询元数据。 一般的业务需求是: 系统维护好多个字典,比如学科、学段等等,相当于属性库。 但是&…

vue3与react、 react hooks

一、Vue3新特性:setup、ref、reactive、computed、watch、watchEffect函数、生命周期钩子、自定义hooks函数、toRef和toRefs、shallowReactive 与 shallowRef、readonly 与 shallowReadonly、toRaw 与 markRaw、customRef、provide 与 inject、Fragment、Teleport、…

LINUX网络基础 [二] - 网络编程套接字,UDP与TCP

目录 前言 一. 端口号的认识 1.1 端口号的作用 二. 初识TCP协议和UDP协议 2.1 TCP协议 TCP的特点 使用场景 2.2 UDP协议 UDP的特点 使用场景 2.3 TCP与UDP的对比 2.4 思考 2.5 总结 三. 网络字节序 3.1 网络字节序的介绍 3.2 网络字节序思考 四. socket接口 …

基于SpringBoot+Vue的校园美食分享平台的设计与实现

获取源码:SpringBootVue的校园美食分享平台: 系统角色上分为管理员以及普通用户进行实现, 管理员主要负责整个网站后台的维护管理,包括首页、学校管理、美食类型管理、餐厅管理、美食分享管理、美食评论管理、系统信息、用户管理、角色管理和…

WPS条件格式:B列的值大于800,并且E列的值大于B列乘以0.4时,这一行的背景标红

一、选择数据区域 选中需要应用条件格式的区域(例如A2:E100 )。 二、打开条件格式 点击“开始”选项卡,选择“条件格式” > “新建规则”。 三、选择规则类型 选择“使用公式确定要设置格式的单元格”。 四、输入公式 在公式框中输入以…

自由学习记录(42)

可能会出现到后面没有教程可以看,走不动,,但还是尝试吧 过程远比想象的要多 那连Live2d的这些脚本怎么控制的都要了解一下 ------------ 文件类型和扩展名 | 编辑手册 | Live2D Manuals & Tutorials 全部导入之后 在这下载SDK Live2D…

结构型模式---享元模式

概念 享元模式是一种结构型设计模式,他摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。享元模式将原始类中的数据分为内在状态数据和外在状态数据。 内在状态:就…