游戏引擎学习第137天

news/2025/3/6 18:42:25/

演示资产系统中的一个 bug

我们留下了个问题,你现在可以看到,移动时它没有选择正确的资产。我们知道问题的原因,就在之前我就预见到这个问题会出现。问题是我们的标签系统没有处理周期性边界的匹配问题。当处理像角度这种周期性的标签时,系统不知道如何匹配最接近的值,这就是问题的根源。因此,今天我们需要花一点时间在黑板上讲解这个问题,看看该如何修复它。

黑板:周期性标签匹配

我们面临的问题是如何处理周期性的标签匹配。昨天,有人提出了一个很好的建议,即为了统一处理所有标签,或许我们可以引入“所有标签都是周期性”的概念,对于那些不想作为周期性处理的标签,我们可以为它们设置一个非常大的周期。这是一个非常不错的思路。接下来,我将详细解释一下我们遇到的问题,并探讨如何实施这个建议,即让所有标签都有一个周期,但将其设置为非常大,以免影响那些不需要周期性处理的标签。

问题的根源

我们的核心问题在于使用了一个叫做 atan2 的函数。这个函数的作用是将一个方向向量转换为一个角度,表示物体朝向的方向。举个例子,当我们使用方向向量(如速度向量)时,atan2 会将其转化为一个角度,这样就能得到一个标量值,用作标签值。我们可以根据这个标签值来从资产库中匹配对应的精灵(比如玩家移动的方向,或者其他物体的移动方向)。

问题出现在 周期性 上。atan2 返回的角度是周期性的,意味着当角度转动一圈(360度)时,它会回到初始位置。比如,如果我们转动了360度,就会回到0度,而不是转到一个新的角度。为了匹配资产,我们不能简单地比较两个角度的标量值,像0度和360度,虽然它们看起来差距很大,但它们其实是相同的,都是代表同一个方向。因此,我们需要一种方式来判断这些看似不同的值其实是接近的,这就是周期性带来的问题。

atan2 的输出范围

atan2 返回的结果是围绕0对称的,它给出的角度范围是 -180 到 +180,而不是从0到360。换句话说,如果角度是0,atan2 会返回 -180 到 +180 之间的值,分别表示逆时针和顺时针的180度。如果用弧度来表示,它的范围是 -π 到 +π。因此,我们的目标是让这个匹配机制适应这样的角度范围。

解决方案

我们可以将角度范围编码成 -π 到 +π,这样的匹配会比较容易。比如,某个标签值可能对应 0、±π/2、π 和 -π/2,这样就能够很自然地进行匹配。而且,负π和π实际上是一样的,应该匹配到同一个标签。

对于那些不需要周期性处理的标签,我们可以简单地为它们设置一个非常大的范围,比如从 -100000 到 100000,这样就避免了它们与周期性标签冲突,因为这个范围远远超过了我们需要处理的角度范围。只要这个范围足够大,不会与周期性标签的范围交错,就可以避免错误匹配。

总结来说,通过将周期性标签的范围设置为 -π 到 +π,对于不需要周期性匹配的标签设置一个很大的范围,这样就能够高效且准确地处理标签匹配问题。

黑板:“邻域运算符”

问题的核心是如何在匹配过程中处理周期性角度。在周期性标签的匹配中,当我们有一个角度 A 和一个角度 B,假设我们想要将这两个角度与一个已设置为 π 的角度进行匹配时,我们需要避免计算出 A 和 B 与 π 的差异非常大,导致不正确的匹配。换句话说,我们希望能够处理这种情况,使得无论角度如何旋转,能够正确地认为它们是接近的。

问题的背景

对于周期性角度,我们希望当角度 A 和角度 B 相差很大时,能够将它们视为接近的,而不是认为它们在圆的两侧,相差接近 360 度。例如,当 A 是 350 度,B 是 10 度时,它们实际上应该是非常接近的,因为在 360 度的周期内,它们实际上只相差 20 度。这个问题的解决方案是引入一个“邻域操作符”(neighborhood operator)。

邻域操作符的作用

邻域操作符的作用是计算两个角度的最近距离,而不是简单地通过角度值来比较它们的大小。邻域操作符将两个值映射到它们之间的最小距离,而这个距离是相对的,考虑了周期性的因素。

假设我们有两个角度 A 和 B,我们希望计算 A 在 B 的邻域中时应该如何调整。我们不关心得到的邻域值本身,而是关心调整后的角度之间的差异。具体地,如果我们有一个角度 A,当前角度值为正值且距离 B 很远,我们希望通过邻域操作符将 A 调整成负值,使其更加接近 B。这样,两个角度之间的差异将变得更小,而不是通过直接计算它们的差值(可能得到一个很大的正数)来产生错误的匹配。

举例说明

假设 A 的值是 350 度,B 的值是 10 度。直接计算它们的差值,会得到 350 度和 10 度之间的差值是 340 度,这会被认为它们距离很远。通过邻域操作符处理后,A 的角度会变为 -10 度(即顺时针旋转 10 度),而 B 的角度依然是 10 度,这样它们之间的差值就变成了 0 度,也就是完全匹配。这个调整是通过邻域操作符来实现的。

实现思路

在实现时,我们可以将邻域操作符视作一个函数,接受两个角度值作为输入,输出它们之间最小的差异。这意味着,如果两个角度值相差超过 180 度(例如 A 是 350 度,B 是 10 度),我们会通过周期性调整将其“拉近”到较小的范围,从而获得更精确的匹配结果。

简单来说,邻域操作符的功能就是根据周期性调整角度的差值,使得它们始终保持在最小的范围内,从而确保匹配过程的正确性。

总结

为了处理周期性角度的匹配问题,我们需要使用邻域操作符来计算角度之间的最小差距,而不是直接计算它们的差异。这使得我们可以在角度的周期性范围内,避免因大范围的旋转而导致错误的匹配,确保角度在匹配时总是能够准确地找到最接近的值。

黑板:“邻域距离”

在这里,我们实际上并不需要一个“邻域算子”(Neighborhood Operator),因为我们并不关心生成这个“a”,我们真正关心的是计算“邻域距离”。所以,我们的目标是做一个类似于邻域操作的过程,但我们只关心计算两个值之间的距离,而不需要生成具体的邻域值。

我们不需要实际生成邻域值,只需要计算它们的距离就可以了,这样可以让整个过程变得更简单。虽然在某些情况下,确实需要进行邻域操作(例如插值时),但在这种情况下,我们只关心两个角度之间的最短距离,而不需要真正生成邻域。因此,我们只需要计算距离即可,而不需要实际执行复杂的邻域操作。

接下来,假设我们已经知道所有值都在一个特定的范围内(比如在-π到π之间),我们就可以根据这个范围来判断角度之间的关系。具体来说,正数和负数总是有方向上的差异,但这并不影响它们之间的距离计算,因为无论如何,我们都能够在一个周期范围内进行计算。

在这种情况下,最简单的做法是检查这两个角度的符号(即它们的正负),如果它们的符号不同,那么它们就可能不在同一个邻域内。否则,它们就可能在相同的邻域内。

然而,在进一步思考后,我们发现其实并不需要专门进行符号检查。我们可以简化流程,直接跳过这一步。总的来说,我们只需要关注如何处理这两个角度之间的最短距离,而不需要关心它们是否在同一个邻域内。

黑板:将一个负范围的点的符号改变,计算它到正范围点的距离

我们有两个数,一个是正数,一个是负数,目标是计算它们之间的距离。首先,我们发现它们的符号不同,因此我们想要改变其中一个数的符号,使其变成与另一个数相同。具体来说,我们希望将负数变成正数。

为了做到这一点,我们可以通过以下方式来操作:负数的旋转(负旋转)其实就是从零开始,绕一圈的正向距离加上多出来的部分。因此,我们可以从负π开始,向正方向进行旋转,直到到达目标数B的位置,这样就变成了正旋转。

