堆栈:https://gitee.com/mrxiao_com/2d_game_3
回顾昨天的内容
这个节目展示了我们怎样从零开始制作一款完整的游戏。我们不使用任何游戏引擎或库,而是重新开始创建一款游戏,整个开发过程都会呈现给各人。你将能够看到每一行代码的编写,相识游戏是怎样从引擎到游戏玩法再到其他各个方面一一实现的。
现在,我们正在举行资产体系的改进工作。之前我们有一些初步的代码,能够从磁盘加载资产,比如从BMP和WAV文件加载资源。但是现在,我们对资产的管理做出了更为集中的调解。我们现在已经有了一种方法来构建本身的资产包文件,这些文件可以轻松地传输、存储、交换或举行其他处理。
这些资产包文件包含了运行游戏所需的所有数据,究竟上,以致可以有多个资产包,它们会组合在一起,包含不同的资源。每个资产包文件不但包含所有的图像数据(如位图数据),还包罗音频数据(如WAV文件),以及所有其他必须的元数据,用来描述这些资源的功能和用途。
如许,我们通过这种方式将所有的资源打包成单一的文件或多个文件,并能够方便地管理和传输它们,从而大大简化了游戏资源的管理和加载过程。
今天的筹划
我们已经完成了大部分工作,接下来的任务是将资产流体系构建在现有的底子上,使得它不再直接访问BMP和WAV文件,而是通过我们已经创建的资产包文件来加载资源。我们之前已经完成了对体系的初步移植,但其时是一次性加载整个文件,现在的目标是通过异步方式从操纵体系中获取数据,而不是在启动时就加载所有的资源文件。现在我们正在清理平台和AI部分的代码,以便能够更高效地加载数据。
今天的任务就是完成这一部分的工作。
接下来,我们回顾一下我们上次的进展。上次我们根本上已经完成了大部分的移植工作,剩下的只是举行末了的调解和完满。
现在资源的元数据已经被加载,因此 LoadBitmap 和 LoadSound 的工作根本上就只剩下获取数据了
之前,加载不同范例的资源(如位图和音频文件)时,体系必要分别处理,因为我们必要从这些文件中获取数据,比如位图的尺寸或音频文件的样本数。这个区分是因为必须知道详细的资源数据格式。然而,随着我们将资源整合到资产文件中,这些细节已经在更高层级处理好了,所有必要的元数据已经被预先加载,因此现在加载资源时,我们已经完全知道数据的布局,不再必要举行额外的处理。
因此,我们现在的做法是简化这个过程,同一通过一段代码来加载资源,无论是位图数据还是音频数据,都可以通过相同的异步加载方式来处理。我们只必要提供数据的存储位置和内存地点,体系就能根据这些信息将数据加载到内存中,其他的就不必要再做了。
已经完成的部分是位图的加载,我们已经将之前的位图加载代码简化为同一的 LoadAssetWork 函数,如许就不再必要专门为位图加载举行额外的处理。接下来还必要完成音频资源的部分,进一步清理并简化加载逻辑,确保所有资源都能通过同一的方式加载。
开始移除 LoadSound 函数
接下来,目标是将音频的加载过程与位图加载过程同一,使得音频的加载方式与位图相同,从而简化和同一资源的加载流程。之前,音频的加载使用了一个 load_bitmap_work 的函数,但我们想要去掉这个函数,并改为直接使用类似位图加载的 load asset 函数,简化整个过程。
在 load_asset_work 中,之前告急做的事变是设置音频的样本数和通道数等,这些信息是从音频的资产数据(hha asset)中获取的。固然现在的目标是制止在每次加载时都必要设置这些信息,但现在看来,还是必要在某些情况下从资产数据中复制这些信息到音频资源中。固然有些人大概会以为不必要复制这些信息,而是可以直接使用它们,但现在复制这些信息似乎并不会带来太大题目,以是暂时保存这个过程。
接下来,音频加载过程必要做的事与位图的加载类似。详细来说,load_sound_work 必要改成 load_asset_work,使得音频的加载也走同一的加载流程。与位图加载相同,在加载音频资源时,文件句柄初始化为零,因为文件处理部分还没有完成,之后会实现文件操纵相关的代码。
此外,音频的内存目标地点会指向音频样本数据,而且可以在必要时引入一个专门的音频内存结构来存储这些数据。最终,音频资源的状态也会标记为已加载,就像位图一样。
末了,音频资源的槽位必要和正确的音频数据指针关联起来,确保加载后能够正确地使用音频数据。此外,GetSound 的函数已经做了修改,确保它在访问音频资源时能够检查是否已经加载,假如已经加载,则直接返回资源。
总体来说,目的是将音频的加载方式与位图的加载方式同一,简化代码并提高资源加载的服从。
(插曲)确保在检查资源是否加载和访问资源之间强制保证读取的次序
在思考当前代码时,意识到必要轻微调解一下逻辑。最初使用了三元运算符来处理条件判定,如许做固然轻便,但也存在潜在的题目。题目在于,编译器大概会将某些读取操纵重新排序,这大概会导致数据读取的次序错误。为相识决这个题目,必要确保在读取数据之前,之前的读取操纵已经完成,以制止出现不正确的数据状态。
详细来说,要确保在举行某个读取操纵之前,前面的读取操纵已经完成。这是因为假如读取次序不正确,大概会导致数据不同等或错误的结果。固然现在在像Intel芯片上,这种题目的大概性较低,但思量到将来大概会支持不同的硬件平台,比如树莓派或Android设备,这种次序题目仍然必要特别注意,尤其是在ARM架构的设备上。
为了保证代码的正确性和健壮性,决定在读取之前加上检查,确保前面的读取操纵已经完成。固然如许做大概不会带来明显的性能差别,因为编译器可以优化掉不必要的操纵,但这种预防措施可以确保在不同平台和不同架构下都能够正常运行。因此,还是决定保持这个更安全的做法。
(插曲)实现 CompletePreviousReadsBeforeFutureReads(确保之前的读取完成才执行新的读取)
现在,我将检查一下是否已经实现了必要的读取屏障,因为现在似乎还没有实现。根本上,我们必要确保在举行未来读取之前,所有之前的读取操纵都已完成。如许做实际上相当于在代码中插入一个读取屏障。然而,我并不确定在特定平台(如LLVM)中,怎样实现这个读取屏障。
因此,我想做的就是插入一个内存屏障,但仍不确定LLVM是否有专门针对读取的屏障功能。通常,内存屏障会确保某些操纵的次序,防止编译器在处理时举行不正确的重排,尤其是在多核处理器或复杂的架构中。
我决定暂时插入一个假设性的读取屏障,并标注出对于某些平台(如LLVM),是否支持特定的读取屏障功能。这将取决于编译器的实现。假如有更认识LLVM的开发者,他们可以告诉我们是否已添加这种可以专门指定读取屏障而非写入屏障的功能。
固然我不确定是否必要这个屏障,但我以为为了确保代码的安全性,最好还是写出明白的意图,添加得当的屏障。这对于确保编译器不会举行不必要的重排非常告急,固然现在看起来这种重排的大概性较低,但是预防总是更稳妥的。
回到移除 LoadSound 函数的工作
我们正在处理音频加载部分,并盼望使其与图像加载的处理方式同等。起首,我们要做的是提取音频信息,并对其举行处理,类似于处理位图数据的方式。为了保证加载过程的结构同等性,我们决定去除不再必要的旧代码,并将新代码结构化。
在此过程中,音频加载部分必要举行一系列初始化操纵。起首,要为音频数据分配内存空间。为了做到这一点,我们必要计算出加载音频所需的内存大小。这个计算方式与位图加载中的方式相似:我们必要用通道数乘以每个通道的样本数,再乘以每个样本的大小,得到音频数据的总内存需求。
接下来,我们必要为音频数据分配内存。就像加载位图时一样,我们会从内存池中申请合适大小的内存空间来存储音频数据。
一旦内存分配完成,我们就开始将音频数据加载到内存中,确保音频的各个通道在加载后正确地指向相应的内存位置。在音频数据加载时,我们会遍历每个通道,并确保每个通道的样本指针都正确地指向加载的数据。
此外,我们还必要做一些初始化工作。音频的“样本计数”和“通道计数”等参数必要从音频数据中提取出来,而且确保这些参数在加载后能够正确地反映音频的结构和格式。加载完成后,我们会对音频数据举行后续处理,确保它能够在游戏中正确播放。
总的来说,音频加载的过程几乎与图像加载过程相同,告急的区别在于数据的结构不同。通过这种方式,我们确保了代码的同等性和模块化,使得后续的维护和修改变得更加轻便高效。
将资源数据复制到内存中的合适位置
我们现在正在处理音频数据的加载部分,而且我们意识到可以直接将音频数据从资产文件中复制到内存中的指定位置,而不必要通过复杂的分配或移动操纵来实现这一过程。这将使我们的音频加载流程更加轻便高效,因为数据已经在资产文件中按预定的格式存储,我们只必要简单地将其复制到合适的内存地点即可。
在这一过程中,我们发现我们之前已经创建了一个用于清零内存的函数(类似 memzero 或 zero_memory 之类的功能),但我们似乎并没有一个专门的内存复制函数(类似 memcpy 的功能)。这意味着我们当前的工具链中并没有一个标准化的方式来将数据从一块内存地区复制到另一块内存地区。
因此,假如我们想要将音频数据直接复制到内存中,我们必要创建一个新的内存复制函数。这将类似于标准库中的 memcpy 函数,其告急功能是将一块内存中的内容复制到另一块内存中。其根本逻辑如下:
- 确定源地点和目标地点:我们必要确定命据在资产文件中的起始地点(源地点),以及数据在内存中被分配的目标地点。
- 计算数据大小:我们已经提前计算过音频数据的总内存大小,因此我们知道必要复制的字节数。
- 举行内存拷贝:将源地点中的数据逐字节复制到目标地点。
- 确保数据正确对齐:固然大多数音频数据不必要严格的内存对齐,但在某些架构(如 ARM 设备)中,确保内存地点对齐大概会提高访问服从,因此假如我们未来必要支持这些设备,思量内存对齐是必要的。
只管现在我们没有这个复制函数,但我们完全可以基于我们之前的 memzero 函数快速创建一个 memcopy 函数。这将允许我们直接将音频数据块复制到内存中,而不必要通过复杂的中心转换或处理。
此外,将数据直接复制到内存中的优势还包罗:
- 制止冗余操纵:不必要在加载音频数据时举行不必要的处理,减少了 CPU 的计算负担。
- 保持数据格式同等:直接复制确保了数据格式不会在加载过程中发生不测变化。
- 加速加载速度:数据复制比剖析或重构更快,因此这种方式可以显著减少音频加载时间。
因此,下一步我们要做的就是编写一个类似 memcopy 的内存复制函数,然后将其应用到音频加载过程中,使音频数据直接从资产文件复制到内存中,最终提高加载服从和代码轻便性。
编写通用的内存拷贝函数
我们现在决定编写一个简单的内存复制函数,用于将数据从一块内存地区复制到另一块内存地区,类似于标准库中的 memcpy 功能。这个函数告急是为了方便我们在加载音频数据时,将音频样本数据直接从资产文件中的内存地区复制到我们分配的音频内存地区中,从而实现数据的直接加载,制止额外的处理步调。
我们在实现这个函数之前,已经意识到这个函数并不会被广泛使用,因为在实际开发中,假如我们真的关心内存复制的性能,我们通常会基于更详细的情况对内存复制举行优化。比如:
- 假如我们清楚数据的内存布局,我们大概会举行内存对齐优化,比如按照 128 位或更大的数据块举行复制;
- 假如我们知道目标平台的特性,比如支持 SIMD 指令或特定缓存优化,我们大概会接纳更高效的内存复制方式;
- 假如数据具有固定格式或结构,比如音频数据是按样本排布的,我们大概直接通过批量传输大概 DMA 传输等方式举行数据移动,而不是单纯的字节复制。
但由于我们现在的需求非常简单,仅仅是将音频数据从资产文件加载到内存中,因此我们决定编写一个最简单、最底子的内存复制函数,即:
- 接收一个内存大小参数,表示必要复制的字节数;
- 接收一个源地点,表示源内存块的起始地点;
- 接收一个目标地点,表示目标内存块的起始地点;
- 循环遍历指定的字节数,将源地点的数据逐字节复制到目标地点。
内存复制的详细实现
我们起首定义一个 memory_index,用于跟踪必要复制的总字节数。然后我们使用两个 uint8_t 范例的指针,将源地点和目标地点都转换为字节指针(byte pointer),如许我们就可以逐字节地举行内存复制,而不必要思量数据结构或范例对齐等题目。
在循环中:
- 每次将源地点中的一个字节赋值到目标地点中;
- 然后将源地点和目标地点的指针都递增;
- 重复这个过程,直到所有数据都被复制完毕。
为什么我们不做优化
我们也明白指出,这个函数是一个极为底子的内存复制函数,因此它不会举行任何情势的性能优化:
- 没有内存对齐优化:我们没有检查内存地点是否对齐,因此它不会使用 CPU 提供的对齐访问优势;
- 没有批量复制:我们没有使用 memmove 大概 SIMD 指令举行块复制,因此性能较低;
- 没有并行优化:我们没有思量使用多线程大概 DMA 控制器举行数据传输,因此服从较低。
但我们也以为这是可担当的,因为:
- 当前我们只是简单地加载音频数据,不涉及任何性能瓶颈;
- 数据复制的时间几乎不会成为瓶颈,加载过程的焦点瓶颈通常是磁盘 IO 或文件体系操纵;
- 假如未来必要针对某些平台举行优化,我们完全可以替换掉这个底子的内存复制函数,直接编写更高效的版本。
为什么要转换指针
我们将源地点和目标地点转换成 uint8_t* 范例的字节指针,这是因为:
- 在内存复制过程中,我们关注的是逐字节复制,而不是以特定命据范例为单元举行复制;
- 假如直接使用 void* 范例,我们无法直接访问内存;
- 假如使用 uint8_t*,我们就能保证无论数据范例是什么,都可以正确复制每一个字节的数据。
最终的代码结构
最终的内存复制函数结构非常简单,大致如下:
- inline void Copy(memory_index Size, void *SourceInit, void *DestInit) {
- uint8 *Source = (uint8 *)SourceInit;
- uint8 *Dest = (uint8 *)DestInit;
- while (Size--) {
- *Dest++ = *Source++;
- }
- }
复制代码 这个函数的焦点逻辑就是将两个指针分别向前推进,将源地点的数据复制到目标地点中,直到指定的大小被完全复制。
使用场景
我们将会在加载音频数据时使用这个 Copy 函数。当我们从资产文件加载音频数据时,我们会:
- 确定音频数据在文件中的偏移位置;
- 分配一块内存,专门存储该音频数据;
- 使用 Copy 将数据直接从文件内存中复制到分配的音频内存中;
- 完成音频数据的加载。
这种方式简单直接,制止了复杂的解码或转换过程,提高了数据加载的速度和轻便性。
设置音频通道的采样指针
我们现在开始处理音频加载过程中通道数据的分配和对齐,目的是确保每个通道的音频样本都能正确地指向内存中相应的数据位置,而且在内存中一连排布。详细的工作流程如下:
1. 遍历所有通道并设置采样指针
我们起首必要遍历音频文件中的所有通道,将每个通道的数据正确地指向内存中相应的采样数据位置。因此,我们编写了一个循环,循环次数为通道数 (ChannelCount),在循环中完成以下工作:
- 为每个通道分配采样指针:通过 Sound->Samples[ChannelIndex] = SoundAt; 将当前通道的样本数据指向内存的当前位置;
- 调解内存指针位置:每次处理完一个通道的数据后,将内存指针推进到下一个通道数据的位置,以确保每个通道的数据不会相互重叠。
在这里我们接纳的逻辑是:
- int16 *SoundAt = (int16 *)Memory;
- for (uint32 ChannelIndex = 0; ChannelIndex < Sound->ChannelCount; ++ChannelIndex) {
- Sound->Samples[ChannelIndex] = SoundAt;
- SoundAt += ChannelSize;
- }
复制代码 此中:
- Sound->Samples[ChannelIndex] = SoundAt 表示当前通道的样本数据;
- SoundAt 表示当前内存指针,用于记录数据填充到哪一块内存;
- 每次循环之后,将内存指针 SoundAt 增加一个通道的数据大小 (ChannelSize),确保数据一连分布。
2. 计算每个通道的数据大小
我们必要提前计算每个通道的数据大小 (ChannelSize),这个数据大小是:
- ChannelSize = SampleCount * sizeof(int16_t)
复制代码 此中:
- SampleCount 是音频的总采样数;
- sizeof(int16_t) 是每个采样点的数据大小,假设这里使用的是 16 位采样格式。
计算完成之后,每次复制完一个通道的数据后,将内存指针推进 ChannelSize 大小,如许下一个通道的数据就能正确分列在内存中。
3. 优化内存使用
我们意识到在这里我们并不必要显式存储内存指针 (sound_memory) 的初始位置,因为它仅用于在循环中分配内存,并不会在其他地方使用。因此,我们决定将:
直接简化为:
我们不必要存储内存指针的初始地点,只必要确保:
- 在循环中正确递增内存指针;
- 保证数据一连存储;
- 在循环竣过后,所有通道的数据都已经正确分列。
这也意味着我们将内存指针仅用于内存分配而不做其他用途。
4. 完全去除冗余指针
由于我们最初有一个 Memory 变量,用于记录分配的内存地点,但现在我们发现该指针并没有实际用途:
- 它不会在函数外部使用;
- 它仅在分配内存时被使用;
- 分配完内存后,我们只关心通道数据是否正确指向该内存,而不关心初始指针地点。
因此,我们直接将分配内存的函数改为:
- void *Memory = PushSize(&Assets->AssetArena, MemorySize);
复制代码 这里:
- PushSize 是分配内存的函数;
- MemorySize 是我们计算的总内存大小;
- 我们不再保存 Memory 的起始地点,而是直接将数据指向 Sound->Samples。
5. 内存指针推进的焦点逻辑
在循环中,我们通过:
- Sound->Samples[ChannelIndex] = SoundAt;
- SoundAt += ChannelSize;
复制代码 完成了:
- 将当前通道的数据起始地点指向 SoundAt;
- 将 SoundAt 指针向前推进 ChannelSize,确保下一次循环的数据存储在正确位置;
- 制止任何冗余的指针存储或额外的变量。
如许我们确保:
- 内存地点一连:所有音频通道的数据按次序一连分列;
- 内存指针主动推进:无需手动计算地点,只必要增加 ChannelSize;
- 无冗余内存使用:我们只使用内存中的实际数据部分,不保存额外内存指针。
6. 为什么要使用一连内存
我们之以是接纳一连内存的设计,而不是为每个通道分配独立内存块,是因为:
- 更符合音频引擎需求:音频数据通常必要一连存储,方便播放时直接读取;
- 简化内存管理:制止为每个通道分配不同内存块,减少内存碎片;
- 提高加载速度:加载时直接将数据填充到一块一连内存中,制止内存重新分配和对齐操纵。
7. 内存指针转换的意义
我们将 sound_memory 转换为 void* 的原因在于:
- 制止范例依赖:在内存复制过程中,我们只关心内存地点,而不关心数据范例;
- 机动性更强:假如未来必要支持不同数据格式(如 float 或 double),我们无需修改内存指针定义;
- 符合内存分配规范:标准内存分配函数(如 malloc 或 PushSize)通常返回 void*,我们保持同等。
8. 为什么我们不保存内存起始地点
我们最终决定不保存内存起始地点 (void* Memory),原因如下:
- 我们只关心通道数据,而不是内存块的起始地点;
- 内存地点不会被重用,因此不必要保存;
- 节省内存使用,制止存储无关信息。
9. 为什么不思量内存对齐
我们没有举行内存对齐(alignment)的原因:
- 音频数据本身就是一连的,没有跨通道的结构体或数据;
- 内存分配直接使用 PushSize,该函数本身通常具备肯定的对齐特性;
- 采样数据是 16 位(int16_t),而大多数体系默认内存对齐至少为 16 位,因此不会产生未对齐题目。
假如未来必要优化,我们可以:
- 将 PushSize 改成更高级的内存对齐分配器;
- 在复制数据时使用 SIMD 指令提高传输速度;
- 针对不同平台使用内存对齐技术。
触发段错误
else 的位置不对
声音出题目 分配的地点不对
测试刚刚的修改,发现存在奇怪的点击声Bug,大概和今天的修改无关,稍后再调试
我们现在已经完成了音频加载功能的告急部分,而且确保了音频数据在内存中是一连存储的,各个通道的数据指针也都正确指向了内存中的相应位置。然而,在测试音频播放结果时,我们发现音频中存在一些“点击声”大概“断断续续”的杂音,这表明我们的音频加载大概播放流程中大概存在一些题目。接下来我们必要针对该题目举行分析和排查。
1. 确认当前音频加载功能是否正常
起首,我们快速确认了加载音频的焦点流程,确保以下几件事变没有题目:
- 内存分配:使用 PushSize 分配的内存大小是正确的;
- 通道数据指针:通过 loaded_sound->samples[channel_index] 将每个通道的数据指针正确指向内存中的相应位置;
- 内存地点推进:在加载过程中,内存地点通过 sound_memory += channel_size 推进,确保每个通道数据不会相互重叠。
我们再次检查了一遍这些内容,发现内存分配和数据指针的设置是完全正确的,没有发现任何题目。
2. 识别音频播放中的“点击声”
在播放过程中,我们注意到音频中存在一种轻微的“点击声”,体现为:
- 在某些音频片段,听起来像是短促的断裂声或噪音;
- 这种“点击声”并不一连,但在播放过程中会偶尔出现;
- 感觉像是音频样本数据在某些特定时刻没有正确对齐,大概数据传输中断导致的。
这个题目让我们意识到大概有两种情况导致了这种征象:
- 数据加载过程中出错:某些音频样本数据未正确写入内存,导致音频数据中存在错误的波形;
- 音频混音器中存在题目:音频数据在传输给混音器时未正确处理,导致播放中断或音频抖动。
3. 快速排查加载阶段的潜在题目
为了确保加载过程没有题目,我们重点检查了以下内容:
3.1. 检查样本数是否正确
我们起首检查了加载过程中记录的样本数 (sample_count):
- uint32_t sample_count = asset_file_header->sample_count;
复制代码 在加载过程中,我们确保了:
- 每个通道的数据大小是 sample_count * sizeof(int16_t);
- 总内存大小是 channel_count * sample_count * sizeof(int16_t);
- 内存地点在填充时正确指向了 loaded_sound->samples[channel_index]。
通过检查发现样本数计算没有错误。
3.2. 检查内存分配是否正确
我们接着检查内存分配是否存在溢出或未分配富足内存的情况:
- void* sound_memory = PushSize(arena, memory_size);
复制代码 在 PushSize 分配内存时,我们确保:
- memory_size 是计算出的总大小;
- 在填充过程中,内存地点通过 sound_memory += channel_size 正确推进;
- 而且没有在中途发生任何内存重叠或未分配富足内存的情况。
检查发现内存分配也是完全正确的。
3.3. 检查数据填充是否正确
我们末了检查了数据填充过程:
- loaded_sound->samples[channel_index] = sound_memory;
- sound_memory += channel_size;
复制代码 确保:
- 每次填充时,内存指针指向了正确的位置;
- 每次填充完成后,内存指针向前推进 channel_size;
- 不存在填充重叠大概遗漏。
经过检查,这一部分没有发现任何题目。
4. 排查音频混音器中的题目
由于加载过程没有任何题目,我们推测题目很大概出现在音频混音器中。在音频混音器中,有以下几种大概导致“点击声”的原因:
4.1. 缓冲区边界未对齐
假如音频混音器在读取样本数据时,没有对齐缓冲区的边界,就大概导致音频数据读取断裂。例如:
- 当一个音频缓冲区的竣事位置刚利益于采样点的中心;
- 在播放下一个音频缓冲区时,数据未正确拼接,导致音频播放中断;
- 听起来就像是“咔哒”一声的点击音。
我们可以通过检查音频混音器中音频缓冲区的读取和拼接,确保它们是一连的,而不是在数据边界断裂。
4.2. 音频数据跨缓冲区传输
另一种大概是音频数据在跨缓冲区传输时出现数据断裂:
- 例如音频缓冲区大小是 1024;
- 当前缓冲区播放到 1023,接下来播放 1024;
- 但音频数据大概出现了跳跃大概延迟,导致音频听起来有“点击声”。
我们可以通过:
- 在音频缓冲区之间添加渐变(fade in/fade out);
- 确保数据拷贝时不出现断裂;
- 检查音频缓冲区切换是否存在空缺区。
4.3. 音频数据未对齐
假如音频样本在内存中未按 16 位对齐,也大概导致“点击声”:
- 例如音频数据是 16 位 (int16_t),但内存分配未对齐;
- 造成某些采样点的数据错位,形成间歇性的“点击声”。
我们回头检查了内存分配:
- void* memory = PushSize(arena, memory_size);
复制代码 由于 PushSize 本身已经确保内存对齐,因此排除该题目。
4.4. 音频数据未零填充
假如音频数据未零填充 (zero padding),在播放末尾大概出现“点击声”:
- 比如音频长度是 30123 个样本,缓冲区是 4096;
- 播放到末尾时,未填充的缓冲区部分含有随机数据;
- 导致音频末了播放出现异常。
我们可以通过在音频数据末尾举行零填充解决:
- memset(sound_memory + used_size, 0, remaining_size);
复制代码 如许可以制止播放末尾的“点击声”。
5. 下一步优化
现在我们已经确认:
- 音频加载流程没有题目;
- 内存分配完全正确;
- 通道数据存储一连无断裂。
接下来,我们打算:
- 排查混音器中的缓冲区传输逻辑,确保不会产生数据跳跃;
- 在音频播放竣事时添加零填充,制止未初始化数据被播放;
- 在跨缓冲区时添加渐变处理,消除点击声。
✅ 最终排查结果
题目位置排查结果是否导致“点击声”内存分配正常❌数据填充正常❌通道指针正常❌音频混音器大概存在题目✅ 我们可以确定,点击声的题目并非加载过程导致,而是音频混音器存在肯定的题目。我们下一步将排查音频混音器中的:
- 缓冲区边界是否对齐;
- 数据跨缓冲区是否断裂;
- 未填充数据是否播放。
修复这部分题目后,我们的音频播放结果将会更加流通且无任何杂音。
假如release模式运行有时候会段错误,从新编译生成hha就行,不知道什么原因
在 C/C++ 中,Debug 模式和 Release 模式的告急区别体现在 优化、调试信息、运行时检查 等方面。下面是两者的焦点区别:
|