例如,如果B是负数,我们可以通过从负π开始,顺时针旋转得到B,实际上就是等于B加上整个范围(2π)。所以,B变成正数的操作就是将B加上这个周期范围,即加上2π。

这个过程说明,负数通过加上整个周期(2π)就可以转化为对应的正数。这样我们就可以用这种方式来处理符号不同的数值,使它们符合我们需要的范围和计算要求。

黑板:将一个正数移动到负数

如果我们有一个正数,想要将其转化为负数,过程和之前的情况类似,但这次是进行负旋转。

  1. 负数转正数
    如果我们有一个负数,我们可以通过加上整个周期(2π)使其变成正数。这样,通过顺时针旋转的方式,可以将负数转变为正数。

  2. 正数转负数
    如果我们想要把一个正数变成负数,步骤就有所不同。具体来说,我们需要从正数的位置(例如π的位置)开始,进行逆时针的旋转。

    以正数A为例,假设A位于[0, π]范围内,要将其转变为负数,可以按照以下步骤:

    • 从π的地方开始逆时针旋转到A的位置。
    • 这时候,我们需要从π位置开始,逆时针旋转到A,这相当于减去一个π,即我们需要将A减去π(因为π是最大正数)。

    例如,如果A是一个正数,位于(0, π)之间,我们可以将其转换为负数,方法是将这个数减去π,使其变成负数。

  3. 总结

    • 对于负数转正数,我们通过加上2π来实现。
    • 对于正数转负数,我们通过减去π来实现。

    这个过程就相当于在单位圆上进行旋转,改变角度的符号,通过加或者减掉2π或π来实现符号的转换。

黑板:做这个邻域计算的完整公式

我们讨论的是如何计算两个数之间的“邻域”(neighborhood),这个操作实际上并不是特别复杂,我们可以通过计算它们之间的差距来完成。

1. 正数和负数之间的转化

  • 如果我们有一个正数和一个负数,并且想要计算它们之间的距离,我们首先需要确定它们的符号。如果这两个数符号不同(一个是负数,一个是正数),我们可以通过调整其中一个数的符号,使得两个数具有相同的符号。
  • 具体来说,如果我们想把一个负数变为正数,方法是加上一个周期 2 π 2\pi 2π。反之,如果我们想把一个正数变为负数,方法是减去 π \pi π

2. 计算两个数之间的距离

  • 如果两个数符号相同,我们可以直接计算它们的距离,即它们的差值。
  • 如果两个数符号不同,则我们需要做一些额外的操作。我们可以将负数转化为正数,或者正数转化为负数,来让它们具有相同的符号,然后再计算它们之间的差距。
  • 在计算时,我们可以用一种简化的方式,避免进行符号判断。我们可以分别计算两个数之间的差距,然后选择较小的距离作为最终结果。

3. 具体操作

  • 假设我们有两个角度值,分别是 θ \theta θ ϕ \phi ϕ
    • 如果 θ \theta θ 是负数,而 ϕ \phi ϕ 是正数,我们可以通过加上周期来将负数转为正数。
    • 如果 θ \theta θ 是正数,而 ϕ \phi ϕ 是负数,我们可以通过减去 π \pi π 来将正数转为负数。
  • 无论是正负数之间的转换,核心操作是计算它们之间的“差距”。我们可以通过加减周期的方式,将符号统一,然后计算差值。

4. 简化计算

  • 不需要显式判断符号,我们可以直接计算两者之间的距离:
    • 首先计算 θ − ϕ \theta - \phi θϕ
    • 然后计算 θ − (range) × sin ⁡ ( θ ) \theta - \text{(range)} \times \sin(\theta) θ(range)×sin(θ),其中 range 是整个范围的大小(例如 2 π 2\pi 2π)。
    • 然后,取这两个差值的最小值,即可得到两个数之间的实际距离。

5. 结论

  • 通过简化的操作,我们可以避免使用条件分支来判断符号,而是直接计算两种情况的差值,最终选择较小的距离。这样既避免了分支操作,也提高了计算效率。

game_asset.h:将 HalfTagRange 添加到游戏资产

在进行资产匹配时,我们希望将周期性(periodicity)引入到资产中,以确保匹配的准确性和一致性。为了实现这一点,我们需要对周期的理解和管理。

1. 理解周期性

  • 我们并不需要为每个标签(tag)都单独设置一个周期。实际上,周期通常是由标签的类型决定的,因此只需要为每种标签类型定义一个周期即可。
  • 这样,周期性实际上是和标签类型紧密相关的,而不是每个标签实例都需要一个单独的周期。

2. 周期的存储和管理

  • 在资产中,每个标签(tag)通常会有一个周期字段,这个周期字段可以命名为 tag period 或者 half tag range,它表示标签的周期范围。
  • 这个“半标签范围”(HalfTagRange)可以理解为每种标签类型对应的周期的一半。例如,如果一个标签的周期是 2 π 2\pi 2π,那么半周期就是 π \pi π
  • 这样,我们在管理标签时,只需要在资产的标签中为每个标签类型存储一次周期值,而不需要为每个标签实例都单独存储周期。这样可以减少存储空间并提高效率。

3. 实际操作

  • 每次在处理资产标签时,只需要知道每种标签的周期范围就可以进行相应的操作。例如,对于每种标签类型,我们可以在标签结构中定义一个字段来存储周期信息(如 tag period),并根据该值来进行计算。
  • 通过这种方式,标签的周期性可以高效地管理和应用,从而简化了资产匹配的过程。

4. 总结

  • 不需要为每个标签单独定义周期,而是可以通过定义标签类型的周期(或者半周期)来管理周期性。这样,我们只需存储每种标签类型的周期范围,而在实际处理时直接使用这些周期信息。
  • 这种方法不仅简化了周期的管理,也提高了资产匹配过程的效率和准确性。

game_asset.cpp:为每个 TagType 设置 HalfTagRange

大多数情况下,我们并不关心周期性问题,周期性标签的出现频率远低于非周期性标签。因此,我们可以采取一种简化的方式来处理标签的周期性。以下是处理流程的详细步骤:

1. 初始化资产时的周期性处理

  • 在初始化游戏资产时,我们可以遍历所有的标签类型,并为每种标签类型设置一个周期范围。对于大多数标签类型,我们不需要关心周期性,所以可以将它们的周期设置为一个非常大的数字。
  • 这个大数字的作用是标识这些标签类型没有周期性(即它们的周期范围几乎是无穷大的)。这个值可以是一个极大值,只要它是我们永远不会使用的值就行,因为它只是占位符,并不会实际影响计算。

2. 处理周期性标签

  • 对于需要周期性处理的标签类型,我们不需要事先知道所有这些标签,只需要关注那些确实具有周期性的标签。
  • 目前已经知道的一个周期性标签是“朝向方向”(facing direction),它的周期是 π \pi π,即无论朝哪个方向,最大旋转角度是 π \pi π(180度)。对于这种情况,我们可以直接将其周期设置为 π \pi π,而其他不需要周期性处理的标签则保持为一个非常大的值。

3. 具体实现

  • 在初始化过程中,对于所有标签类型,我们先给它们设置一个默认的大周期值(如 1 0 10 10^{10} 1010 或其他极大值)。然后,对于确实需要周期性处理的标签(如朝向方向),我们单独为它们设置实际的周期值(如 π \pi π)。
  • 这样,周期性标签的处理就能被单独标识出来,并在后续的计算中正确处理,而其他标签类型则无需进行周期性计算。

4. 总结

  • 大部分标签都不涉及周期性,所以我们为这些标签设置一个非常大的周期值,表示它们不需要周期性处理。
  • 只有少数几个标签会涉及周期性处理(如朝向方向),这些标签的周期值设置为实际的周期值(如 π \pi π)。
  • 通过这种方法,可以在初始化时简化标签的周期性处理,并在需要时进行特定的周期性计算。

在这里插入图片描述

在这里插入图片描述

在 BestMatchAsset 中使用 HalfTagRange

在进行标签匹配时,目的是确保匹配结果是合理的,并且能够正确计算标签之间的差异。以下是实现这一过程的详细步骤:

1. 计算差异

  • 在进行标签匹配时,我们首先计算两个值之间的差异,假设这两个值分别是 A A A B B B,那么它们之间的差异就是 A − B A - B AB
  • 但为了确保差异计算时考虑周期性,我们需要引入“邻域差异”这个概念。也就是说,计算时不仅仅是简单地减去 A A A B B B 的差值,还要考虑它们之间的周期性差异,特别是在涉及周期性标签时。

2. 处理符号问题

  • 在计算差异时,需要检查两个值的符号(正负)。如果两个值的符号不同,那么我们需要进行符号转换,以确保差异的计算符合周期性要求。
  • 为了实现这个功能,可以使用正弦函数(sine function)来辅助判断。如果符号不同,可以根据需要调整差异,确保计算结果在期望的周期范围内。

3. 符号判断和调整

  • 如果没有现成的正弦函数实现,我们可以通过简单的条件语句(如 if 语句)来判断 A A A B B B 的符号,并根据符号的不同做出适当的调整。
  • 判断符号的方式可以通过比较 A A A B B B 的大小来实现,如果 A A A B B B 符号不同,则需要调整差异值。

4. 最终实现

  • 可以在标签匹配时,通过判断标签的符号来决定是否需要调整差异值。
  • 如果符号相同,直接计算差值。如果符号不同,则需要调整差值,确保结果正确地反映周期性影响。

5. 总结

  • 在进行标签匹配时,我们首先计算两个标签的差异,并根据需要调整周期性差异。
  • 通过符号判断和正弦函数,确保计算结果符合周期性要求。
  • 最终,我们通过条件语句(如 if)来判断符号并调整差异,从而确保匹配过程合理且准确。

在这里插入图片描述

game_intrinsics.h:编写一个 real32 版本的 SignOf

在进行操作时,遇到一些问题,可能会觉得有些麻烦。为了简化工作,可以假设将来会实现一个更好的版本来处理这些问题。经过一定的调整,现在已经实现了所需的功能,可以有效地进行操作。
在这里插入图片描述

game_asset.cpp:继续将 (Half)TagRange 添加到 BestMatchAsset

为了处理某些标签类型的范围,我们需要首先计算标签的半范围,然后根据标签类型的范围调整。具体操作是,我们将标签的范围(TagRange)乘以正弦值,并进行一些运算,来确定最终的范围。

接着,我们通过计算两个数的差异(比如 ab)来得到两种不同的距离值,然后通过取最小值来找到最佳匹配。这意味着我们会选择这两个差异值中较小的那个,以便获得更准确的结果。

此外,为了优化工作,我们可以使用一个新的变量 TagRange 来替代半范围计算,这样可以简化操作。还可以使用 tau(即 2π)替代 π,以便让数学计算更加一致和简洁。总之,通过这些优化操作,最终能得到更好的匹配结果。
在这里插入图片描述

在这里插入图片描述

调试器:进入 BestMatchAsset

首先,进入到相关代码部分,查看资产组的内容。查看当前的匹配逻辑。当前的匹配是对两个值进行比较,初步结果是将 00 进行匹配,显然这两个数的差异为 0,因此结果也会是 0,并且没有特别的变化。

接下来,我们看到程序在进行计算时,计算了两个值之间的差异,同时还会计算另一种可能的差异,即完整的一个周期(tau)。这种计算是正确的,因为对于周期性的匹配问题,考虑整个周期是很重要的。这样处理后,程序能够正确地处理周期性差异,确保匹配的准确性。
在这里插入图片描述

玩游戏并查看我们新功能

接下来,我们对计算出的两个差异值取最小值,这样结果依然是 0,然后继续进行下一步操作。之后,通过加权计算,得到最终的差异值(DIF)。这种方法在标签匹配上看起来更为准确。

通过这种方式,我们能够实现更精确的方向匹配,并且成功地加载正确的周期性资产。同时,系统也没有任何问题,能够顺利处理周期性资产的使用。因此,现在的实现已经能够很好地处理这些需求,确保一切正常运行。

我们差不多完成了

今天的基本完成了,但还剩下半小时的时间。接下来,想要处理一些遗留的问题。一个问题是目前系统没有正确地处理位移,导致一些效果(比如阴影)显示不正常。原因是在代码中,位移数据被硬编码了,但是我们没有真正地回溯这些位移数据。

我打算先看看标准位移的值,看是否可以统一使用一个固定值。通过检查,发现对于所有对象,位移值似乎都是一致的,具体数值是 72182,适用于所有的对象。因此,我想先快速验证一下,看看是否可以为所有对象使用相同的位移值。

我计划先在代码中做一个临时的修改,看看效果如何。为了调试,我将检查加载位图的部分,确认一下是否有任何问题。

game_asset.cpp:添加 *FileName[] 以便遍历位图并设置 TopDownAlign

为了处理位移问题,我计划通过以下方式进行调试。首先,我将所有的文件名放入一个数组中,然后使用一个循环遍历每个文件名。接下来,我将调用 DebugLoadBMP 函数,加载每个位图。

在这个过程中,我决定通过修改代码来测试加载的效果。首先,创建一个 FileName 数组,然后将所有相关的文件名添加到该数组中。接着,使用一个简单的方式加载位图数据。为了确保一切正常,我还会对加载后的数据进行一些调试,确保按预期进行处理。

通过使用 TopDownAlign 来调整这些位图,我能够对这些位图进行检查,确保它们符合需求,正确加载并且显示。最终,这个操作只是一个调试步骤,用来确保没有其他问题干扰,确保数据处理准确无误。

调试器:查看对齐方式

尝试让调试器以正确的方式显示数据,并逐步获取正确的位图数据。这次,数据看起来正常了,显示为 0.51.56,并且这些值是一致的,没有发现任何异常。
接下来,我需要做的就是整理这些数据并返回正确的值,确保它们可以用于接下来的处理。至此,调试过程中的问题已经得到了解决,位图数据也加载成功了。
在这里插入图片描述

game_asset.cpp:将计算出的对齐方式烘焙到英雄位图中

为了确保数据正确,现在需要创建一个 V2 的变量,这个变量用于表示“英雄对齐”(HeroAlign)。这个英雄对齐的值已经通过之前的调试读取出来,分别是 0.50.156 等。
接下来,每次执行添加位图资产(AddBitmapAsset)时,都可以指定这个对齐值(alignment value)。这意味着,只要设置了这个值,就能确保位图的正确对齐方式。
至此,这部分的工作就完成了。

运行游戏并检查正确的对齐方式

现在,所有的资产都已经正确对齐,系统也按照需求运行了。对此,我感到满意。虽然目前不确定是否已经完全完成所有工作,但至少目前的进展是令人满意的,资产系统已经达到了预期目标。

开始讨论资产文件格式

目前,所有的资产匹配工作已经完成,系统运行得也很平稳,感觉一切都在预期之内。因此,可能是时候开始考虑资产文件格式了。虽然这可能有些早,但从当前的进展来看,已经有了合理的基础,可以开始讨论资产文件的格式和它的工作方式。我觉得我们不需要在资产文件格式中使用描述符,而是希望所有的描述信息都来自代码那一侧。因此,感觉现在的状态已经接近完成,所有的工作都顺利进行,应该可以进入下一阶段了。

game.h:向资产流式传输 TODO 中添加两项,并优先处理音频

目前,资产流式加载系统已经基本完成,剩下的工作就是对文件格式的设计。为了完成这部分,首先需要确保能够从磁盘加载资产,并进行内存管理,避免数据无序地加载。接下来,需要检查是否还有其他方面需要优化或改进。如果以后需要进行优化,那时再处理即可,因此现在可以将资产流式加载部分标记为完成。

考虑到接下来要进行文件格式的处理,可能需要在这之前先处理音频加载部分。如果没有音频加载功能的支持,资产加载系统就无法完全运作,因此音频部分是必须解决的问题。接下来的20分钟里,可以先完成音频加载的实现,确保音频功能能正常工作,然后再着手文件格式的设计,以确保文件格式能够同时支持音频和视频内容。

game_asset.cpp:引入 LoadSound

当前的任务是为音频资源的加载系统做准备。之前已经为音效准备好了资产槽和标签,但目前还没有实现加载音频的功能。为了完成这个目标,需要实现一个与加载位图类似的音频加载系统。换句话说,需要创建一个加载音频的流程,功能上与加载位图非常相似。

首先,可以借鉴LoadBitmap的概念,创建一个LoadSound的功能。这个功能将负责加载音频资源,具体来说,当音频文件的ID被传入时,加载任务会开始,然后执行实际的音频加载操作。这个过程会涉及到类似于位图的工作,但操作的对象会是音频文件。

为了实现这个目标,需要实现以下步骤:

  1. 音频信息结构:需要定义一个类似于位图的音频信息结构,其中包括音频的文件名、样本数等数据。
  2. 加载音频文件:创建一个类似DebugLoadBitmap的函数DebugLoadWAV,用于加载音频文件。这将确保音频文件能够被正确加载到内存中,并提供音频所需的信息。
  3. 音频ID和槽管理:为音频资源分配ID并将其加载到适当的资产槽中,这样就能够方便地访问和管理音频资源。

同时,创建一个LoadedSound结构体,这个结构将存储加载的音频信息,例如样本数和音频文件的内存数据。这个结构与位图的LoadedBitmap结构非常相似,主要的区别在于它是针对音频数据的。

在功能上,音频的加载与位图加载非常相似,因此可以考虑将这些功能进行整合,减少冗余代码。如果这两个过程在实现过程中越来越相似,最终可能会将它们合并为一个通用的加载函数,这样可以减少代码量,使系统更加简洁高效。

目前还没有实现具体的LoadedSound结构体,但可以先假设它的存在,并着手开发音频加载的相关功能,之后再根据需要进一步完善音频结构体的实现。总的来说,音频加载系统的基础功能框架已经初步构建,接下来的工作是实现具体的音频加载和管理逻辑。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

引入 DEBUGLoadWAV

当前的任务是实现DEBUGLoadWAV函数,该函数用于加载WAV格式的音频文件,并将其数据解析并存储到适当的结构中,以便后续的音频处理。这个过程与DebugLoadBitmap类似,都是读取文件并解析内容,但WAV文件的解析稍微复杂一些。

实现步骤

  1. 读取整个文件

    • 使用DEBUGReadEntireFile函数将WAV文件的全部内容读取到内存中。
    • 检查读取是否成功,如果失败,则终止加载。
  2. 解析WAV文件头部信息

    • WAV文件的格式比BMP文件稍复杂,但仍然是标准的RIFF格式。
    • 解析WAV文件的RIFF头部,确认文件类型是否正确(应包含RIFF标识和WAVE格式标签)。
    • 解析fmt子块,提取音频格式信息(采样率、通道数、位深度等)。
    • 解析data子块,找到实际的音频数据部分,并记录其大小和位置。
  3. 存储解析结果

    • 提取出音频的关键信息,如:
      • 采样率(Sample Rate)
      • 通道数(Channels)
      • 采样深度(Bit Depth)
      • 总样本数(Sample Count)
      • 音频数据指针(Data Pointer)
    • 创建LoadSound结构体,将这些数据存入其中,以便后续音频播放或处理。
  4. 返回加载结果

    • 确保所有数据正确解析,并返回LoadSound结构的指针,使其可以用于后续的音频处理流程。

额外优化

  • 由于WAV格式支持不同的编码方式(如PCM、ADPCM等),当前可以先实现最常见的PCM(线性脉冲编码调制)格式解析。
  • 可以考虑与位图加载流程进行结构化对比,以便未来优化代码复用,例如抽取通用的“加载资源”方法,减少重复代码。

下一步

  • 继续完善DEBUGLoadWAV函数的实现,并进行基本的测试,确保能够正确解析WAV文件并提取音频数据。
  • 检查是否需要调整LoadSound结构的设计,以适应不同格式的音频资源管理。

在这里插入图片描述

网络:展示 WAVE 规格

WAV 文件的格式基于 RIFF(资源交换文件格式),它由多个数据块(Chunk)组成,每个数据块包含不同的信息,如文件头、音频格式和音频数据。

WAV 文件解析流程

  1. 读取 RIFF 头部

    • WAV 文件以 RIFF 作为文件头标识(4 字节)。
    • 之后是文件大小信息(4 字节),用于指示整个文件的大小。
    • 紧接着是 WAVE 标识(4 字节),表明该文件是 WAV 格式。
  2. 解析 fmt 块(音频格式信息)

    • fmt 块(子块 ID 为 "fmt ")包含音频数据的格式信息:
      • 音频编码格式(Audio Format)(2 字节),常见值:
        • 1 代表 PCM(未压缩的脉冲编码调制)
        • 其他值可能代表 ADPCM 或其他压缩格式
      • 通道数(Number of Channels)(2 字节),如:
        • 1 表示单声道
        • 2 表示立体声
      • 采样率(Sample Rate)(4 字节),如 44100 Hz 表示 44.1kHz。
      • 字节率(Byte Rate)(4 字节),用于计算数据流的速率。
      • 块对齐(Block Align)(2 字节),用于存储每个采样块的大小。
      • 位深度(Bits per Sample)(2 字节),常见值:
        • 8(8 位 PCM)
        • 16(16 位 PCM)
  3. 解析 data 块(音频数据)

    • data 块(子块 ID 为 "data")包含音频的原始 PCM 数据:
      • 数据大小(Data Size)(4 字节),表示音频数据的总字节数。
      • 音频数据(Data),实际的 PCM 采样值,格式取决于 fmt 块中定义的 Bits per Sample

加载 WAV 文件的实现思路

  1. 读取整个文件并确认 RIFF 头是否正确
    • 解析前 12 个字节,检查是否以 "RIFF" 开头,是否包含 "WAVE" 标识。
  2. 查找 fmt 块并解析音频格式信息
    • 提取音频格式、通道数、采样率、位深度等信息。
  3. 查找 data 块并读取音频数据
    • 提取数据大小,并存储音频 PCM 数据。
  4. 将解析的音频数据存入 loaded sound 结构
    • 存储采样率、通道数、样本数及数据指针,以便后续音频播放。

后续优化

  • 目前仅解析 PCM 格式的 WAV 文件,可后续扩展支持 ADPCM 等其他编码格式。
  • 结合已有的 bitmap 资源管理方式,优化代码结构,实现更通用的资源加载机制。
    https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html

黑板:“IFF” 文件格式

块状文件格式(Chunked File Format) 是一种层次化数据编码方式,广泛用于早期计算机文件格式中。例如,IFF(Interchange File Format)Electronic ArtsAmiga 计算机 上引入,是最早采用此方法的格式之一。

块状文件格式的基本结构

  1. 数据按块(Chunk)存储

    • 每个块都有一个 ID(通常是 4 个 ASCII 字符)。
    • 之后是 大小字段,表示该块的数据长度。
    • 紧随其后的是实际的 数据内容
  2. 块的解析方式

    • 读取 ID 确定块的类型。
    • 读取 大小字段,确定数据长度。
    • 如果程序支持该块类型,则解析其数据;否则,跳过该块。
    • 部分块可以包含 子块(Sub-chunk),形成层次结构。

WAV 文件的块结构

WAV 文件采用 RIFF(Resource Interchange File Format),其结构与 IFF 类似:

  • RIFF 块(主块)
    • 包含 WAVE 标识
    • 内部包含多个子块:
      • fmt 块(存储音频格式)
      • data 块(存储 PCM 音频数据)
      • 其他可选块(如 LISTfact 等)

解析 WAV 文件的流程

  1. 读取 RIFF 头部,确认文件类型是否为 WAVE
  2. 遍历文件中的块:
    • 识别 fmt 块,提取音频格式信息(采样率、通道数、位深度等)。
    • 识别 data 块,读取 PCM 数据。
    • 遇到未知块则跳过,确保兼容扩展格式。

优缺点分析

优点
  • 结构清晰,可扩展(可添加新块类型而不影响旧软件)。
  • 易于解析,支持跳过未知数据块。
  • 适用于存储层次化数据。
缺点
  • 额外的 ID大小字段 增加了文件体积。
  • 解析逻辑较复杂,需处理不同块类型。
  • 现代格式(如 JSON、Protocol Buffers)提供更高效的替代方案。

总结

块状文件格式是一种早期的数据存储方法,在 WAV、AVI、3DS 等格式中广泛应用。尽管 结构灵活,但在现代开发中通常使用 更紧凑、高效 的数据格式来代替。

game_asset.cpp:引入 WAVE_header

在解析 WAV 文件时,我们主要关注 文件头(Header)数据块(Chunk) 的基本结构。由于这是调试加载器(Debug Loader),我们不需要支持所有 WAV 格式,只需解析出基本的 PCM 音频数据,以便后续处理资产。

WAV 文件基本结构

WAV 文件使用 RIFF(Resource Interchange File Format) 作为容器格式,其结构包括多个 块(Chunk)

  1. RIFF 头部(文件标识)
  2. fmt 块(音频格式)
  3. data 块(PCM 数据)
  4. 其他可选块(如 LIST, fact 等)

解析 WAV 文件的初始步骤

1. 读取 WAV 头部

  • WAV 文件的前 12 字节 定义为 RIFF 头部
    • 4 字节"RIFF"(ASCII 编码)
    • 4 字节:整个文件的大小(不包括前 8 字节)
    • 4 字节"WAVE"(ASCII 编码)

2. 读取 fmt 块(音频格式信息)

  • 4 字节:块 ID,通常为 "fmt "(包括空格)
  • 4 字节:块大小(通常为 16,表示 PCM 格式)
  • 2 字节:音频编码格式(1 表示 PCM)
  • 2 字节:通道数(1 = 单声道,2 = 立体声)
  • 4 字节:采样率(如 44100 Hz)
  • 4 字节:字节率(= 采样率 × 通道数 × 每个样本的字节数)
  • 2 字节:块对齐(= 通道数 × 每个样本的字节数)
  • 2 字节:每个样本的位数(如 16 表示 16 位 PCM)

3. 读取 data 块(PCM 音频数据)

  • 4 字节:块 ID,通常为 "data"
  • 4 字节:数据大小
  • 接下来的字节:音频 PCM 数据

代码解析 WAV 头部

我们可以用 C 语言的 结构体(struct) 来定义 WAV 文件头:

typedef struct {uint32_t riff_id;   // "RIFF"uint32_t file_size; // 文件总大小 - 8 字节uint32_t wave_id;   // "WAVE"
} WaveHeader;

然后是 fmt 块

typedef struct {uint32_t fmt_id;        // "fmt "uint32_t fmt_size;      // 块大小uint16_t audio_format;  // 编码格式(1 = PCM)uint16_t num_channels;  // 通道数(1 = 单声道,2 = 立体声)uint32_t sample_rate;   // 采样率uint32_t byte_rate;     // 字节率uint16_t block_align;   // 块对齐uint16_t bits_per_sample; // 采样位数
} FmtChunk;

最后是 data 块

typedef struct {uint32_t data_id;   // "data"uint32_t data_size; // PCM 数据大小
} DataChunk;

调试加载 WAV 文件

在调试加载器中,我们可以按照以下步骤解析 WAV 文件:

  1. 读取 WaveHeader,确保是 "RIFF""WAVE" 文件
  2. 查找 "fmt " 块,解析音频格式信息
  3. 查找 "data" 块,提取 PCM 数据
  4. 如果遇到未知块,使用 chunk_size 跳过该块
  5. 将音频数据存入 LoadedSound 结构体,供后续播放

总结

  • WAV 文件采用 块状结构,解析时按 ID + 大小 + 数据 方式读取。
  • 只需解析 “RIFF”, “WAVE”, "fmt ", “data” 块,即可提取 PCM 数据。
  • 解析时可跳过 未知块,确保兼容不同 WAV 变种格式。
  • 由于本次目标是 调试加载器,不需要支持所有 WAV 格式,只需确保能加载调试所需的 WAV 资源。

在这里插入图片描述

引入 WAVE_fmt

在解析 WAV 文件时,fmt 块是关键部分之一,它存储了音频格式信息。我们需要解析其中的多个字段,以确保能够正确读取 PCM 数据。以下是详细解析步骤:


解析 fmt 块(Format Chunk)

fmt 块用于定义音频数据的格式,包括采样率、通道数、比特深度等。其基本结构如下:

偏移量大小 (字节)字段名称说明
04fmt (块 ID)该块的标识符,固定为 "fmt "
44chunk_size该块的大小(不包括前 8 字节)
82wFormatTag音频格式(1 表示 PCM)
102nChannels声道数(1 = 单声道,2 = 立体声)
124nSamplesPerSec采样率(Hz)
164nAvgBytesPerSec平均字节率(= 采样率 × 通道数 × 每样本字节数)
202nBlockAlign块对齐大小(= 通道数 × 每样本字节数)
222wBitsPerSample每个样本的比特数(如 16 表示 16 位 PCM)
242cbSize(可选)扩展数据的大小(仅对非 PCM 格式有效)
26+可变扩展数据例如 dwChannelMaskSubFormat GUID

字段解析

  1. wFormatTag(音频格式)

    • 该字段决定了 WAV 文件的编码方式:
      • 1 = PCM(标准无压缩格式,最常见)
      • 其他值(如 3 = IEEE 浮点格式、6 = A-law、7 = μ-law)不作处理
    • 由于我们只支持 PCM,因此遇到 wFormatTag != 1 的 WAV 文件可以直接忽略。
  2. nChannels(通道数)

    • 1 表示单声道
    • 2 表示立体声
    • 其他通道数(如 5.1、7.1)不在调试加载器的支持范围内。
  3. nSamplesPerSec(采样率)

    • 例如:
      • 44100 Hz(CD 音质)
      • 48000 Hz(专业音频)
      • 96000 Hz(高分辨率音频)
  4. nAvgBytesPerSec(字节率)

    • 计算方式:
      nAvgBytesPerSec = nSamplesPerSec * nChannels * (wBitsPerSample / 8)
      
    • 该字段可以用来验证数据完整性,但不直接影响 PCM 解析。
  5. nBlockAlign(块对齐)

    • 计算方式:
      nBlockAlign = nChannels * (wBitsPerSample / 8)
      
    • 该字段决定了每个采样帧的大小,例如:
      • 16-bit 立体声: nBlockAlign = 2 × (16 / 8) = 4
      • 8-bit 单声道: nBlockAlign = 1 × (8 / 8) = 1
    • 解析 PCM 数据时,每次读取 nBlockAlign 个字节,即可得到完整的采样帧。
  6. wBitsPerSample(采样位数)

    • 常见值:
      • 8 = 每个样本 8 位(无符号)
      • 16 = 每个样本 16 位(有符号)
      • 2432 = 高精度音频
    • 仅支持 16 位 PCM,因为它是最广泛使用的格式。
  7. cbSize(扩展数据大小,可选)

    • 仅当 wFormatTag 不是 1 时才存在,如 cbSize = 22 可能用于 IEEE 浮点格式。
    • 由于我们只支持 PCM,因此可以忽略该字段。
  8. dwChannelMask(通道掩码,可选)

    • 该字段用于多通道音频,如 5.1 或 7.1 声道,但调试加载器无需解析。
  9. SubFormat GUID(可选)

    • 该字段用于非 PCM 格式(如 WAVE_FORMAT_EXTENSIBLE),但对于 PCM,我们可以直接跳过。

WAV 解析结构体

我们可以用 C 语言定义 fmt 块的数据结构:

typedef struct {uint32_t fmt_id;         // "fmt "uint32_t fmt_size;       // fmt 块大小uint16_t wFormatTag;     // 音频格式(1 = PCM)uint16_t nChannels;      // 通道数uint32_t nSamplesPerSec; // 采样率(Hz)uint32_t nAvgBytesPerSec;// 字节率uint16_t nBlockAlign;    // 块对齐uint16_t wBitsPerSample; // 采样位数// 如果需要支持 WAVE_FORMAT_EXTENSIBLE,可在此扩展结构体
} FmtChunk;

解析 WAV 文件的 fmt

在调试加载 WAV 文件时,我们按照以下步骤解析 fmt 块:

  1. 读取 fmt 头部(前 8 字节),确保 ID 为 "fmt "
  2. 读取 fmt_size,确定该块的大小
  3. 解析 PCM 相关字段,如 nChannelsnSamplesPerSecwBitsPerSample
  4. wFormatTag != 1,则跳过该块,不解析
  5. 跳过可能的扩展字段,直到找到 data

总结

  • fmt 块是 WAV 文件解析的核心部分,决定了音频数据格式。
  • 只支持 PCM(wFormatTag = 1),忽略 cbSize 及扩展字段。
  • 关键字段包括 nChannels(通道数)、nSamplesPerSec(采样率)、wBitsPerSample(采样位数)。
  • 解析时需检查 nBlockAlignnAvgBytesPerSec,确保数据正确性。
  • 如果遇到非 PCM 格式或不支持的位深度,直接跳过该文件。

接下来,我们可以继续解析 data 块,以获取实际的 PCM 音频数据。
在这里插入图片描述

引入 WAVE_chunk

在解析 WAV 文件时,我们还需要处理 WAV 文件中的其他数据块(chunk)。WAV 文件采用 分块(chunked)格式,其中每个数据块都包含 块 ID块大小。我们需要解析这些数据块,以正确地读取 WAV 文件的结构。


WAV 文件中的通用块格式

WAV 文件的结构基于 RIFF(Resource Interchange File Format),每个块的基本格式如下:

偏移量大小 (字节)字段名称说明
04ChunkID数据块的 ID(例如 "fmt ""data" 等)
44ChunkSize该块的数据大小(不包括 ChunkIDChunkSize
8ChunkSize数据该块的具体内容

WAV 文件主要包含的块

一个标准的 WAV 文件通常包括以下几个关键块:

1. RIFF 头部

  • ChunkID"RIFF"(4 字节)
  • ChunkSize:文件的总大小(4 字节)
  • Format"WAVE"(4 字节)

该部分定义了整个文件的类型,WAV 文件必须以 "RIFF" 开头,并且其格式必须是 "WAVE"


2. fmt 块(格式块)

  • ChunkID"fmt "(4 字节)
  • ChunkSize:格式块大小(通常为 1618 字节,PCM 格式时为 16
  • 数据内容
    • wFormatTag(音频格式,1 = PCM)
    • nChannels(通道数)
    • nSamplesPerSec(采样率)
    • nAvgBytesPerSec(平均字节率)
    • nBlockAlign(块对齐大小)
    • wBitsPerSample(每个样本的位数)

3. data 块(音频数据块)

  • ChunkID"data"(4 字节)
  • ChunkSize:音频数据的大小(4 字节)
  • 数据内容:实际的 PCM 音频数据

这是 WAV 文件中最重要的数据块,它包含了音频的 PCM 采样数据。我们需要找到 data 块,并解析其中的音频数据。


解析 WAV 文件的通用方法

为了正确解析 WAV 文件,我们需要按照以下步骤处理数据块:

  1. 读取文件头部

    • 确保 ChunkID"RIFF"
    • 读取 ChunkSize(可忽略)。
    • 确保 Format"WAVE"
  2. 遍历文件中的数据块

    • 读取 ChunkIDChunkSize
    • 根据 ChunkID 进行不同的解析:
      • 如果 ChunkID"fmt ",则解析格式信息。
      • 如果 ChunkID"data",则读取音频数据。
      • 如果 ChunkID 是其他未知值,则跳过该块(通过 ChunkSize 跳过相应字节)。
  3. 读取 data 块的数据

    • 解析 PCM 采样数据(支持 8 位或 16 位 PCM)。

示例代码

我们可以用 C 语言实现 WAV 解析的基本逻辑:

typedef struct {uint32_t ChunkID;   // "RIFF"uint32_t ChunkSize; // 文件总大小uint32_t Format;    // "WAVE"
} RIFFHeader;typedef struct {uint32_t ChunkID;   // "fmt "uint32_t ChunkSize; // 块大小uint16_t wFormatTag;uint16_t nChannels;uint32_t nSamplesPerSec;uint32_t nAvgBytesPerSec;uint16_t nBlockAlign;uint16_t wBitsPerSample;
} FmtChunk;typedef struct {uint32_t ChunkID;   // "data"uint32_t ChunkSize; // 音频数据大小
} DataChunk;

然后,我们遍历 WAV 文件的块:

FILE *file = fopen("example.wav", "rb");
RIFFHeader riff;
fread(&riff, sizeof(RIFFHeader), 1, file);// 确保是 WAV 文件
if (riff.ChunkID != 0x46464952 || riff.Format != 0x45564157) {printf("错误:文件不是 WAV 格式!\n");fclose(file);return;
}// 遍历数据块
while (!feof(file)) {uint32_t chunkID;uint32_t chunkSize;fread(&chunkID, sizeof(uint32_t), 1, file);fread(&chunkSize, sizeof(uint32_t), 1, file);if (chunkID == 0x20746D66) {  // "fmt "FmtChunk fmt;fread(&fmt, sizeof(FmtChunk), 1, file);printf("音频格式: %d, 采样率: %d Hz, 通道数: %d\n",fmt.wFormatTag, fmt.nSamplesPerSec, fmt.nChannels);} else if (chunkID == 0x61746164) {  // "data"printf("找到数据块,大小: %d 字节\n", chunkSize);break;  // 读取音频数据} else {fseek(file, chunkSize, SEEK_CUR);  // 跳过未知块}
}fclose(file);

总结

  1. WAV 文件由多个块组成,每个块都有 ChunkIDChunkSize
  2. 关键块包括 RIFF 头部、fmt 格式块、data 音频数据块
  3. 解析 WAV 文件时,我们需要遍历所有块,并根据 ChunkID 进行不同的处理
    • fmt:存储音频格式信息(采样率、位深、通道数)。
    • data:存储 PCM 音频数据。
    • 未知块:跳过处理。
  4. 只支持 PCM (wFormatTag == 1),遇到其他格式可以忽略

这样,我们就可以正确解析 WAV 文件,并提取其中的 PCM 音频数据!
在这里插入图片描述

#define RIFF_CODE

在解析 WAV 文件时,我们需要能够识别文件中的不同数据块(chunk),而这些数据块是通过 4 字节的 ID 进行标识的,例如 "fmt "(格式块)、"WAVE"(数据块)和 "RIFF"(文件头)。由于 C 语言不允许直接用字符串字面量作为 uint32_t 类型的值,因此我们需要一种方法来将 4 个字符转换成一个 32 位整数,以便进行高效的比对。


目标:将 4 字节字符转换为 32 位整数

为了便于 WAV 解析,我们希望能够定义这些块 ID,例如:

enum {WAVE_ChunkID_fmt = RIFF_CODE('f', 'm', 't', ' '),WAVE_ChunkID_RIFF = RIFF_CODE('R', 'I', 'F', 'F'),WAVE_ChunkID_WAVE = RIFF_CODE('W', 'A', 'V', 'E'),
};

然而,C 语言并不支持直接写 'fmt ' 这样的多字符字面量并让它变成 uint32_t 类型,因此我们需要定义一个 来自动转换字符串。


实现思路

由于 WAV 文件使用 小端字节序(Little Endian) 存储数据,我们需要确保字符的存储顺序正确。例如:

  • "RIFF" 在内存中的存储可能是 0x46464952(即 R I F F)。
  • "fmt " 在内存中的存储可能是 0x20746D66(即 f m t )。

为了确保正确的字节顺序,我们可以定义一个 来将 4 个字符转换为 uint32_t 值,确保它们按 小端字节序 排列。


宏实现

我们定义一个宏 RIFF_CODE(),用于将 4 个字符转换为 32 位整数:

#define RIFF_CODE(a, b, c, d) \((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))

这个宏的工作原理如下:

  • a 是最低字节(最右边),不需要位移。
  • b 左移 8 位,占据第二个字节。
  • c 左移 16 位,占据第三个字节。
  • d 左移 24 位,占据最高字节。

使用示例

通过这个宏,我们可以定义 WAV 解析所需的块 ID:

const uint32_t WAVE_CHUNK_ID_FMT  = RIFF_CODE('f', 'm', 't', ' ');
const uint32_t WAVE_CHUNK_ID_RIFF = RIFF_CODE('R', 'I', 'F', 'F');
const uint32_t WAVE_CHUNK_ID_WAVE = RIFF_CODE('W', 'A', 'V', 'E');

这样,我们就可以用这些值与 WAV 文件中的 ChunkID 进行比对,确保正确解析数据。


如何验证字节顺序

由于 小端存储 可能导致字符顺序错误,我们可以通过读取实际 WAV 文件并打印 ChunkID 来验证:

uint32_t chunk_id;
fread(&chunk_id, sizeof(uint32_t), 1, file);
printf("Chunk ID: 0x%X\n", chunk_id);

如果打印出的 Chunk IDRIFF_CODE() 计算出的值不匹配,则可能需要调整 字节顺序(例如更改 RIFF_CODE() 中的移位顺序)。


总结

  1. WAV 文件使用 4 字节的 ChunkID 标识数据块,例如 "fmt ""data""RIFF"
  2. 由于 C 语言不支持直接使用字符串字面量作为 uint32_t 值,我们定义了 RIFF_CODE(),用来将 4 个字符转换成 32 位整数。
  3. RIFF_CODE() 确保字符按小端字节序排列,符合 WAV 文件格式要求。
  4. 通过 RIFF_CODE() 生成的 ChunkID 可以用于比对文件中的数据块,确保 WAV 解析正确

这使得 WAV 解析代码更加清晰易读,并且方便扩展其他块类型的处理!
在这里插入图片描述

解析 WAVE_header 的 ID

在解析 WAV 文件时,我们首先需要解析 WAV 头部(Wave Header)。我们期望 WAV 头部包含 RIFF 块,并且 RIFF 块的 ID 字段应包含 "RIFF",同时 WAVE ID 也应该存在。如果这两个条件都满足,那么文件的基本结构就是符合预期的,我们可以继续解析其中的具体数据块(chunks)。


Interchange(立体交叉道, 互换 vt. 交换, 互换 vi. 交替发生)

解析 WAV 头部

  1. WAV 头部的主要结构

    • RIFF 块(Resource Interchange File Format):
      • 4 字节的 Chunk ID(必须是 "RIFF")。
      • 4 字节的 文件大小
      • 4 字节的 格式(必须是 "WAVE")。
    • 只有在 Chunk ID"RIFF"Format"WAVE" 时,我们才会继续解析文件。
  2. 数据结构示例

    typedef struct {uint32_t ChunkID;   // 必须是 "RIFF"uint32_t ChunkSize; // 整个文件大小 - 8uint32_t Format;    // 必须是 "WAVE"
    } WaveHeader;
    

使用断言(assert)验证 WAV 文件

由于这只是用于调试的 WAV 解析器,而不是最终的资产加载器,因此我们可以使用 assert 来验证 WAV 文件是否符合基本格式:

   WaveHeader* header = (WaveHeader*)fileData;// 断言检查是否是有效的 WAV 头部Assert(Header->RIFFID == WAVE_ChunkID_RIFF);Assert(Header->WAVEID == WAVE_ChunkID_WAVE);

这里使用 RIFF_CODE() 宏(之前定义过)将 4 字节字符串转换为 uint32_t,确保 ChunkIDFormat"RIFF""WAVE",否则程序会在调试阶段崩溃,提醒开发者该文件格式错误。

到这里,稍微展望一下未来

目前正在进行 WAV 文件加载的编写,预计明天能够实现完整的 WAV 文件解析。这不仅有助于完成资产加载的最后阶段,同时也能推进音频部分的开发。这样一来,整体的开发任务会减少,使进度更加顺利。

开发目标

  • 计划在第 200 天左右开始编写正式的游戏逻辑代码,因此希望在此之前完成所有核心引擎工作。
  • 目前正在清理待办事项,希望尽快完成所有和游戏无关的引擎开发工作,使后续能够专注于游戏代码的编写。

当前进度

  • 渲染部分已经基本完成,但仍有两个主要任务需要解决:

    1. 调试工具(Debug Code):这部分相对简单,应该不会花费太多时间。
    2. 字体渲染(Fonts):这需要较多的工作量,仍然是一个待办事项。
  • WAV 文件解析即将完成:

    • 目前已经建立了对 RIFF 结构的解析框架。
    • 计划明天完成具体的数据加载,以便 WAV 文件能够被正确解析和使用。
    • 这一部分完成后,音频子系统的主要功能也将基本完善。

后续规划

  • 继续推进 资产加载和音频系统,确保它们可以稳定运行。
  • 完成调试工具和字体渲染,进一步减少待办事项。
  • 优化代码,减少引擎层面的开发工作,以便在第 200 天左右切换到游戏逻辑的开发。

目前整体进度良好,正在有条不紊地完成各项核心引擎任务,为正式的游戏开发做最后的准备。

对于获取角色精灵,能不能只做以下操作? int sprite_to_load = (angle + (180 / sprites)) / (360 / sprites) % sprites

当前讨论的是角色精灵(sprite)的加载和角度映射问题。有人提出了一种计算方式:

int sprite_to_load = (angle + 180) / (sprites / 360) % sprites;

但这种方式存在多个问题,不能直接适用于当前的资产系统。

主要问题

  1. 缺乏角度分布的信息

    • 当前的资产系统并没有存储精灵在特定角度上的数量信息。
    • 角度的分布可能并不均匀,例如某些角度的精灵较多,而另一些角度的精灵较少。
    • 例如,一个 Boss 角色 可能大部分时间是面向前方的,因此前方视角的精灵较多,而背后视角的精灵较少。
  2. 现有系统无需量化(Quantization)

    • 当前的实现方式已经能够正确处理不同角度的精灵加载,而不需要进行额外的量化计算。
    • 直接量化角度可能会导致问题,例如 某些角度下根本没有可用的精灵,导致错误的映射或加载失败。

结论

  • 直接使用这种计算方式不可行,原因是它假设了 精灵角度均匀分布,但实际情况可能并非如此。
  • 现有系统已经能够正确加载精灵,并适应 不均匀的角度分布,因此不需要进行额外的量化计算。

我有一个关于音频资产结构的联合问题。它需要 SoundLoopable,或者 LoopStart / LoopEnd 吗?(LoopEnd == -1 表示一次性播放的声音)

关于音频资产结构体的问题,是否需要添加如 循环起始点循环结束点 以及将 循环结束点设置为 -1 以表示单次播放的声音,目前还不确定。虽然这种功能可能会需要,但由于音频系统还没有完全构建好,因此目前还不清楚具体需求。

大概的做法是,先开发混音器(mixer),然后根据混音器的需求,回过头来调整音频资产系统,加入所需的功能和字段。

实现其他音频格式如 MP3 的计划?

关于实现其他音频格式,如 MP3,无法实现,因为 MP3 格式有专利,受到法律保护,不允许实现。即使在 2016 年之后专利可能会过期,但在当前仍然不允许实施 MP3 解码。美国的专利系统特别复杂和限制,尤其是软件专利,给开发带来很多麻烦。

因此,虽然理论上可以讲解如何实现 MP3 解码,但实际的代码实现是违法的。如果源代码被发布并包含 MP3 播放功能,那将是违法的行为。虽然通过直播或说明可能不违法,但还是不鼓励这样做。对于其他格式,如 FLAC,虽然可以用于压缩,但由于其压缩率较低(通常不到两倍),对空间的节省效果并不显著,使用起来的意义不大。对于真正需要压缩音频的场景,OGG Vorbis 可能是一个更合理的选择,但这类格式实现起来复杂,且消耗的时间和资源较多,因此不太可能在当前实现。

MP3 到底怎么被专利化的?

MP3 格式受到了多个专利的保护,可以在互联网上找到有关 MP3 专利的详细信息,包括专利的持有者和专利过期的时间。根据这些资料,MP3 的专利并非在全球范围内统一到期。在一些地区,比如美国,MP3 专利仍然有效,而在其他地方,可能早在 2012 年就已经过期。因此,在某些地区,可能是合法的来使用 MP3 格式的相关代码,但在美国,使用 MP3 解码或编码相关的技术仍然是违法的。

根据不同的专利信息,有些专利可能在 2015 年或 2017 年才会过期,具体取决于不同的专利和地区的法律规定。因此,尽管在一些地区 MP3 可能不再受专利保护,但在美国,专利依然存在并限制相关技术的使用。
在这里插入图片描述

是的,但为什么有人会专利文件扩展名呢?

MP3 文件的专利并不是针对文件扩展名,而是针对创建音频、以及编码和解码音频所使用的技术。因此,仅仅解析 MP3 文件的内容就可能会违反这些专利,因为专利保护的就是这些解析技术。虽然这种做法令人难以理解,但遗憾的是,现行的法律要求遵守这些专利,因此在技术上,我们不能解析 MP3 文件。

FLAC 是 WAV 的 50% 大小,所以它对无损压缩非常有用

FLAC(无损压缩)并不会像很多人认为的那样将文件压缩到原始 WAV 文件的 50% 大小,实际上最好的情况下,压缩率大约是 60%。在许多情况下,FLAC 的压缩效果要比这个更差,甚至接近于无压缩。因此,若只是为了节省一些存储空间,使用 FLAC 并不值得,因为它无法提供显著的节省。

假设一个游戏的总大小为 2GB,其中音频占据 500MB,如果通过 FLAC 压缩能够节省大约 25%的空间,那么最终仅节省 100MB。相对于整个游戏文件大小,这种节省是微不足道的。再加上为了实现这种压缩,游戏代码将变得更加复杂,这样的压缩就没有太大的意义。

如果真需要大幅压缩,应该考虑使用像 OGG Vorbis 这样的格式,它能够提供 8:1 或更高的压缩比,这样的压缩效果才更有实际意义。因此,如果目标是有效地减小音频文件的体积,选择 FLAC 并不会带来太多好处,反而可能增加不必要的复杂性。
https://xiph.org/flac/
在这里插入图片描述

在这里插入图片描述

https://z-issue.com/wp/flac-compression-level-comparison/
在这里插入图片描述

所以这就是 RAD 为什么不专利他们的东西的原因吗?

某些技术并未申请专利,是因为认为这类技术的专利制度非常愚蠢,因此选择不进行专利申请。


http://www.ppmy.cn/news/1577141.html

相关文章

【基于手势识别的音量控制系统】

基于手势识别的音量控制系统 github 项目效果 这是一个结合了计算机视觉和系统控制的实用项目&#xff0c;通过识别手势来实现音量的无接触控制&#xff0c;同时考虑到了用户隐私&#xff0c;加入了实时人脸遮罩功能。 核心功能实现 1. 手势识别与音量映射 系统使用 Media…

扫描纸质文件转pdf---少页数+手机+电脑协作

针对手机上扫描软件扫描文件转pdf要收费的问题&#xff0c;提供一种在页数较少时的免费替代方案 。 实现方法&#xff1a;手机软件的免费功能将文件扫描并保存为图片电脑端在word中将图片拼成文档word转pdf 1.借助于“扫描全能王”APP可以免费扫描文件为图片的功能&#xff0…

阿里万相,正式开源

大家好&#xff0c;我是小悟。 阿里万相正式开源啦。这就像是AI界突然开启了一扇通往宝藏的大门&#xff0c;而且还是免费向所有人敞开的那种。 你想想看&#xff0c;在这个科技飞速发展的时代&#xff0c;AI就像是拥有神奇魔法的魔法师&#xff0c;不断地给我们带来各种意想…

【问题解决】Jenkins使用File的exists()方法判断文件存在,一直提示不存在的问题

小剧场 最近为了给项目组提供一个能给Java程序替换前端、后端的增量的流水线&#xff0c;继续写上了声明式流水线。 替换增量是根据JSON配置文件去增量目录里去取再替换到对应位置的&#xff0c;替换前需要判断增量文件是否存在。 判断文件是否存在&#xff1f;作为一个老Ja…

nlp第十节——LLM相关

一、模型蒸馏技术 本质上是从一个大模型蒸馏出小模型&#xff0c;从小模型训练出来的概率分布&#xff08;如自回归模型预测下一个字的概率分布&#xff09;分别与大模型预测的概率分布和ground label求loss。与大模型预测的概率分布用KL散度求loss&#xff0c;与ground label用…

蓝桥杯算法——铠甲合体

问题描述 暗影大帝又开始搞事情了&#xff01;这次他派出了 MM 个战斗力爆表的暗影护法&#xff0c;准备一举摧毁 ERP 研究院&#xff01;MM 个暗影护法的战斗力可分别用 B1,⋯,BMB1​,⋯,BM​ 表示。 ERP 研究院紧急召唤了 NN 位铠甲勇士前来迎战&#xff01;每位铠甲勇士都…

如何在React中正确处理异步操作?

文章目录 1. 引言2. 异步操作的典型场景与潜在问题2.1 典型场景2.2 常见问题 3. 基本原则与最佳实践3.1 封装异步逻辑3.2 使用React Hooks管理副作用3.3 管理加载、错误与数据状态3.4 防止内存泄漏3.5 避免竞态条件 4. 在React中处理异步操作的方法4.1 使用 useEffect 处理异步…

Webpack分包与合包深度解析

Webpack分包与合包深度解析 引言&#xff1a;现代前端工程的模块化困境 在单页面应用&#xff08;SPA&#xff09;复杂度日益增长的今天&#xff0c;一个未经优化的Webpack构建产物可能面临&#xff1a; 首屏加载缓慢&#xff08;超过3秒白屏&#xff09;公共模块重复打包&am…