视频参考:https://www.bilibili.com/video/BV1VkBCYmExt
解释 off-by-one 错误
从演讲者的视角:对代码问题的分析与修复过程
- 问题的起因
- 演讲者提到,他可能无意中在代码中造成了一个错误,这与“调试时间标记索引”有关。
- 他发现了一个逻辑问题,即在查抄数组边界时,使用了“调试时间标记索引是否大于数组计数”的条件。
- 这个问题的核心在于,数组是零基索引的,以是边界查抄须要更加严谨。
- 数组索引的解释
- 在C语言中,数组的索引从零开始。
- 比方,如果一个数组包含N个元素,第一个元素的索引是0,而最后一个元素的索引是N-1。
- 如果实验访问索引等于数组计数的元素,会指向数组分配空间之外的位置,这是非法的,也是潜在的错误来源。
- 代码中的现实逻辑
- 演讲者解释说,代码逻辑中涉及的操作是获取数组的起始地址(指针),然后根据索引值计算访问地址。
- 如果索引值超出合法范围,好比大于或等于数组巨细,就会越界访问。
- 他提到,“当索引等于数组计数时,现实指向的是数组分配空间的最后一个字节之后的一个字节”。
- 修复方案与改进
- 演讲者意识到,不须要用“索引大于数组计数”的条件来查抄边界。
- 他以为更公道的方式是通过“索引等于数组计数”来判定是否到达数组的末端,这样可以制止多余的查抄逻辑。
- 为了确保代码结实性,他建议添加断言(assert),用于验证程序永远不会实验访问数组范围之外的地址。
- 代码错误的反思
- 演讲者坦承这是一个“愚笨的错误”,可能是由于编写代码时的疏忽或头脑不敷严谨所致。
- 他提到,这种错误虽然看似简朴,但在特定情况下可能导致程序的不稳定乃至崩溃,因此须要格外注意。
- 修复后的状态
- 通过修复,问题已经解决,程序如今可以正确地处理数组边界情况。
- 演讲者计划进一步查抄代码,确保没有其他雷同问题。
- 调试过程的代价
- 演讲者夸大,只管这是一次调试和修复的过程,但它对程序员,特殊是初学者和中级程序员来说,是一个很好的学习时机。
- 通过这个过程,可以更好地理解数组操作和边界查抄的告急性,以及如何编写更安全的代码。
音频代码状态概览
上面内容涉及到游戏开辟中音频和视频同步的问题,以及作者在编程时遇到的一些挑战和解决方案的可能性。以下是对上述内容的详细复述和解析:
1. 问题配景
作者实验以每秒30帧(30 FPS)的速率运行游戏,这意味着:
- 每帧须要处理的时间是 1000毫秒 / 30 ≈ 33.3 毫秒。
- 目标是在这33.3毫秒的时间窗口内,完成当前帧的计算和渲染,同时天生对应的音频,并保证音频和视频同步。
然而,他们发现现实实现过程中存在音频延迟问题,这使得音频与视频无法美满同步。
2. 音频延迟的根本问题
音频延迟源于硬件(如声卡)和软件(如DirectSound API)的限制:
- 在测量播放光标(Play Cursor)和写入光标(Write Cursor)时,作者发现声卡本身的音频延迟约为 30毫秒。
- 这正好与一帧的时间相当(33.3 毫秒),意味着即使当前帧的音频被正确计算,它也会延迟一个帧时间才被播放。
因此,当前帧的音频无法立刻与该帧同步显示,而是会出如今稍后的帧中。
3. 抱负状态 vs. 现真相况
抱负状态:
- 在每帧时间窗口内,游戏计算并渲染当前帧,同时天生音频。
- 计算和天生的音频可以在显示该帧时立刻播放,与画面同步。
现真相况:
- 由于音频延迟,即使在当前帧天生的音频数据,也要等到下一帧乃至更晚才会被播放。
- 这导致音频和画面的差别步。
4. 延迟导致的音频与视频差别步问题
作者通过微软的DirectSound API测试了音频延迟,并总结了以下结论:
- 音频延迟约为 30毫秒,这与每帧时间基本相称。
- 音频的播放不会立刻发生,而是有一个延迟,这种延迟与音频被写入声卡缓冲区的位置有关。
比方:
- 当前帧计算音频的时间点是第 T T T 毫秒。
- 由于延迟,该音频最早可能在 T + 30 T + 30 T+30毫秒时播放,而此时画面已经进入下一帧或之后的帧。
5. 潜在解决方案
作者提出了几种可能的解决方案,但都存在局限性:
方案1:接受延迟
- 将音频和视频的差别步视为不可制止。
- 在代码中实现一个根本的音频循环,允许延迟存在。
- 这种方法简朴,但会导致游戏体验下降。
方案2:优化音频延迟
- 实验使用其他音频API(如XAudio2)或更新硬件。
- 某些声卡可能支持更低的延迟,从而淘汰差别步的水平。
方案3:重新设计音频处理
- 修改音频处理的逻辑,使其与游戏帧的渲染更独立:
- 在计算帧的同时,预测未来的音频需求。
- 提前计算并写入后续几帧的音频数据。
方案4:依赖更高性能的呆板
- 在延迟更低的呆板上测试,确保在特定硬件上能够实现同步结果。
- 这种方法不可广泛应用于所有设备。
6. 深入探讨同步问题
作者还详细讨论了帧内的音频计算流程:
- 帧内事件:
- 当前帧显示时,下一帧的图形和音频开始计算。
- 图形部门可以实时显示,但音频部门会因为延迟问题滞后。
- 音频的延迟播放:
- 当前帧计算的音频数据,即使被立刻写入缓冲区,也要延迟约30毫秒才能播放。
- 这导致音频总是落后于画面。
- 解决难点:
- 要同时解决音频延迟和实时天生的需求几乎不可能。
- 除非使用特定的硬件或优化方法,否则同步问题难以彻底解决。
总结
作者在讨论中明确了音频与视频同步的技能难点,并提出了多个方向的解决方案,同时也表达了这些方案的局限性。他们可能会在之后实验优化音频处理逻辑,或者接受延迟作为当前技能条件下的妥协。这是一个典型的游戏开辟中的技能挑战,须要在性能、延迟和开辟复杂性之间找到均衡。
分析每一帧的时间分配
上面的内容详细陈诉了游戏开辟中的帧处理逻辑,包括每帧时间的分配和实现方式。以下是内容的详细复述和解析:
1. 帧时间的分解
在每秒30帧的情况下,每帧时间为 33毫秒。作者试图进一步分解这33毫秒,解释帧内详细实行的使命,以及它们在时间上的分布。这些使命包括:
- 输入收罗(Gather Input)
- 游戏逻辑更新(Update Game)
- 渲染准备(Render Prep)
- 渲染图像(Rendering)
- 等候与显示(Wait & Display)
2. 输入收罗阶段
- 时间占用:这一阶段通常耗时较短。
- 使命内容:
- 从键盘、鼠标或游戏手柄网络输入信息。
- 在当前项目中主要使用键盘(可能是为了模拟游戏手柄的输入)。
- 扩展用途:虽然当前游戏中并未使用鼠标,但作者提到可能会在其他场景中须要。
这一阶段的目标是为游戏提供输入数据,用于下一阶段的逻辑更新。
3. 游戏逻辑更新与渲染准备
- 时间占用:比输入收罗耗时稍长,但整体依然较短。
- 使命内容:
- 根据输入数据更新游戏逻辑,比方物理计算、状态变化等。
- 准备渲染所需的数据(渲染列表),这可能包括将场景对象、纹理等整理好,以供渲染器使用。
在二维游戏中,这部门通常不会太复杂。作者提到如果游戏涉及流体结果或其他复杂物理模拟,这部门可能会变得更耗时。
4. 渲染阶段
- 时间占用:这是最耗时的阶段,占据帧时间的大部门。
- 使命内容:
- 将渲染列表中的数据绘制到屏幕上。
- 在软件渲染器的情况下,这一过程尤其耗时,因为所有渲染都由CPU完成。
- 如果切换到GPU渲染,特殊是高性能GPU,渲染时间可能会大幅淘汰。
二维游戏与三维游戏的差别:
- 对于三维游戏,复杂的物理计算和碰撞检测可能占用更多时间。
- 对于当前的二维游戏,渲染可能是性能的主要瓶颈。
作者还提到,如果启用了更高分辨率(如1920×1080)或多重采样(multi-sample),即使是GPU渲染,性能开销也会增加。
5. 等候与显示阶段
- 时间占用:只占用剩余的时间。
- 使命内容:
- 在所有渲染工作完成后,等候帧时间竣事。
- 将当前帧的内容显示在屏幕上(称为“翻转”或Flip)。
- 管道化的可能性:
- 当代硬件允许将多个帧的处理管道化。比方,在一帧渲染的同时,下一帧的输入收罗和逻辑更新可以开始。
- 如果是多线程情况,使命可以进一步重叠,比方在主线程渲染时,子线程收罗输入或更新逻辑。
6. 同步实行的情况
- 如果整个过程在单核CPU上以同步方式运行(无管道化、无多线程),帧处理流程会依次实行:输入收罗 → 逻辑更新 → 渲染 → 等候 → 显示。
- 这种方式较为原始,但在多核CPU和GPU出现之前,这种模式是主流。
7. 关键挑战与解决思路
- 瓶颈辨认:
- 渲染阶段在当前帧中占用时间最多,因此可能成为性能优化的重点。
- 未来优化:
- 切换到GPU渲染以淘汰渲染时间。
- 实现管道化或多线程,以提高帧处理的效率。
- 通过代码优化,降低逻辑更新和物理模拟的复杂度。
8. 总结
帧时间的分解资助作者明确了性能瓶颈和优化方向。通过公道分配帧内使命、使用硬件特性(如GPU渲染和多线程),可以有用淘汰延迟,提升游戏流通度。只管同步实行是较为传统的方式,但当代技能提供了更多提升效率的可能性,比方管道化处理和多线程使命分配。
一种可能的但不抱负的解决音频延迟的方法
上面的内容讨论了如何在游戏的帧处理中插入声音输出的问题,特殊是如何只管淘汰音频延迟,同时和谐声音和画面更新的时序。以下是内容的详细复述和解析:
1. 插入声音的挑战
- 在帧处理流程中,声音的插入须要保证与画面同步,同时只管降低音频延迟。
- 如果存在 33毫秒的音频延迟,那么声音的输出现实上须要提前一个帧时间(即在当前帧的输入收罗之前)进行。
然而,这种方式存在现实困难:
- 在当前帧的输入收罗之前就输出声音意味着我们还不知道这帧的详细输入内容。
- 因此,提前输出声音可能是“不可能”实现的,因为它须要预知尚未发生的事件。
2. 帧处理的时间分布
为了更好地理解声音插入的可能性,作者提出通过分解帧时间来看声音输出的时间点。
单帧时间分布
假设一帧的总时间是33毫秒,此中:
- 输入收罗和逻辑更新:占用 10毫秒。
- 渲染:占用 23毫秒。
这表明:
- 输入收罗和逻辑更新在帧的前10毫秒完成。
- 剩下的时间主要用于渲染和等候。
跨帧的时间线
如果我们把时间线扩展到跨两帧的场景:
- 当前帧的渲染:占用了后半部门时间。
- 下一帧的输入收罗和更新:可以与当前帧的渲染 重叠 进行。
3. 如何淘汰声音延迟
通过分析,作者提出了一种 音频输出延迟最小化方案:
- 在上一帧的渲染过程中,就可以开始为下一帧天生声音数据。
- 这种方式使得声音输出只会比画面延迟 一帧,而不会产生更大的延迟。
时间轴示意
- 在帧时间的后期(渲染阶段),我们已经有足够的信息来确定下一帧的声音内容。
- 如果在当前帧渲染时输出下一帧的声音,则:
- 声音数据可以与画面同步,只落后于画面 一帧时间(33毫秒)。
- 这是可以接受的延迟范围。
4. 使用多核处理优化
为了进一步优化,作者提到可以使用多核处理本领,将帧处理的使命分配到差别的核上,以实现更高效的并行处理。
多核优化示例
- 当前帧的渲染使命在一个核心上进行。
- 下一帧的输入收罗和逻辑更新使命同时在另一个核心上启动。
- 这样,两者可以 重叠,进一步淘汰帧时间的浪费,并提升整体性能。
5. 总结
- 声音输出的插入是一个复杂的问题,必须考虑音频延迟和画面同步的要求。
- 抱负情况下,声音数据的天生应尽可能早地开始,使用当前帧的渲染时间来完成下一帧的声音准备。
- 如果设计公道,声音输出的延迟可以控制在 一帧时间(33毫秒)以内,这是一个可接受的范围。
- 多核处理和使命并行化为进一步优化提供了可能性,通过重叠使命可以最大限度地使用帧时间。
这种分析为游戏开辟中的声音系统设计提供了有代价的参考,同时夸大了多线程和管道化的告急性。
音频延迟与输入延迟之间的权衡
以下是对上述内容的详细复述:
使用两核心处理音频与输入同步的权衡
- 优化音频同步的潜在收益:
- 通过公道使用两核心,渲染和输入处理可以并行运行:
- 一个核心负责渲染当前帧;
- 另一个核心提前处理下一帧的输入收罗和更新。
- 结果:我们可以在音频输出上提前约 40 毫秒完成,确保音频和画面同步输出。
- 这种优化能够在淘汰音频延迟的同时,提升听觉体验,特殊是在须要准确声音反馈的场景中。
- 成本:引入输入滞后:
- 为了实现上述优化,输入收罗的时间会被推迟,因为渲染使命占用了较多的处理资源。
- 这将导致额外的输入滞后,约莫 23 毫秒,这是渲染过程所需的最大时间。
- 换言之,玩家的输入会在更晚的时间点反映到游戏中,影响即时性和流通性。
针对本游戏的适用性分析
- 音频同步的告急性:
- 本游戏的核心玩法并不依赖精准的音频反馈。
- 游戏音效的作用主要是为玩家提供辅助信息,比方:
- 提示敌人攻击的机遇;
- 技能冷却的完成;
- 场景氛围的营造。
- 虽然音频在游戏体验中很告急,但其同步性对于本游戏来说并不是最高优先级,因为没有操作须要与声音反馈直接对齐。
- 输入滞后的影响:
- 与音频同步相比,输入同步对本游戏更为关键。
- 游戏体验的核心在于玩家的即时操作响应,因此输入滞后会显著降低手感:
- 玩家动作的延迟响应会影响操作流通性;
- 游戏的实时性和操控感将因此下降,尤其是在须要快速反应的场景中。
- 引入额外的 23 毫秒输入滞后,将使得玩家的操作与游戏反馈之间出现显着脱节,得不偿失。
结论:优先级选择
- 优先优化输入同步:
- 尽可能提前进行输入收罗,将输入滞后降到最低。
- 输入同步对玩家的操作体验至关告急,必须优先保证。
- 音频延迟可以接受:
- 在音频与输入的均衡中,音频的延迟可作为妥协点。
- 音频滞后对整体体验的影响相对较小,只要延迟在公道范围内即可。
最终,我们的战略应该是:只管优化输入与音频的同步,但绝不以输入同步为代价来寻求音频的完全同步。
回归稍微笨拙的实现
以下是对上述内容的详细复述:
配景:音频与输入同步的问题
在当前开辟阶段,团队面临的挑战是如何在确保音频与画面同步的同时,只管淘汰输入延迟。这须要在系统性能、玩家体验以及代码复杂度之间进行权衡。
核心问题及解决方案的变化
- 现状评估:选择有限,优化空间有限
- 开辟者首先认识到,在硬件性能或其他设计限制下,没有抱负的解决方案。
- 音频同步的目标:
- 希望音频与当前帧完全同步,但实现这一点的成本是巨大的。
- 现实权衡:
- 由于同步音频会引入更多输入延迟,开辟者不得不调解战略。
- 调解后的方案:
- 开辟者决定采取更简朴但“粗糙”的方法,不再强求音频完全对齐当前帧边界,而是接受音频滞后可能落在下一帧的中间位置。
- 音频滞后的接受:
- 预计音频会滞后 15 毫秒左右;
- 游戏代码将被调解以掩饰这种滞后,对游戏逻辑“谎报”音频输出为当前帧的音频,以制止增加代码复杂度。
- 目标优先级:
- 优先淘汰输入延迟,而非寻求准确的音频同步。
- 这一选择被以为比延迟整整一帧(可能多达 16 毫秒)或引入显著输入滞后更为公道。
详细实现战略
- 调解音频代码:
- 非同步音频处理:
- 开辟者将更新音频代码,使其顺应当前硬件条件下的滞后情况;
- 如果检测到音频滞后凌驾可接受的阈值(比方 16 毫秒),系统将尽可能快速地处理音频输出,制止进一步延迟。
- 同步优化:
- 当音频滞后低于阈值时,开辟者会实验通过调解缓冲区,将音频只管与帧同步。
- 两种音频处理路径:
- 系统会根据音频滞后的差别情况选择差别的处理方法:
- 高滞后路径:音频直接输出,但滞后落在下一帧中;
- 低滞后路径:音频输出与当前帧同步。
- 灵活调解战略:
- 系统会测量当前的平均更新时间,并动态调解音频的输出节奏,以实现尽可能平滑的体验。
- 代码复杂性的权衡:
- 开辟者承认这种音频代码的编写过程较为复杂且易出错,须要格外审慎:
- 在过程中已经发现并修正了多处错误,预计后续还会遇到新的问题。
- 只管云云,开辟者以为这是一条可行的优化方向,并将继承积极美满。
开始修改音频输出方法的代码
找到最小的盼望音频延迟
这段内容解说了一个与音频延迟处理相干的技能实现,详细是通过计算两个光标之间的位置差来估算音频的延迟。这些光标包括播放光标(play cursor)和写光标(write cursor)。以下是详细的复述:
核心概念和问题
- 播放光标和写光标:
- 播放光标表示音频正在播放的位置。
- 写光标表示当前写入数据的安全位置。
- 两者之间的差值反映了最低的音频延迟,这是我们盼望在抱负情况下可以到达的延迟。
- 延迟的计算挑战:
- 环形缓冲区问题:
- 使用环形缓冲区时,写光标可能会绕过缓冲区的末了并从头开始写入,而播放光标可能仍在缓冲区的较前位置。
- 这种情况下,简朴地计算写光标和播放光标的差值会得到一个错误的负值,须要对这一问题进行处理。
解决方案的实现
- 未围绕(unwrapped)的写光标:
- 定义一个未围绕的写光标:
- 如果写光标的位置小于播放光标,说明它已经围绕到了缓冲区的开头。
- 在这种情况下,未围绕的写光标值等于写光标加上缓冲区的总巨细。
- 如果写光标未绕回开头,则未围绕的写光标等于写光标本身。
- 计算光标差值:
- 使用未围绕的写光标与播放光标计算差值,这样可以正确反映环形缓冲区中的相对位置。
- 差值公式:
- delta = unwrapped_right_cursor - play_cursor
复制代码 - delta 表示光标之间的字节数,也就是音频延迟。
- 优化代码:
- 使用更简洁的代码实现:
- 通过判定写光标是否小于播放光标来决定是否须要添加缓冲区巨细。
- 这样可以制止复杂的条件判定,使代码更易读。
验证和调试
- 验证环形缓冲区的场景:
- 作者特殊验证了写光标位于播放光标之前的场景,以确保在这种情况下,计算的延迟值仍然正确。
- 测试结果表明,使用未围绕的写光标计算得出的延迟值是稳定且正确的。
- 实时延迟展示:
- 输出的延迟值(以字节为单元)清楚地反映了音频缓冲的当前状态。
- 该值可以转化为秒或者其他单元以供进一步分析。
方法总结
- 这种方法计算出的延迟值能够反映现实的音频延迟状态,同时对环形缓冲区的特殊情况进行了处理。
- 只管有多种方法可以实现雷同的功能,作者选择了最易于理解和调试的方式。
- 最终,代码实现被验证为高效且可靠,能够用于实时音频处理系统。
这段解说清楚地形貌了如何通过光标的位置差计算音频延迟,并解决了环形缓冲区带来的潜在问题,同时还展示了如何写出简洁、清楚、可维护的代码。
使用量纲分析转换为秒
下面的内容解释了如何通过计算音频延迟样本来推导音频延迟时间,涉及了基本的量纲分析和计算方法。以下是逐步的详细复述:
- 从字节到样本的转换
- 计算光标之间的字节数:
首先计算两个光标之间的字节差值。这表示在音频缓冲区中未播放的音频数据量,以字节为单元。
- 将字节转换为样本:
通过将光标之间的字节差除以每个音频样本所占的字节数(bytes per sample),可以得到未播放的音频样本数量。这是因为每个样本由固定数量标字节表示,除以字节数后结果是样本数量。
- 从样本到时间的转换
- 计算样本对应的时间:
将样本数量再除以音频的每秒采样率(samples per second),可以得到音频延迟的时间。
这一步使用了量纲分析:
- 样本数 ÷ 每秒样本数 = 时间(秒)。
这是一种简朴的方法来确保单元的转换是正确的。
- 维度简化与优化
- 归并公式简化计算:
为了淘汰计算步调,可以将“每秒样本数”和“每样本字节数”归并,直接计算“每秒字节数”(bytes per second)。这样,光标之间的字节数直接除以每秒字节数即可得到延迟时间。
这可以通过添加一个“每秒字节数”的字段或变量来优化代码逻辑,制止多次计算和转换。
- 量纲分析的告急性
- 量纲分析(Dimensional Analysis)是确保单元正确的核心工具。在计算中:
- 字节除以“每样本字节数”会得到样本;
- 样本除以“每秒样本数”会得到秒数。
- 若希望在代码中直接跳到秒数的结果,可以通过简化公式,淘汰显式转换操作。
- 数据类型转换的注意事项
- 整数除法与浮点数:
如果变量均为整数,直接实行除法会导致舍入误差。因此,在计算中建议将变量显式转换为浮点数,以获得更准确的结果。
比方:- float latencyInSeconds = (float)BytesBetweenCursors / (float)BytesPerSecond;
复制代码
- 计算结果的验证
- 打印结果进行验证:
输出计算结果以秒为单元的延迟时间。通过观察,计算出的延迟约为 33 毫秒,这符合音频帧时间的预期范围(略低于 30 毫秒)。
- 告急性:
程序中可能出现许多料想之外的问题,因此验证结果是确保计算正确的关键步调。
- 进一步优化与清算
- 代码优化方向:
在代码清算阶段,可以添加字段如“每秒字节数”,淘汰冗余计算,使代码更简洁、更易维护。
- 计算和浮点数转换:
整体逻辑应该在计算前显式地进行类型转换,以确保计算精度,特殊是处理音频时间的计算时。
总结
这段逻辑通过计算光标间的字节差值,联合音频格式(每样本字节数与采样率)来推导音频延迟时间。借助量纲分析,可以清楚地验证公式正确性并确保单元转换无误。最闭幕果(33 毫秒)表明延迟在公道范围内,这种验证是音频开辟的告急组成部门。
根据音频延迟写入声音
音频延迟的计算与保存:
- 音频延迟秒值的作用
- “音频延迟秒”(audio latency seconds)是一个关键的计算值,反映了音频处理中的时间延迟。
- 它被以为是音频系统中能够到达的最佳性能。计算音频延迟秒的目标是在系统中公道化这些延迟并进行后续优化。
- 保存音频延迟值
- 这些音频延迟值被临时保存下来,作为后续处理的一部门,尤其是在验证声音是否有用时(“sound is valid”)。
- 保存的值可能包括音频延迟秒和字节形式的值(audio latency bytes)。这些值为后续操作提供了一个可靠的基准。
- 计算的配景与使用
- 延迟值(秒和字节)会在音频的输出阶段使用。详细来说,当我们计算“写入位置”时(where to write to),延迟值会成为一个告急参考。
样本索引的处理与光标的关联:
- 运行样本索引
- “运行样本索引”(running sample index)是一个核心变量,初始值被设置为当前写光标的位置(write cursor position)。这确保了索引与现实音频数据的位置同步。
- 运行样本索引的单元可以是字节,也可以是样本。设计时须要考虑是否直接以字节为单元进行计算,以淘汰单元转换中的错误。
- 光标的作用
- 光标的位置用于确定音频缓冲区中写入和读取的区域:
- “写光标”(write cursor):指示当前写入的起点。
- “播放光标”(play cursor):记录前次播放的位置。
- 计算时,运行样本索引通常与写光标同步,这样在下一次操作时可以轻松找到目标位置。
计算写入位置的逻辑:
- 目标光标的锁定
- 系统须要根据上一次的写光标和播放光标计算下一次写入的目标光标。通过这种方式,可以确保写入操作不会覆盖未被播放的数据,从而制止音频问题。
- 新设计中提到可能须要保留“最后一个写光标”和“最后一个播放光标”以提高对写入位置的控制精度。
- 淘汰误差的优化
- 在设计中,建议只管使用字节单元直接计算目标位置。这可能会制止因单元转换而导致的错误,同时简化数学操作。
确定音频写入的位置
这段内容形貌了一种计算音频写入机遇的逻辑方法,尤其是在低延迟的音频处理场景中。以下是更详细的复述:
配景与问题
在低延迟音频处理中,程序须要在特定时间写入音频数据,以确保音频播放的流通性并制止延迟导致的中断。
目标是确定:
- 何时应该写入音频数据。
- 写入多少音频数据以覆盖预期的延迟时间。
为了实现这一点,系统会查询音频硬件的播放和写入光标的位置,并使用这些位置计算写入机遇和数据量。
关键概念
- 播放光标 (Play Cursor)
- 表示音频硬件当前正在播放的位置。
- 该位置会随着音频播放不绝向前推进。
- 写光标 (Write Cursor)
- 表示音频缓冲区中可以安全写入而不会覆盖播放数据的位置。
- 通常位于播放光标之后。
- 音频延迟 (Audio Latency)
- 表示从音频数据写入缓冲区到现实播放之间的时间差。
- 延迟通常包括硬件处理时间和软件计算时间。
- 帧时间 (Frame Time)
- 表示一帧渲染的时间(如 33 毫秒对应 30 FPS 的场景)。
- 在这种场景下,音频写入的目标是覆盖至少一帧的音频。
处理逻辑
- 初始翻转 (Flip)
- 第一次翻转(显示图像帧)时,音频缓冲区为空,程序须要写入初始数据。
- 第一次写入时,播放光标和写光标的位置未知,但程序可以假设播放光标靠近初始位置,写光标稍微超前。
- 查询当前光标位置
- 在翻转帧之后,程序会查询播放光标和写光标的位置,以相识缓冲区状态。
- 查询结果可能显示写光标已经超前于播放光标肯定距离。这个距离决定了可用的缓冲空间。
- 计算须要写入的音频量
- 程序根据帧时间和音频延迟计算出须要写入的数据量。目标是确保在下一帧到来之前,音频缓冲区的内容不会被播放光标斲丧殆尽。
- 判定是否须要额外推迟写入
- 如果发现写光标距离播放光标太近(意味着缓冲区几乎满了),可能须要“推迟写入”以制止覆盖未播放的数据。
- 如果缓冲区有足够空间,程序会直接写入一帧时间所需的音频数据。
详细计算方法
步调 1:计算音频写入的起始位置
- 确定当前的写光标位置:
- 查询写光标与播放光标之间的距离,确保在安全范围内写入数据。
- 如果写光标的位置足够靠前,直接开始写入。否则,可能须要延迟写入。
步调 2:计算音频写入的量
- 基本写入量:至少覆盖一帧(33 毫秒)的音频数据。
- 额外考虑音频延迟:
- 如果当前音频延迟较大(大于一帧时间),则须要额外增加写入量以增补延迟。
- 写入量 = 帧时间(33 毫秒)+ 音频延迟 - 已使用的缓冲区空间。
步调 3:动态调解
- 如果硬件和软件之间的时钟差别步,可能须要动态调解写入机遇。
- 比方,如果查询显示写光标超前太多,则可以淘汰写入量,制止产生更多延迟。
特殊情况
- 第一次写入
- 第一次翻转时,播放光标和写光标可能都在缓冲区的起始位置。程序须要写入足够的数据来初始化播放。
- 在这种情况下,直接写入一帧的音频数据通常是安全的。
- 高效硬件的低延迟问题
- 如果硬件处理速度非常快,可能会导致写光标超前许多。此时,程序须要“拉回”音频写入的机遇,淘汰初始延迟。
- 动态调解机制
- 如果缓冲区使用情况不均衡,可能须要动态调解写入战略。比方,增加音频延迟的占比,确保播放光标有足够数据可供播放。
总结
核心目标是确保音频播放的连续性和低延迟:
- 确保每次写入的音频量足以覆盖一帧时间,同时分身硬件延迟。
- 根据当前播放和写入光标的位置,动态调解写入机遇和数据量。
这种方法虽然复杂,但它有用地均衡了实时性和稳定性,适用于高性能音频应用。
如何处理低延迟场景
我们须要实现一个系统,以确保在音频处理中,每个动作都能够以最优化的方式完成。为了确保实现这个目标,我们须要将动作拆分为两帧来实行。
首要目标
我们首先明确目标:
- 确保音频的延迟尽可能低。
- 确定“写光标”位置并调解音频的写入逻辑。
初步规划
案例分析
首先,我们从一些简朴的案例入手,计算现真相况以资助解释整个过程。这么做不但有助于更清楚地理解,同时还能为后续的实施提供指导。这里的关键是:音频低延迟处理。
低延迟音频处理
- 音频延迟须要非常低,以便声音信号能够迅速传递到声卡。
- 我们须要仔细跟踪写光标的移动情况,确保写入的音频与现实播放保持一致。
操作步调
- 初始帧:
- 确定第一帧时的写光标位置。
- 写入一帧音频,使其与光标对齐。
- 后续帧:
- 根据写光标的移动调解写入内容。
- 每次写入平均帧数,确保音频延迟保持最低。
问题分析
音频时钟与系统时钟的差别
一个常见的问题是,音频时钟可能与系统时钟存在微小偏差。比方,音频时钟可能运行得稍慢,或者稍快,这可能导致缓冲区的填充不一致。如果始终按照固定时间写入数据(好比固定的33毫秒),可能会导致缓冲区的溢出或数据不敷。
因此,我们须要一种方法:
- 动态调解写入时长:根据音频时钟的现实斲丧情况,动态调解写入的时长。
- 跟踪光标移动:通过观察写光标的移动情况,计算现实须要写入的音频长度。
详细实现方法
平均光标移动量
我们将观察写光标在两次操作之间的平均移动量。这可以确保:
- 写入的音频与写光标的现实移动保持同步。
- 制止因系统时间和音频时钟的偏差导致的问题。
每次写入时,我们都会使用这样的逻辑:
- 假设33毫秒的音频数据是基准。
- 现实写入的音频长度以写光标的移动量为参考。
实现逻辑
- 初次写入:
- 观察写光标的位置,将其与播放光标对齐。
- 写入一帧音频(对应33毫秒)。
- 后续写入:
- 查抄写光标的移动距离。
- 按照光标移动的平均值,动态调解写入的帧数。
优化方向
在现有方法的根本上,还可以进一步优化计算:
- 直接通过写光标和播放光标的差值,计算须要写入的帧数,而不是依赖平均值。
- 如果可以准确地从音频时钟中推算出偏差值,则可以制止恒久追踪平均值的问题。
挑战和潜在改进
- 复杂计算:
当前方法须要不绝追踪光标的移动,并计算平均值。这在实时音频处理中可能会带来额外开销。
- 简朴替代方案:
如果能找到一种直接使用写光标和播放光标的方法,则可能简化计算过程。
- 计算框架边界:
确保样本索引总是落在帧的边界上,从而制止复杂的跨帧计算。
总结
以上方法旨在实现音频低延迟处理,核心头脑是:
- 动态调解写入的音频时长,使其与音频时钟保持一致。
- 通过追踪写光标的移动,确保音频写入的准确性。
- 如果可能,进一步简化计算过程以提高效率。
这种设计能够在现实使用中大幅降低音频延迟,同时保证播放的稳定性和准确性。
一个令人困惑的情况
下面是上述内容的详细复述:
配景与问题概述
在音频处理过程中,我们须要解决如何准确地估算并写入音频样本的问题,同时应对音频时钟和墙钟(系统时钟)之间的可能差别。
主要思路与挑战
样本数的估算
我们知道每帧音频对应的估算样本数,也能推测须要向前推进的距离。但是,这种估算并不全面,因为我们并不真正相识所有可能的细节,也无法直接确定准确的写入方式。
墙钟与音频时钟的同步问题
墙钟(系统时钟)是我们计算时间的主要依据,但音频时钟并不总是与墙钟同步。它可能稍微偏快或偏慢,详细表现可能与音频采样器的行为相干。这种同步问题增加了估算的复杂性,因为我们不能仅依赖墙钟来决定须要写入多少音频样本。
方法探索
为了应对这些问题,我们须要找到一种既能处理墙钟与音频时钟差别,又能动态调解写入样本数的方法。以下是一些核心思路:
追踪平均值
现在最直接的方法是追踪写光标的平均移动量。这种方法通过多次采样统计写光标的移动情况,计算出平均值,并以此为基准决定写入多少样本。这种方法较为稳定,但计算量可能较大。
光标位置估算
- 写光标与播放光标的关系:
- 如果写光标在一个位置,而播放光标在另一个位置,我们可以使用二者的差值,估算出写光标的目标位置。
- 当时间流逝后,播放光标会前进,我们可以推测出播放器将处于什么位置。
- 确保写入的音频超前于光标:
- 在写入音频时,我们须要确保数据至少超前于写光标的位置,以制止播放过程中出现断裂或延迟。
- 详细而言,我们须要写入凌驾写光标至少一帧的位置,确保音频缓冲区中有足够的数据供播放使用。
墙钟与音频时钟的动态调解
只管墙钟是一个参考,但因为音频时钟可能偏离墙钟,我们须要一个动态调解机制:
- 使用墙钟来估算写光标的目标位置。
- 根据音频时钟的现实表现,修正墙钟的估算结果。
这种调解方法联合了墙钟的稳定性和音频时钟的实时性,有助于提高估算的准确度。
实现过程
以下是一个可能的操作步调:
- 初始化估算:
- 查询墙钟时间,确定当前写光标和播放光标的位置。
- 根据墙钟时间和写光标位置,推算写光标未来的位置。
- 动态写入:
- 确保写入的数据覆盖写光标的位置,并至少超前一帧。
- 根据墙钟与音频时钟的同步情况,动态调解写入的样本数。
- 迭代更新:
- 每次写入后,重新查询墙钟和光标位置,更新估算模型。
- 如果发现墙钟与音频时钟之间存在显着的偏差,调解估算方法以顺应新的情况。
方法的局限与改进空间
- 复杂性问题:
当前方法依赖多次采样宁静均值计算,这可能在某些实时场景下造成额外的性能开销。
- 同步精度问题:
墙钟与音频时钟的偏差可能因设备或情况的差别而表现各异,须要更多的实验数据来优化同步算法。
- 改进方向:
- 开辟更高效的算法,直接使用写光标和播放光标的位置关系进行估算,淘汰对平均值的依赖。
- 如果硬件支持,直接访问音频时钟数据,以提高同步的准确性。
结论
以上方法提供了一种解决音频低延迟处理问题的思路,通过动态调解写入样本数和光标位置估算,可以有用地缓解墙钟与音频时钟差别步带来的影响。只管仍有优化空间,但这一框架能够在大多数场景下提供较为稳定和可靠的性能。
恍然大悟的瞬间
问题配景与解决提议
在音频系统开辟中,如何确保高效、低延迟且精准的音频输出是一个核心挑战。这里提出了一个新思路,用以解决光标位置估算和写入同步的问题。
提议的核心思路
- 动态估算写光标的位置:
- 通过墙钟(系统时钟)获取当前时间,估算写光标未来的位置。我们称这一过程为“预测”或“向前推算”。
- 基于预测结果,决定写入音频的目标位置。
- 对齐至帧边界:
- 为了简化处理和提高效率,将写入的样本索引向上对齐到下一帧的边界。
- 这样,系统总是填充到下一个完备帧的位置,从而保证数据的稳定性和连贯性。
- 条件判定:
- 只有当写光标落后于当前时间时才会进行上述操作。这种判定制止了不须要的重复写入,也确保数据流的实时性。
详细实现步调
1. 初始估算
- 每次唤醒时:
- 查询写光标的当前位置。
- 使用墙钟预测写光标未来的位置。
2. 确定写入目标
- 将估算出的写光标目标位置向上舍入到下一个帧边界。
- 计算须要写入的样本索引,该索引通常略超出写光标的位置以确保完备帧覆盖。
3. 写入操作
- 根据计算结果填充音频数据,确保写光标始终有足够的数据进行播放。
4. 循环迭代
- 连续更新光标位置,重复上述操作以维持系统的流通性和低延迟。
提议的优势
- 同步改进:
- 这种方法通过墙钟估算写光标位置,并根据帧边界对齐,显著提高了音频输出的同步性。
- 高效填充:
- 写入操作始终基于帧边界,既淘汰了不须要的计算,又确保了数据流的完备性。
- 动态响应:
- 系统能够根据光标位置的实时变化,动态调解写入逻辑,增强了灵活性。
个人感受与反思
开辟者表达了对这一方法的承认,以为它解决了困扰已久的难题:
- 渐进突破的成就感:
- 解决这个问题的过程雷同于玩一款复杂的拼图游戏。只管一开始让人困惑,但逐步找出解决方案带来的成就感非常猛烈。
- 系统性改进的告急性:
- 已往的音频系统可能忽略了声卡的潜力,导致音频输出过早或过晚,无法充分使用低延迟硬件。而通过专注于优化写入逻辑,这一方法不但提升了当前项目标质量,还为未来的音频开辟奠基了根本。
未来预测
- 理论总结:
- 该方法有潜力成为音频输出优化的尺度解决方案。开辟者希望未来能撰写一篇详尽的文章,分享这一履历,让更多人受益。
- 长远代价:
- 通过一次性解决这个复杂问题,未来的音频层将具备动态顺应的本领,从而提升整个系统的结实性和可靠性。
总结
通过联合墙钟估算、帧边界对齐和动态响应,这一提议为音频输出提供了一个创新的解决思路。只管开辟过程充满挑战,但其成果令人满足,不但为当前项目提供相识决方案,也为行业带来了潜在的代价提升。这种渐进式突破的过程体现了编程的魅力和意义。
我们将采取的低延迟处理方法
音频输出的两种延迟情况
在处理音频输出时,系统可能面临两种延迟情况:
- 低延迟场景:
- 系统延迟很小,比方 5 到 10 毫秒,大部门延迟主要来源于音频处理本身。
- 这种场景适用于专业级音频设备、控制台或低延迟驱动程序(如 ASIO 驱动程序)。
- 高延迟场景:
- 延迟相对较大,须要单独处理以确保音频和视频的同步。
基本操作流程
1. 唤醒与光标获取
- 系统唤醒:音频系统定期唤醒以查抄当前状态。
- 获取写光标位置:查询当前写光标(写光标)的位置。这是判定系统当前音频状态的关键数据。
2. 映射写光标位置
- 基于写光标的当前位置,预测其在未来的某个时间点的位置:
- 通过将当前写光标位置加上系统帧样本数进行计算。
- 比方,若每帧包含 N 个样本,则写光标的新位置为:
写光标预测位置 = 当前写光标位置 + 帧样本数。
3. 确定目标帧边界
- 对齐到下一帧边界:
- 映射出的写光标位置可能落在一个帧的中间,因此须要将其向上舍入到下一个完备帧的边界。
- 这种对齐确保写入操作总是以完备帧为单元进行。
4. 写入音频数据
- 根据计算出的帧边界,决定写入的样本范围。
- 填凑数据直至目标帧边界,确保光标总是提前有足够的数据可供播放。
示例说明
假设场景
- 当前写光标位置:位于某帧的中部。
- 系统计划每次写入一帧的样本数。
操作流程
- 预测光标位置:
- 系统计算写光标未来的位置(基于当前光标位置加帧样本数)。
- 如果预测的光标位置仍位于帧的范围内,则进一步向上舍入。
- 确定写入目标:
- 根据预测的写光标位置,找到下一个完备帧的边界。
- 目标是确保填充到下一帧的开始位置,或稍微超出一帧以制止音频数据不敷。
- 写入数据:
- 从当前光标位置填充音频数据,直至计算出的目标帧边界。
- 确保写入操作覆盖下一个播放周期所需的数据量。
实现要点
- 同步战略:
- 在低延迟场景中,这种方法通过准确的预测和对齐,确保音频和系统的同步。
- 在高延迟场景中,须要进一步调解以顺应较大的延迟。
- 灵活性:
- 该方法适配于多种硬件情况,比方专业级音频设备或游戏主机等。
- 效率提升:
- 通过向上舍入到帧边界,优化了音频数据的填充流程,淘汰了不须要的计算和数据丢失的风险。
开辟者的感受与总结
开辟者以为这一方案提供了一个清楚的方向,用以解决音频输出的核心问题。以下是其反思:
- 低延迟设备的潜力:
- 在专业级硬件(如 ASIO 驱动程序或控制台设备)上,音频延迟可以非常小(5-10 毫秒)。如果能够充分使用这些设备的特性,音频系统的性能会显著提升。
- 预测与调解的告急性:
- 通过映射光标位置并动态调解写入范围,可以有用制止音频失真或差别步的问题。
- 逐步美满的成就感:
- 只管这个问题复杂且涉及许多细节,但逐步攻克难点的过程令人振奋,带来了猛烈的成就感。
高延迟方法
高延迟情况分析
在处理高延迟音频卡时,与低延迟情境的处理逻辑雷同,但有一些显著区别。主要体如今以下几点:
- 延迟影响:
- 音频系统唤醒时,写光标的位置可能已经凌驾了当前帧的边界。
- 须要针对这种延迟情况,调解写入数据的范围和战略。
- 计算目标写入位置的调解:
- 在高延迟场景中,目标不再简朴地对齐到下一帧边界,而是可以引入“安全系数”(Safety Margin),以淘汰延迟的不确定性。
基本操作流程
1. 系统唤醒与光标位置
- 系统唤醒:
- 音频系统按照固定的时间间隔唤醒,用于处理音频数据。
- 唤醒时间与声卡延迟无关,因此音频和游戏更新的时间间隔保持一致。
- 获取写光标位置:
- 通过查询写光标的位置,判定它是否已经超出了当前帧边界。
- 高延迟情况下,写光标的位置通常会超出当前帧,乃至靠近下一帧的范围。
2. 确定写入目标位置
- 目标位置的计算:
- 通过计算,决定写入数据的范围是从当前光标到下一帧边界,或者仅添加肯定的“安全系数”。
- 如果写光标位置已经超出当前帧边界,则目标可以选择:
- 对齐到下一帧边界:直接舍入到完备帧的边界。
- 加入安全系数:在写光标预测位置的根本上,额外添加一小段数据,制止数据不敷导致的播放问题。
- 安全系数的使用:
- 安全系数可以是一个非常小的值(如 1 毫秒或几个样本)。
- 它的作用是淘汰高延迟下由于变量变化可能引入的偏差。
- 与直接舍入到下一帧相比,使用安全系数能够提供更灵活的写入范围控制。
3. 写入数据
- 从写光标的当前位置开始填凑数据,根据目标位置决定填充的范围。
- 在高延迟场景中,目标位置通常考虑到安全系数,而不是直接舍入到下一帧。
启动时的特殊处理
1. 光标位置的未知性
- 初次启动时的挑战:
- 在系统刚启动时,写光标的位置可能不明确。
- 须要通过查询播放光标和写光标的位置差(Delta),推断当前的帧同步状态。
- 光标脚色分配:
- 播放光标(Play Cursor):表示音频系统当前正在播放的位置。
- 写光标(Write Cursor):表示音频系统准备写入的目标位置。
- 初次启动时,通过计算两者的距离(Delta),确定写光标的位置相对于当前帧的偏移。
2. 映射复杂性
- 映射写光标的位置:
- 须要将写光标的位置映射到帧的范围内,以确保计算的目标写入位置公道。
- 这增加了额外的复杂性,但对系统启动后的正常运行至关告急。
示例说明
假设场景
- 当前系统唤醒,写光标位置位于下一帧范围之外。
- 系统计划填充的数据量须要足够覆盖高延迟带来的额外播放时间。
操作流程
- 获取光标位置:
- 查询当前写光标的位置。
- 如果写光标超出当前帧的边界,则目标位置调解到下一帧或加入安全系数。
- 计算目标位置:
- 写入数据:
总结与思考
1. 高延迟场景的关键点
- 安全系数的作用:
- 在高延迟情况下,安全系数提供了一种灵活的控制方法,淘汰了系统过度填充的风险。
- 安全系数的巨细可以根据现实延迟的颠簸范围动态调解。
- 对齐战略的选择:
- 根据延迟的严峻水平,可以选择直接对齐到帧边界或采用安全系数战略。
2. 启动阶段的处理
- 在初次启动时,写光标的位置可能不明确,须要通过计算与播放光标的差值来推断其位置。
- 这一过程增加了映射计算的复杂性,但为后续的正常运行奠基了根本。
3. 实现目标
- 确保音频数据的连续性,制止播放中断或失真。
- 均衡写入的范围,淘汰延迟对整体音频输出质量的影响。
4. 未来改进方向
- 开辟动态调解安全系数的算法,使系统能够根据实时延迟状态优化填充战略。
- 简化初次启动时的光标映射过程,进一步提升系统的鲁棒性和效率。
通过上述方法,可以有用应对高延迟音频卡的挑战,同时确保音频输出的同步和稳定。
又遇到一个障碍
关于播放计数器的延迟更新
一个关键点是播放计数器的更新频率较低,这使得它在时间上的位置并不总是准确。详细来说,播放计数器每10毫秒更新一次,这意味着在任意时刻,计数器的陈诉位置可能存在正负5毫秒的误差。
举例来说,我们查看音频数据缓冲区并记录播放计数器位置时,这个位置可能已经过期了,或即将更新。这种不确定性增加了处理音频输出的复杂性。
用代码分析粒度误差
通过现实计算,播放计数器更新的粒度是每480个样本更新一次(基于常见的采样率计算得出约10毫秒)。这意味着每个更新周期可能引入正负5毫秒的误差。
这种误差对于音频输出的准确性可能是显著的,尤其在处理低延迟音频时,它可能导致音频差别步的征象。
延迟对同步的影响
假想以下情形:
- 在音频缓冲区中,计算出播放计数器的位置。
- 与“写光标”的比力(写光标指代音频缓冲区中预计要播放的位置)。
- 如果写光标的位置在播放计数器的后面,表明我们有足够的时间去计算并处理下一个音频帧。
- 如果写光标超前过多,可能说明时间估算出现偏差。
然而,由于播放计数器的更新粒度较低,我们无法准确定位播放的位置。这种情况雷同于用模糊的舆图导航,难以准确判定。
应对战略
为相识决这个问题,我们可以:
- 以播放计数器为参考,估算音频帧的边界。
- 播放计数器是当前播放位置的近似值,通过它可以大抵计算须要播放音频数据的区域。
- 在这个区域内,我们基于写光标的位置,进一步确定现实要写入的音频数据。
- 引入“安全边际”来处理潜在的误差。
- 由于写光标的位置可能并不完全准确,我们可以在其根本上增加一个小的安全边际(如几毫秒)。
- 这种方法确保即使在延迟存在的情况下,我们仍能覆盖可能须要播放的音频数据。
- 动态调解安全边际的巨细。
- 安全边际可以根据现实硬件的表现来调解。比方,如果观察到系统延迟更高,可以增加边际;如果延迟较低,可以恰当淘汰。
实现的细节
- 初始同步问题:
- 刚启动时,由于无法准确知道写光标的详细位置,我们可以保守地假设播放计数器提供的是最小边界值。
- 使用这种方法,即使写光标的陈诉稍有误差,我们仍然可以确保音频的流通性。
- 恒久收敛:
- 随着音频系统的运行,硬件可能渐渐趋向于更准确的计时。这时,音频的帧同步可以准确地到达翻转点,从而实现低延迟、高准确度的音频输出。
总结和反思
- 理论上的可行性:
- 当前的分析和逻辑表明,这种方法可以应对粒度误差带来的问题。
- 使用保守的估算和公道的安全边际,可以最大水平地降低潜在问题。
- 实践中的验证:
- 还须要通过现实运行和测试来验证理论的可靠性。
- 只管逻辑上看起来是公道的,但可能存在一些尚未发现的漏洞。
- 反馈机制:
- 一种建议是通过用户社区或论坛网络反馈,讨论如何进一步优化解决方案,比方选择更高效的方法或改进安全边际的动态调解战略。
用文字形貌我们的解决方案
在音频输出的代码部门,首先要说明的是这个过程是如何运作的。我们会定义一个“安全值”,这个值是我们以为游戏更新循环可能会变化的样本数量。假设这个变化范围是2毫秒(或者其他公道值)。
当我们准备写音频数据时,我们会查看播放光标的位置,并预测它在下一帧边界会处于哪里。然后,我们会查抄写入光,标的位置,判定它是否处于目标位置之前,至少在安全值的范围内。如果是这样,目标填充位置会是该帧的边界加上一帧,我们会将音频数据写入到下一个帧边界,再加上一帧,以保证音频的美满同步。
如果写入光标的位置已经超出了这个安全值的范围,我们就以为无法美满同步音频。在这种情况下,我们会写入一个帧的音频数据,再加上一些额外的掩护样本。掩护样本的数量会根据我们定义的安全值来确定,这个安全值可以是以毫秒为单元的延迟,或者是样本的数量。我们假设游戏的更新可能有最多2毫秒的变化。
这个安全值是用来确保即使游戏更新的时间发生变化,音频同步仍然可以尽可能准确。在大多数情况下,如果硬件的延迟足够低,音频可以美满同步。但如果硬件的延迟较高,音频同步就会受到影响,这时我们会通过额外的掩护样本来制止音频“掉帧”。
如果我们判定右边的光标处于目标位置之前,说明音频同步是可行的,我们会将数据写到下一个帧边界,再加上一帧;如果光标已经超出目标边界,就会写入当前帧的数据,再加上一些掩护样本,以确保音频的平稳播放。
这段话的核心是通过预测和查抄播放光标与写入光标的相对位置,联合一个定义好的安全值,来决定如何写入音频数据,从而在差别的硬件情况下尽可能实现音频的准确同步。
这个代码片段看起来是音频处理系统的一部门,此中 SafetyBytes 是根据音频输出的采样率、每个采样的字节数和游戏更新率来计算的。
公式解析:
- SoundOutput.SafetyBytes = (SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample / GameUpdateHz) / 3;
复制代码 各部门的寄义:
- SoundOutput.SamplesPerSecond:
这个值表示音频的采样率,即每秒钟采样的次数(比方,44,100 采样/秒是CD音质的音频采样率)。这是确定音频数据缓冲区巨细的关键因素。
- SoundOutput.BytesPerSample:
这个值表示每个音频采样使用的字节数。好比,16位音频通常每个采样为2字节(因为16位 = 2字节)。
- GameUpdateHz:
这个值表示游戏的更新频率,通常是游戏的帧率或更新频率,单元是Hz(每秒更新次数)。比方,如果游戏以60帧每秒(FPS)更新,则 GameUpdateHz 为60。
- SafetyBytes:
这个值表示一个“安全”字节数,用来确保每次游戏更新之间有足够的音频数据处理。通过保证有足够的音频数据在处理过程中,制止音频播放时出现延迟或卡顿。
公式的作用:
- SamplesPerSecond × BytesPerSample:
这个操作计算出每秒音频数据的总字节数。
- 除以 GameUpdateHz:
将音频数据按游戏更新频率进行调解,计算出每个游戏更新周期(每帧)须要的音频数据量。现实上,计算的是每帧须要多少字节的音频数据,以便在每次游戏更新时能平稳处理。
- 除以 3:
这部门的作用是为缓冲区设置一个“安全余量”。数字3可能是为了在处理音频时留出肯定的缓冲时间,确保音频处理不会由于延迟或数据不敷而导致问题。
示例计算:
假设以下值:
- SamplesPerSecond = 44,100(尺度CD音质音频采样率)
- BytesPerSample = 2(16位音频,每个采样2字节)
- GameUpdateHz = 60(游戏更新频率为60帧每秒)
那么计算过程如下:
- SoundOutput.SafetyBytes = (44,100 * 2 / 60) / 3
- = (88,200 / 60) / 3
- = 1,470 / 3
- = 490
复制代码 因此,SoundOutput.SafetyBytes 的值将被设置为 490 字节。这意味着,每个游戏更新周期内,应该有 490 字节的音频数据准备好,以保持音频的平稳播放。
总结:
这个公式的核心目标是确保每个游戏更新周期之间有足够的音频数据进行处理,从而制止出现音频播放延迟或卡顿的情况。SafetyBytes 用来调解音频缓冲区的巨细,以便在后台处理时制止音频数据的耗尽,同时也不会造成过多的内存斲丧或延迟。
记录一下这次改动
接下来修改界面显示调试输出的代码
添加按下P键界面停息
初次查看调试绘图输出并添加全局停息键
固然,以下是对上述内容的详细复述,用于梳理逻辑并确保更容易理解:
我们当前正在调试一个程序,并查看其工作情况。首先,我们开始查抄一些显示内容,这些内容主要是音频缓冲区内差别指针的状态。
查抄内容
- 添加标签和辅助信息:
- 虽然添加标签和辅助显示内容可能会使调试更清楚,但当前我们先看现有的输出结果。
- 查看第一个标记:
- 我们主要观察第一个标记的状态,它包含:
- 播放光标 (Play Cursor):这是当音频缓冲区正在播放时指针地点的位置。
- 写入光标 (Write Cursor):这是当我们向缓冲区写入数据时指针地点的位置。
- 调试器中的展示:
- 在调试器中,我们可以清楚地看到:
- 白线表示播放光标的位置。
- 红线表示写入光标的位置。
- 翻转位置 (Flip Location) 显示为底部的一条白线。
- 查抄逻辑正确性:
- 通过调试结果可以确认:
- 各个光标的位置符合我们的预期,显示逻辑和程序的现实行为一致。
- 翻转位置(底部的白线)是在翻转时播放光标的位置。
- 改进调试:
- 由于调试内容会动态更新,当前没有一个“停息”功能,导致鼠标无法自由移动或指针难以准确查看。以是我们决定为显示添加停息功能。
添加全局停息功能
- 设计停息功能:
- 我们引入一个全局变量 GlobalPause 来控制程序是否进入停息状态。
- 通过按下某个键(比方 P 键)切换 GlobalPause 的状态。
- 当程序停息时,主要实现以下目标:
- 停止更新(Update)逻辑。
- 停息所有绘制(Blit)和输入处理。
- 实现停息逻辑:
- 在主循环中添加查抄逻辑:
- 如果 GlobalPause 被激活,则不再实行主要的更新和绘制操作。
- 保留处理输入的功能,以便用户可以通过再次按键规复程序。
- 处理潜在问题:
- 当前实现的停息功能存在一些潜在问题,比方:
- 如果主循环完全停息,那么无法响应输入规复状态。
- 调试显示可能会卡顿。
- 以是我们须要保留某些核心功能(比方消息处理 Win32ProcessMessages),以便程序可以正确地取消停息。
- 改进方案:
- 将停息的实现改进为更细粒度的控制,确保可以逐步扩展功能,比方:
- 仅停息特定部门(如音频渲染、动画更新等)。
- 保留用户交互功能。
停息功能的逻辑代码
以下是实现停息功能的伪代码:
- // 定义全局变量
- bool GlobalPause = false;
- // 在主循环中添加逻辑
- if (IsKeyDown(P)) { // 按下 'P' 键切换暂停状态
- GlobalPause = !GlobalPause; // 切换暂停状态
- }
- // 更新和绘制逻辑
- if (!GlobalPause) {
- // 如果未暂停,执行更新和绘制
- UpdateGame();
- RenderGame();
- } else {
- // 如果暂停,不执行主要逻辑
- // 保留消息处理以支持取消暂停
- Win32ProcessMessages();
- }
复制代码 主要调试目标
- 展示音频缓冲区状态:
- 白线表示播放光标。
- 红线表示写入光标。
- 底部白线表示翻转光标。
- 停息功能:
- 后续优化:
- 改进停息功能的实现逻辑,制止程序在停息状态下出现其他问题。
总结
通过调试器和程序的改进,我们确认了各个光标状态的显示逻辑正确性,并引入了一个简朴的全局停息功能,方便更好地观察调试输出。这是一个根本的实现,将在后续优化过程中不绝改进停息功能的细节和可靠性。
仔细查抄调试绘图输出
以下是上述内容的详细复述,用于理清逻辑和内容细节:
我们刚刚实现了一种停息功能,如今可以停息程序运行,这使我们更容易观察和验证程序的行为。
调试状态验证
- 当前显示的内容:
- 我们绘制了一个基线 (Baseline),用于显示音频缓冲区状态。
- 如今,让我们逐一验证绘制的内容。
- 第一个绘制的内容:
- 基线以下的第一个绘制内容是:
- 输出播放光标位置 (Output Play Cursor Position)。
- 输出写入光标位置 (Output Write Cursor Position)。
- 这表示当程序开始运行时,我们询问系统当前缓冲区的状态(即光标的位置),并得到了这些值。
- 计算写入位置:
- 接着,我们计算了将要写入缓冲区的位置。
- 详细而言:
- 输出位置 (Output Location) 表示我们将要写入的位置。
- 根据计算结果,我们决定要写入的字节数,并将其显示在图形上。
- 翻页(Page Flip)的机遇:
- 当到达翻页时刻时,我们在图形上记录了翻页时的状态:
- 翻页时写入光标的位置 (Right Cursor Position during Page Flip)。
- 这个位置虽然不直接影响当前调试逻辑,但我们仍然记录了它以备分析。
- 音频延迟分析:
- 音频延迟 (Audio Latency) 是播放光标和写入光标之间的时间差。
- 在我们的图形中,这可以通过两条线之间的距离直观地观察。
- 当前的音频延迟已经小于一帧的时间(Frame Time),这是一个抱负的状态。
延迟性能评估
- 延迟低于一帧:
- 这是我们调试过程中希望到达的结果,即只管淘汰音频延迟。
- 延迟的淘汰有助于提升音频与视频的同步性,从而提供更流通的用户体验。
- 潜在的更低延迟:
- 在某些情况下,延迟乃至可能会进一步降低。
- 详细结果取决于程序运行时的调理、音频缓冲区的巨细等因素。
- 结果满足度:
- 当前结果已经令人满足。
- 虽然有些方面可能须要进一步优化,但从现有状态来看,延迟表现符合预期。
总结
- 我们实现了以下功能:
- 绘制了音频缓冲区状态,包括播放光标、写入光标和翻页光标。
- 验证了缓冲区的状态是否符合预期,并对音频延迟进行了测量和分析。
- 当前的音频延迟:
- 音频延迟已经小于一帧时间,这是一个令人满足的结果。
- 整体感受:
- 对现有的实现感到满足,尤其是在延迟优化方面取得了抱负的结果。
- 下一步可能的优化方向:
- 进一步降低音频延迟。
- 增强程序的稳定性,以应对更多极度情况。
通过这次调试,我们不但验证了程序的行为,同时确认了系统在当前实现下的延迟表现符合预期。这种分析方法对后续优化提供了有代价的参考数据。
绘制另一个调试标记
以下是上述内容的详细复述,只管还原细节和逻辑:
新增功能:绘制预期的帧翻转时间
- 目标:
- 增加一个新的可视化标记,用于显示 “预期的帧翻转时间”。
- 该标记是根据程序计算的预期值,用以验证现实值与计算值是否一致。
- 绘制的依据:
- 绘制一个 “预期的帧边界字节”(Expected Frame Boundary Byte)。
- 该边界字节基于当前 播放光标位置(Play Cursor Position),加上每帧的 预期音频字节(Expected Sound Bytes per Frame) 计算得出。
实现中的问题与调解
- 问题辨认:
- 在最初的实现中,计算逻辑存在偏差:
- 忘记扣除 当前已流逝的时间。
- 翻转时间并不是整帧的时间值,而应该考虑当前时间的进度,从而淘汰不须要的延迟。
- 问题后果:
- 虽然偏差并未显现为显着的 bug,但它会导致音频延迟比预期稍高。
- 修复计划:
- 纠正计算逻辑,确保 帧翻转的预期时间 更加准确。
- 通过修复,进一步降低音频延迟。
绘制逻辑的实现
- 绘制颜色选择:
- 新增标记颜色为黄色(Yellow),因为黄色清楚且容易与其他标记区分。
- 在颜色选择过程中,也考虑过青色(Cyan)和紫色(Purple),但最终选择黄色。
- 绘制代码插入:
- 在现有代码中插入绘制逻辑,使其能够绘制 “预期的帧翻转光标”(Expected Flip Cursor)。
- 关键步调:
- 计算 Expected Flip Play Cursor。
- 在图形界面上绘制该光标。
- 初步结果:
- 修复后绘制的预期光标位置,能够很好地靠近现实光标位置。
- 只管有时光标可能不完全重合,但整体偏差已经非常小,属于公道范围。
程序验证和结果分析
- 光标位置对比:
- 绘制后,可以直观地观察现实光标位置和预期光标位置的差别。
- 在某些情况下,预期光标与现实光标非常靠近,但偶然可能有轻微偏差。
- 误差原因分析:
- 游戏更新的毫秒级延迟非常低,因此估算的翻转时间已经非常靠近现实值。
- 偏差偶发于一些运气较差的场景,但属于可接受范围内。
- 验证逻辑的增补:
- 为确保计算和绘制的准确性,新增了对 Expected Flip Cursor 的断言(Assertion)。
- 断言资助验证计算值是否在正常范围内,进一步保证了程序的妥当性。
最终总结
- 新增功能的意义:
- 绘制预期帧翻转时间的标记,能够资助开辟者直观验证程序的音频同步和延迟控制结果。
- 提供了更好的调试工具,有助于优化和校准程序性能。
- 修复带来的改进:
- 纠正计算逻辑后,音频延迟进一步降低。
- 实现了更准确的帧翻转时间预测,使得程序表现更加可靠。
- 当前表现评价:
- 初步结果令人满足,只管在某些场景下光标位置有轻微偏差,但整体误差已控制在公道范围。
- 程序的实时性和延迟优化表现良好,基本到达了预期目标。
- 后续优化方向:
- 进一步美满测试,验证在复杂场景下的稳定性。
- 优化其他可能的潜在延迟因素,以求到达更加精准和流通的音频与视频同步结果。
通过这次调解和新增功能的实现,程序的延迟控制和可视化本领得到了显著提升。这为后续的优化和功能扩展打下了坚固根本,同时也进一步提高了开辟效率和代码质量。
改进对 PlayCursor 位置的估算以淘汰延迟
详细复述与解释:
配景:
这段内容形貌了在游戏或多媒体应用中,分析和修正“帧翻转”(frame flip)同步的问题。目标是将计算出的“翻转时间”(frame flip timing,与视觉或音频帧的更新点相干)与系统计算的盼望时间对齐,从而提高时间精度并淘汰延迟。
- 绘制盼望的翻转标记:
解说者提到须要增加一个可视化的标记,用来表示盼望的帧翻转时间:
- 该标记显示的是预测的帧边界字节,对应于预估的帧翻转点。
- 最初的假想是,这个标记应该与现实的帧翻转时间点紧密相干。
但在查抄过程中发现,这两个点之间存在肯定偏差,说明计算可能存在问题。
- 辨认问题:
- 盼望的翻转时间计算存在错误。
- 解说者意识到,在计算时没有正确考虑当前帧中已已往的时间。计算假设整个帧的连续时间,但现实只需计算帧内剩余时间(离下一次翻转的时间)。
- 修正计算方法:
- 找到了问题的根源:未将当前帧开始以来的已用时间减去,导致预测的翻转时间点提前或与现实翻转点不符。
- 决定重新绘制标记,直观地查抄偏差,同时修正计算以纳入已用时间。
- 音频延迟的影响:
- 只管这种偏差对画面表现的影响很小(只有微弱的延迟),但修正后可以进一步优化时间同步。
- 解说者指出,这种偏差是由于盼望的帧边界字节数与现实字节数之间的差别引起的。
- 引入挂钟时间(Wall Clock Time):
为了更好地分析问题,他们决定记录挂钟时间:
- 挂钟时间是翻转发生时的准确时间点,由系统在翻转触发后立刻捕获。
- 通过比力挂钟时间与预测时间,能够更好地辨认盼望和现实翻转事件之间的不一致。
- 记录挂钟时间的步调:
- 引入一个新变量 FlipWallClock,用于存储翻转后立刻捕获的挂钟时间。
- 每一帧都通过系统查询记录该值,用于测量上一帧翻转后经过的时间。
- 使预测与现实对齐:
- 为计算离下一次翻转的剩余时间,从每帧目标连续时间中减去已用时间:
- SecondsLeftUntilFlip = TargetSecondsPerFrame - ElapsedSeconds
- 将剩余时间占帧连续时间的比例乘以每帧的总字节数,以计算翻转前应该处理的字节数:
- ExpectedBytesUntilFlip = BytesPerFrame * (SecondsLeftUntilFlip / TargetSecondsPerFrame)
- 可视化的相干性:
- 将新的盼望标记与现实翻转标记绘制在一起,目标是黄色标记(表示预测的翻转时间)能够与观察到的帧翻转事件紧密对齐。
- 解说者注意到,这些标记虽然靠近,但在某些情况下未完全对齐,这可能是因为系统中帧更新时间非常短。
- 确保正确的理解:
- 解说者发现本身之前比力了错误的标记,导致了混淆。现实上,黄色标记应该直接与现实翻转时间相干,因为它代表修正后的预测值。
- 改进可视化方式:
- 为制止未来的误读,解说者决定将绘制的标记延伸到相干的轴上,以便更清楚地对比盼望和现实的翻转事件是否对齐。
- 这样可以更加直观地发现问题并进行调解。
- 下一步计划:
- 通过优化可视化和改进计算方法,他们计划迭代调解逻辑,直到预测的翻转事件与现实翻转事件更一致。
这段过程展示了如何通过详细调试、准确的时间计算以及可视化手段,优化多媒体系统的同步。每一步修正都在淘汰延迟、提高时间精度,从而改进用户体验。
查看估算与现实的对齐情况
详细复述与解释:
- 问题形貌:
解说者仍然未到达预期的结果,特殊是在黄色值和白色值之间的关联性上:
- 黄色线表示预期的播放光标位置,而白色线则是现实的翻转光标位置。
- 解说者希望这两个值能够紧密对齐,但现在并没有完全如预期那样同步。
- 底部点值的解释:
解说者指出,底部的点值是指当翻转发生时,播放光标的现实位置。这个位置被称为翻转光标,而它对应的是白色线的现实位置。
- 解说者曾经以为黄色线和底部的白色线应对齐,现实上它们并不总是准确对齐。
- 黄色线的寄义:
- 黄色线代表的是预期的播放光标位置,这个位置应该与系统预估的翻转时间相匹配。
- 解说者如今确认,黄色线和白色线应该尽可能紧密对齐,这样才能反映系统正确同步的状态。
- 抱负情况:
- 在抱负情况下,**黄色线(预期播放光标)与白色线(现实翻转光标)**应该紧密对齐,这样可以确保翻转和播放时间的美满同步。
- 解说者观察到,现实上这两个线条大部门时间内是对齐的,但有时会出现些许偏差。
- 问题出现的情况:
- 只管大多数时间两条线能对齐,但偶然会有一些微小的偏差,这可能是由于系统同步或时间计算的误差引起的。
总结:
在调试过程中发现,黄色线(表示预期的播放光标位置)和白色线(表示现实的翻转光标位置)应该在理论上紧密对齐。只管大部门时间它们确实对齐,但有时间由于系统同步问题,这两条线之间会出现轻微的偏差。因此,在调解中不绝确认这些线的相干性,以确保系统的准确同步。
绘制我们的 480 样本抖动窗口
详细复述与解释:
- 问题形貌:
注意到,在某些情况下,黄色光标(预期播放光标)和白色光标(现实翻转光标)之间的距离比预期的要远。这怀疑这种情况是否正常。
- 测量了在翻转过程中的400和80样本的抖动,这可能会导致差别。
- 为了进一步相识情况,考虑在图形上绘制一个400和80样本窗口,并使用“美丽的紫色”来表示这个播放窗口的颜色。
- 绘制窗口和观察:
- 绘制了一个包含400和80样本窗口的视图,希望能够清楚地看到播放窗口的变化。
- 声音缓冲区的输出字节每个样本会根据这个窗口巨细产生影响。
- 通过这些图形化的表示,能看到预期的光标位置与现实的翻转光标位置如何对齐。
- 分析观察结果:
- 从图形中可以看出,黄色线(预期的播放光标)和白色线(现实翻转光标)大多数时间是对齐的。
- 黄色线显示的是预期的播放光标位置,而白色线则表示翻转发生时的现实播放光标位置。
- 这两个光标应该总是尽可能靠近,只管大部门时间它们确实云云,但有时会出现不一致。
- 当这两个光标之间的距离过远时,这像是一个bug,即程序中存在某些问题。
- 疑虑与反思:
- 偶然的这种偏差可能是由于更新频率的变化或其他可变因素造成的,这可能是正常征象,也可能表明系统在某些地方存在缺陷。
- 只管这不是最终发布的代码,但仍希望能够看到更为准确的光标同步。
- 决定与后续举措:
- 虽然解说者对当前结果有些不满足,但他决定先停息对音频输出的美满调解,制止过多纠结于小细节。
- 他以为,等到有更多时间或更强大的渲染器时,再回头解决这些问题,可能会有更好的办法。
- 他决定临时竣事这个问题的调试,并把注意力转向其他使命。
- 总结:
- 虽然现在的音频输出同步存在肯定的问题,但在时间和资源允许的情况下,他会回过头来进一步优化代码,确保光标和音频输出更准确对齐。
- 最后,总结:如今的代码质量仍有提升空间,但他临时决定放弃进一步优化,并继承处理其他使命。





game.h
game.cpp
- #include "game.h"
- internal void GameOutputSound(game_sound_output_buffer *SoundBuffer, int ToneHz) {
- local_persist real32 tSine;
- int16 ToneVolume = 3000;
- int16 *SampleOut = SoundBuffer->Samples;
- int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz;
- // 循环写入样本到第一段区域
- for (int SampleIndex = 0; SampleIndex < SoundBuffer->SampleCount; ++SampleIndex) {
- real32 SineValue = sinf(tSine);
- int16 SampleValue = (int16)(SineValue * ToneVolume);
- *SampleOut++ = SampleValue; // 左声道
- *SampleOut++ = SampleValue; // 右声道
- tSine += 2.0f * (real32)Pi32 * 1.0f / (real32)WavePeriod;
- }
- }
- // 渲染一个奇异的渐变图案
- internal void RenderWeirdGradient(game_offscreen_buffer *Buffer, int BlueOffset,
- int GreenOffset) { // TODO:让我们看看优化器是怎么做的
- uint8 *Row = (uint8 *)Buffer->Memory; // 指向位图数据的起始位置
- for (int Y = 0; Y < Buffer->Height; ++Y) { // 遍历每一行
- uint32 *Pixel = (uint32 *)Row; // 指向每一行的起始像素
- for (int X = 0; X < Buffer->Width; ++X) { // 遍历每一列
- uint8 Blue = (uint8)(X + BlueOffset); // 计算蓝色分量
- uint8 Green = (uint8)(Y + GreenOffset); // 计算绿色分量
- *Pixel++ = ((Green << 8) | Blue); // 设置当前像素的颜色
- }
- Row += Buffer->Pitch; // 移动到下一行
- }
- }
- [[maybe_unused]] internal game_state *GameStartup(void) {
- game_state *GameState = new game_state;
- if (GameState) {
- GameState->BlueOffset = 0;
- GameState->GreenOffset = 0;
- GameState->ToneHz = 256;
- }
- return GameState;
- }
- [[maybe_unused]] internal void GameShutDown(game_state *GameState) { delete GameState; }
- // 游戏更新和渲染函数
- internal void GameUpdateAndRender(game_memory *Memory, //
- game_input *Input, //
- game_offscreen_buffer *Buffer) {
- Assert((&(Input->Controllers[0].Terminator) - &(Input->Controllers[0].Buttons[0])) ==
- (ArrayCount(Input->Controllers[0].Buttons)));
- Assert(sizeof(game_state) <= Memory->PermanentStorageSize);
- game_state *GameState = (game_state *)Memory->PermanentStorage;
- if (!Memory->Isinitialized) {
- // 定义一个常量字符指针,指向当前源文件的路径(__FILE__
- // 是一个预定义宏,表示当前源代码文件的文件名)
- const char *Filename = __FILE__;
- // 调用 DEBUGPlatformReadEntireFile 函数读取当前源文件的内容到内存
- debug_read_file_result File = DEBUGPlatformReadEntireFile(Filename);
- // 如果文件内容成功读取
- if (File.Contents) {
- // 将读取的文件内容写入到名为 "test.out" 的文件中
- // 传入参数:文件路径、内容大小、文件内容
- DEBUGPlatformWriteFileEntireFile("./test.out", // 输出文件的路径
- File.ContentsSize, // 文件内容的大小
- File.Contents); // 文件的内容
- // 写入文件后,释放分配的内存
- DEBUGPlatformFreeFileMemory(File.Contents); // 释放读取的文件内容的内存
- }
- GameState->ToneHz = 256;
- GameState->GreenOffset = 0;
- GameState->BlueOffset = 0;
- // TODO: 这可能更适合在平台层中执行
- Memory->Isinitialized = true;
- }
- for (int ControllerIndex = 0; ControllerIndex < ArrayCount(Input->Controllers);
- ++ControllerIndex) {
- // 获取第一个控制器的输入
- game_controller_input *Controller = GetController(Input, ControllerIndex);
- if (Controller->IsAnalog) {
- // 注释:使用模拟运动调节
- // 如果是模拟输入,根据控制器的 EndX 值调节音调频率(X轴的偏移量决定音调)
- GameState->ToneHz = 256 + (int)(128.0f * (Controller->StickAverageX));
- // 音调频率与输入的 EndX 值成比例变化
- GameState->BlueOffset += (int)4.0f * (int)(Controller->StickAverageY);
- // 根据 EndY 值调整蓝色分量偏移
- } else {
- // 注释:使用数字运动调节
- // 如果是数字输入,处理数字输入的按钮事件(目前不做调整)
- // 检查是否按下 "Down" 按钮,如果按下则增加绿色偏移量
- if (Controller->MoveLeft.EndedDown) {
- GameState->BlueOffset -= 1; // 增加绿色偏移量
- }
- if (Controller->MoveRight.EndedDown) {
- GameState->BlueOffset += 1;
- }
- }
- }
- // 渲染渐变效果,根据蓝色和绿色偏移量调整颜色
- RenderWeirdGradient(Buffer, GameState->BlueOffset, GameState->GreenOffset);
- }
- internal void GameGetSoundSamples(game_memory *Memory, //
- game_sound_output_buffer *SoundBuffer) {
- game_state *GameState = (game_state *)Memory->PermanentStorage;
- // 输出声音,根据 ToneHz 控制音调频率
- GameOutputSound(SoundBuffer, GameState->ToneHz);
- }
复制代码 win32_game.h
- #pragma once
- #include "game.h"
- #include <dsound.h>
- #include <minwindef.h>
- #include <windows.h>
- #include <winnt.h>
- #include <xinput.h>
- // 添加这个去掉重复的冗余代码
- struct win32_window_dimension {
- int Width;
- int Height;
- };
- struct win32_offscreen_buffer {
- BITMAPINFO Info;
- void *Memory;
- // 后备缓冲区的宽度和高度
- int Width;
- int Height;
- int Pitch;
- int BytesPerPixel;
- };
- struct win32_sound_output {
- // 音频测试
- uint32 RunningSampleIndex; // 样本索引
- int SamplesPerSecond; // 采样率:每秒采样48000次
- int BytesPerSample; // 一个样本的大小
- DWORD SecondaryBufferSize; // 缓冲区大小
- real32 tSine; // 保存当前的相位
- int LatencySampleCount;
- DWORD SafetyBytes; // 安全值表示游戏更新循环可能会变化的样本数量
- };
- struct win32_debug_time_marker {
- // 当前音频输出设备播放指针的位置,用于标识正在播放的音频缓冲区位置
- DWORD OutputPlayCursor;
- // 当前音频输出设备写入指针的位置,用于标识下一个需要填充音频数据的位置
- DWORD OutputWriteCursor;
- // 音频输出时的当前位置,用于跟踪音频数据的具体写入点(可能与 OutputWriteCursor
- // 类似,但用途更具体)
- DWORD OutputLocation;
- // 已写入到音频缓冲区的字节数,用于统计音频数据量或调试
- DWORD OutputByteCount;
- // 预期的翻转指针位置(下一帧或下一块缓冲区的播放起始位置)
- DWORD ExpectedFlipCursor;
- // 在屏幕翻转(图形帧切换)时,音频播放指针的位置,用于同步音频和图形的关系
- DWORD FlipPlayCursor;
- // 在屏幕翻转时,音频写入指针的位置,用于调试音频数据的写入延迟
- DWORD FlipWriteCursor;
- };
复制代码 win32_game.cpp
- // game.cpp : Defines the entry point for the application.
- //
- /**
- T这不是最终版本的平台层
- 1. 存档位置
- 2. 获取自己可执行文件的句柄
- 3. 资源加载路径
- 4. 线程(启动线程)
- 5. 原始输入(支持多个键盘)
- 6. Sleep/TimeBeginPeriod
- 7. ClipCursor()(多显示器支持)
- 8. 全屏支持
- 9. WM_SETCURSOR(控制光标可见性)
- 10. QueryCancelAutoplay
- 11. WM_ACTIVATEAPP(当我们不是活动应用程序时)
- 12. Blit速度优化(BitBlt)
- 13. 硬件加速(OpenGL或Direct3D或两者?)
- 14. GetKeyboardLayout(支持法语键盘、国际化WASD键支持)
- 只是一个部分清单
- */
- #include "win32_game.h"
- #include "game.h"
- #include <basetsd.h>
- #include <fileapi.h>
- #include <handleapi.h>
- #include <memoryapi.h>
- #include <minwindef.h>
- #include <winnt.h>
- #include <winuser.h>
- #include <xinput.h>
- // 释放文件读取的内存
- internal void DEBUGPlatformFreeFileMemory(void *Memory) {
- if (Memory) {
- // 调用 VirtualFree 释放内存
- VirtualFree(Memory, 0, MEM_RELEASE);
- }
- }
- // 读取整个文件内容到内存
- internal debug_read_file_result DEBUGPlatformReadEntireFile(const char *Filename) {
- // 打开文件以进行读取
- HANDLE FileHandle = CreateFileA(Filename, // 创建文件的文件名
- GENERIC_READ, // 只读访问权限
- FILE_SHARE_READ, // 允许其他进程读取文件
- 0, // 默认安全性,NULL使用默认设置
- OPEN_EXISTING, // 如果文件存在,则打开
- 0, // 文件属性
- 0); // 模板文件句柄
- debug_read_file_result Result = {}; // 初始化返回结果
- if (FileHandle != INVALID_HANDLE_VALUE) { // 如果文件成功打开
- LARGE_INTEGER FileSize;
- if (GetFileSizeEx(FileHandle, &FileSize)) { // 获取文件大小
- // TODO:为最大值定义宏,例如 Uin32Max
- Assert(FileSize.QuadPart <= 0xFFFFFFFF); // 确保文件大小不超过 32 位
- uint32 FileSize32 = SafeTruncateUInt64(FileSize.QuadPart); // 将文件大小转换为 32 位
- // 分配内存用于存储文件内容
- Result.Contents = VirtualAlloc(0, //
- FileSize32, // 文件大小
- MEM_RESERVE | MEM_COMMIT, // 保留并提交内存
- PAGE_READWRITE); // 可读写内存
- if (Result.Contents) { // 如果内存分配成功
- DWORD BytesRead;
- if (ReadFile(FileHandle, //
- Result.Contents, //
- FileSize32, // 要读取的字节数
- &BytesRead, // 读取的字节数
- 0) &&
- (FileSize32 == BytesRead)) { // 如果读取的字节数与文件大小相符
- // NOTE: 文件读取成功
- Result.ContentsSize = FileSize32; // 设置读取内容的大小
- } else {
- // TODO: 记录日志
- DEBUGPlatformFreeFileMemory(Result.Contents); // 释放内存
- Result.Contents = 0; // 清空指针
- }
- } else {
- // TODO: 记录日志
- }
- } else {
- // TODO: 记录日志
- }
- CloseHandle(FileHandle); // 关闭文件句柄
- } else {
- // TODO: 记录日志
- }
- return Result; // 返回结果,包含文件内容和大小
- }
- // 将内存内容写入文件
- internal void *DEBUGPlatformWriteFileEntireFile(const char *Filename, uint32 MemorySize,
- void *Memory) {
- void *Result = 0; // 默认结果为空
- // 创建或打开文件以进行写入
- HANDLE FileHandle = CreateFileA(Filename, //
- GENERIC_WRITE, // 写入权限
- 0, // 不共享文件
- 0, // 默认安全性
- CREATE_ALWAYS, // 如果文件存在,则覆盖
- 0, // 文件属性
- 0); // 模板文件句柄
- if (FileHandle != INVALID_HANDLE_VALUE) { // 如果文件成功打开
- bool32 WriteResult = false;
- DWORD BytesToWritten;
- if (WriteFile(FileHandle, //
- Memory, // 要写入的内存内容
- MemorySize, // 写入的字节数
- &BytesToWritten, // 实际写入的字节数
- 0)) { // 如果写入成功
- // NOTE: 文件写入成功
- WriteResult = (BytesToWritten == MemorySize); // 确保写入字节数与预期一致
- } else {
- // TODO: 记录日志
- }
- } else {
- // TODO: 记录日志
- }
- return Result; // 返回结果,写入成功则返回文件句柄,否则返回空
- }
- #include "game.cpp"
- // TODO: 全局变量
- // 用于控制程序运行的全局布尔变量,通常用于循环条件
- global_variable bool GlobalRunning;
- global_variable bool GlobalPause;
- // 用于存储屏幕缓冲区的全局变量
- global_variable win32_offscreen_buffer GlobalBackbuffer;
- global_variable LPDIRECTSOUNDBUFFER GlobalSecondaryBuffer;
- global_variable int64 GlobalPerfCountFrequency;
- /**
- * @param dwUserIndex // 与设备关联的玩家索引
- * @param pState // 接收当前状态的结构体
- */
- #define X_INPUT_GET_STATE(name) \
- DWORD WINAPI name([[maybe_unused]] DWORD dwUserIndex, \
- [[maybe_unused]] XINPUT_STATE *pState) // 定义一个宏,将指定名称设置为
- // XInputGetState 函数的类型定义
- /**
- * @param dwUserIndex // 与设备关联的玩家索引
- * @param pVibration // 要发送到控制器的震动信息
- */
- #define X_INPUT_SET_STATE(name) \
- DWORD WINAPI name( \
- [[maybe_unused]] DWORD dwUserIndex, \
- [[maybe_unused]] XINPUT_VIBRATION *pVibration) // 定义一个宏,将指定名称设置为
- // XInputSetState 函数的类型定义
- typedef X_INPUT_GET_STATE(x_input_get_state);
- // 定义了 x_input_get_state 类型,为 `XInputGetState` 函数的类型
- typedef X_INPUT_SET_STATE(x_input_set_state);
- // 定义了 x_input_set_state 类型,为 `XInputSetState` 函数的类型
- // 定义一个 XInputGetState 的打桩函数,返回值为
- // ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
- X_INPUT_GET_STATE(XInputGetStateStub) { //
- return (ERROR_DEVICE_NOT_CONNECTED);
- }
- // 定义一个 XInputSetState 的打桩函数,返回值为
- // ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
- X_INPUT_SET_STATE(XInputSetStateStub) { //
- return (ERROR_DEVICE_NOT_CONNECTED);
- }
- // 设置全局变量 XInputGetState_ 和 XInputSetState_ 的初始值为打桩函数
- global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
- global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;
- // 定义宏将 XInputGetState 和 XInputSetState 重新指向 XInputGetState_ 和
- // XInputSetState_
- #define XInputGetState XInputGetState_
- #define XInputSetState XInputSetState_
- // 加载 XInput DLL 并获取函数地址
- internal void Win32LoadXInput(void) { //
- HMODULE XInputLibrary = LoadLibrary("xinput1_4.dll");
- if (!XInputLibrary) {
- // 如果无法加载 xinput1_4.dll,则回退到 xinput1_3.dll
- XInputLibrary = LoadLibrary("xinput1_3.dll");
- } else {
- // TODO:Diagnostic
- }
- if (XInputLibrary) { // 检查库是否加载成功
- XInputGetState = (x_input_get_state *)GetProcAddress(
- XInputLibrary, "XInputGetState"); // 获取 XInputGetState 函数地址
- if (!XInputGetState) { // 如果获取失败,使用打桩函数
- XInputGetState = XInputGetStateStub;
- }
- XInputSetState = (x_input_set_state *)GetProcAddress(
- XInputLibrary, "XInputSetState"); // 获取 XInputSetState 函数地址
- if (!XInputSetState) { // 如果获取失败,使用打桩函数
- XInputSetState = XInputSetStateStub;
- }
- } else {
- // TODO:Diagnostic
- }
- }
- #define DIRECT_SOUND_CREATE(name) \
- HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS, LPUNKNOWN pUnkOuter);
- // 定义一个宏,用于声明 DirectSound 创建函数的原型
- typedef DIRECT_SOUND_CREATE(direct_sound_create);
- // 定义一个类型别名 direct_sound_create,代表
- // DirectSound 创建函数
- internal void Win32InitDSound(HWND window, int32 SamplesPerSecond, int32 BufferSize) {
- // 注意: 加载 dsound.dll 动态链接库
- HMODULE DSoundLibrary = LoadLibraryA("dsound.dll");
- if (DSoundLibrary) {
- // 注意: 获取 DirectSound 创建函数的地址
- // 通过 GetProcAddress 函数查找 "DirectSoundCreate" 函数在 dsound.dll
- // 中的地址,并将其转换为 direct_sound_create 类型的函数指针
- direct_sound_create *DirectSoundCreate =
- (direct_sound_create *)GetProcAddress(DSoundLibrary, "DirectSoundCreate");
- // 定义一个指向 IDirectSound 接口的指针,并初始化为 NULL
- IDirectSound *DirectSound = NULL;
- if (DirectSoundCreate &&
- SUCCEEDED(DirectSoundCreate(0,
- // 传入 0 作为设备 GUID,表示使用默认音频设备
- &DirectSound,
- // 将创建的 DirectSound 对象的指针存储到
- // DirectSound 变量中
- 0
- // 传入 0 作为外部未知接口指针,通常为 NULL
- ))) //
- {
- // clang-format off
- WAVEFORMATEX WaveFormat = {};
- WaveFormat.wFormatTag = WAVE_FORMAT_PCM; // 设置格式标签为 WAVE_FORMAT_PCM,表示使用未压缩的 PCM 格式
- WaveFormat.nChannels = 2; // 设置声道数为 2,表示立体声(两个声道:左声道和右声道)
- WaveFormat.nSamplesPerSec = SamplesPerSecond; // 采样率 表示每秒钟的样本数,常见值为 44100 或 48000 等
- WaveFormat.wBitsPerSample = 16; // 16位音频 设置每个样本的位深为 16 位
- WaveFormat.nBlockAlign = (WaveFormat.nChannels * WaveFormat.wBitsPerSample) / 8;
- // 计算数据块对齐大小,公式为:nBlockAlign = nChannels * (wBitsPerSample / 8)
- // 这里除以 8 是因为每个样本的大小是按字节来计算的,nChannels 是声道数
- // wBitsPerSample 是每个样本的位数,除以 8 转换为字节
- WaveFormat.nAvgBytesPerSec = WaveFormat.nSamplesPerSec * WaveFormat.nBlockAlign;
- // 计算每秒的平均字节数,公式为:nAvgBytesPerSec = nSamplesPerSec * nBlockAlign
- // 这表示每秒音频数据流的字节数,它帮助估算缓冲区大小
- // clang-format on
- // 函数用于设置 DirectSound 的协作等级
- if (SUCCEEDED(DirectSound->SetCooperativeLevel(window, DSSCL_PRIORITY))) {
- // 注意: 创建一个主缓冲区
- // 使用 DirectSoundCreate 函数创建一个 DirectSound
- // 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充
- DSBUFFERDESC BufferDescription = {};
- BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小
- // dwFlags:设置为
- // DSBCAPS_PRIMARYBUFFER,指定我们要创建的是主缓冲区,而不是次缓冲区。
- BufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;
- LPDIRECTSOUNDBUFFER PrimaryBuffer = NULL;
- if (SUCCEEDED(DirectSound->CreateSoundBuffer(
- &BufferDescription, // 指向缓冲区描述结构体的指针
- &PrimaryBuffer, // 指向创建的缓冲区对象的指针
- NULL // 外部未知接口,通常传入 NULL
- ))) {
- if (SUCCEEDED(PrimaryBuffer->SetFormat(&WaveFormat))) {
- // NOTE:we have finally set the format
- OutputDebugString("SetFormat success");
- } else {
- // NOTE:
- OutputDebugString("SetFormat failure");
- }
- } else {
- }
- } else {
- }
- // 注意: 创建第二个缓冲区
- // 创建次缓冲区来承载音频数据,并在播放时使用
- // 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充
- DSBUFFERDESC BufferDescription = {};
- BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小
- // dwFlags:设置为
- // DSBCAPS_GETCURRENTPOSITION2 |
- // DSBCAPS_GLOBALFOCUS两个标志会使次缓冲区在播放时更加精确,同时在应用失去焦点时保持音频输出
- BufferDescription.dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS;
- BufferDescription.dwBufferBytes = BufferSize; // 缓冲区大小
- BufferDescription.lpwfxFormat = &WaveFormat; // 指向音频格式的指针
- if (SUCCEEDED(DirectSound->CreateSoundBuffer(
- &BufferDescription, // 指向缓冲区描述结构体的指针
- &GlobalSecondaryBuffer, // 指向创建的缓冲区对象的指针
- NULL // 外部未知接口,通常传入 NULL
- ))) {
- OutputDebugString("SetFormat success");
- } else {
- OutputDebugString("SetFormat failure");
- }
- // 注意: 开始播放!
- // 调用相应的 DirectSound API 开始播放音频
- } else {
- }
- } else {
- }
- }
- // 处理并映射摇杆的输入值
- internal real32 Win32ProcessXinputStickValue(SHORT Value, SHORT DeadZoneThreshold) {
- real32 Result = 0;
- // 检查当前摇杆值是否小于负的死区阈值
- if (Value < -DeadZoneThreshold) {
- // 如果摇杆值在负方向超过死区,将值映射到 -1.0 到 0 的范围
- // (摇杆值 + 死区阈值) 以死区边界为起点进行线性映射
- Result = (real32)(Value + DeadZoneThreshold) / (32768.0f - DeadZoneThreshold);
- }
- // 检查当前摇杆值是否大于正的死区阈值
- else if (Value > DeadZoneThreshold) {
- // 如果摇杆值在正方向超过死区,将值映射到 0 到 1.0 的范围
- // (摇杆值 - 死区阈值) 以死区边界为起点进行线性映射
- Result = (real32)(Value - DeadZoneThreshold) / (32767.0f - DeadZoneThreshold);
- }
- // 返回处理后的映射值,范围为 -1.0 到 1.0
- return Result;
- }
- // 处理单个按键的状态更新
- internal void Win32ProcessKeyboardMessage(game_button_state *NewState, bool32 IsDown) {
- Assert(NewState->EndedDown != IsDown);
- // 更新按钮的状态(是否按下)
- NewState->EndedDown = IsDown; // 将按钮的状态设置为按下(IsDown 为
- // true)或松开(IsDown 为 false)
- // 增加按键状态变化的计数
- ++NewState->HalfTransitionCount; // 每次按键状态变化时,半次状态转换计数增加 1
- }
- // 处理 XInput 数字按钮状态的函数
- // XInputButtonState: 当前帧的按钮状态(位掩码表示多个按钮状态)
- // OldState: 上一帧的按钮状态
- // ButtonBit: 要检测的具体按钮位
- // NewState: 当前帧更新后的按钮状态
- internal void Win32ProcessXInputDigitalButton(DWORD XInputButtonState, game_button_state *OldState,
- DWORD ButtonBit, game_button_state *NewState) {
- // 判断当前按钮是否处于按下状态
- NewState->EndedDown = ((XInputButtonState & ButtonBit) == ButtonBit);
- // 如果按钮的按下状态发生变化,增加过渡计数
- NewState->HalfTransitionCount = (OldState->EndedDown != NewState->EndedDown) ? 1 : 0;
- }
- internal win32_window_dimension Win32GetWindowDimension(HWND Window) {
- win32_window_dimension Result;
- RECT ClientRect;
- GetClientRect(Window, &ClientRect);
- // 计算绘制区域的宽度和高度
- Result.Height = ClientRect.bottom - ClientRect.top;
- Result.Width = ClientRect.right - ClientRect.left;
- return Result;
- }
- // 这个函数用于重新调整 DIB(设备独立位图)大小
- internal void Win32ResizeDIBSection(win32_offscreen_buffer *Buffer, int width, int height) {
- // device independent bitmap(设备独立位图)
- // TODO: 进一步优化代码的健壮性
- // 可能的改进:先不释放,先尝试其他方法,再如果失败再释放。
- if (Buffer->Memory) {
- VirtualFree(Buffer->Memory, // 指定要释放的内存块起始地址
- 0, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0
- MEM_RELEASE); // MEM_RELEASE:释放整个内存块,将内存和地址空间都归还给操作系统
- }
- // 赋值后备缓冲的宽度和高度
- Buffer->Width = width;
- Buffer->Height = height;
- Buffer->BytesPerPixel = 4;
- // 设置位图信息头(BITMAPINFOHEADER)
- Buffer->Info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // 位图头大小
- Buffer->Info.bmiHeader.biWidth = Buffer->Width; // 设置位图的宽度
- Buffer->Info.bmiHeader.biHeight = -Buffer->Height; // 设置位图的高度(负号表示自上而下的方向)
- Buffer->Info.bmiHeader.biPlanes = 1; // 设置颜色平面数,通常为 1
- Buffer->Info.bmiHeader.biBitCount = 32; // 每像素的位数,这里为 32 位(即 RGBA)
- Buffer->Info.bmiHeader.biCompression = BI_RGB; // 无压缩,直接使用 RGB 颜色模式
- // 创建 DIBSection(设备独立位图)并返回句柄
- // TODO:我们可以自己分配?
- int BitmapMemorySize = (Buffer->Width * Buffer->Height) * Buffer->BytesPerPixel;
- Buffer->Memory = VirtualAlloc(
- 0, // lpAddress:指定内存块的起始地址。
- // 通常设为 NULL,由系统自动选择一个合适的地址。
- BitmapMemorySize, // 要分配的内存大小,单位是字节。
- MEM_COMMIT, // 分配物理内存并映射到虚拟地址。已提交的内存可以被进程实际访问和操作。
- PAGE_READWRITE // 内存可读写
- );
- Buffer->Pitch = width * Buffer->BytesPerPixel; // 每一行的字节数
- // TODO:可能会把它清除成黑色
- }
- // 这个函数用于将 DIBSection 绘制到窗口设备上下文
- internal void Win32DisplayBufferInWindow(HDC DeviceContext, int WindowWidth, int WindowHeight,
- win32_offscreen_buffer Buffer) {
- // 使用 StretchDIBits 将 DIBSection 绘制到设备上下文中
- StretchDIBits(DeviceContext, // 目标设备上下文(窗口或屏幕的设备上下文)
- /*
- X, Y, Width, Height, // 目标区域的 x, y 坐标及宽高
- X, Y, Width, Height,
- */
- 0, 0, WindowWidth, WindowHeight, //
- 0, 0, Buffer.Width, Buffer.Height, //
- // 源区域的 x, y 坐标及宽高(此处源区域与目标区域相同)
- Buffer.Memory, // 位图内存指针,指向 DIBSection 数据
- &Buffer.Info, // 位图信息,包含位图的大小、颜色等信息
- DIB_RGB_COLORS, // 颜色类型,使用 RGB 颜色
- SRCCOPY); // 使用 SRCCOPY 操作符进行拷贝(即源图像直接拷贝到目标区域)
- }
- LRESULT CALLBACK Win32MainWindowCallback(HWND hwnd, // 窗口句柄,表示消息来源的窗口
- UINT Message, // 消息标识符,表示当前接收到的消息类型
- WPARAM wParam, // 与消息相关的附加信息,取决于消息类型
- LPARAM LParam) { // 与消息相关的附加信息,取决于消息类型
- LRESULT Result = 0; // 定义一个变量来存储消息处理的结果
- switch (Message) { // 根据消息类型进行不同的处理
- case WM_CREATE: {
- OutputDebugStringA("WM_CREATE\n");
- };
- case WM_SIZE: { // 窗口大小发生变化时的消息
- } break;
- case WM_DESTROY: { // 窗口销毁时的消息
- // TODO: 处理错误,用重建窗口
- GlobalRunning = false;
- } break;
- case WM_SYSKEYDOWN: // 系统按键按下消息,例如 Alt 键组合。
- case WM_SYSKEYUP: // 系统按键释放消息。
- case WM_KEYDOWN: // 普通按键按下消息。
- case WM_KEYUP: { // 普通按键释放消息。
- Assert(!"键盘输入通过非分发消息到达!!");
- } break;
- case WM_CLOSE: { // 窗口关闭时的消息
- // TODO: 像用户发送消息进行处理
- GlobalRunning = false;
- } break;
- case WM_ACTIVATEAPP: { // 应用程序激活或失去焦点时的消息
- OutputDebugStringA("WM_ACTIVATEAPP\n"); // 输出调试信息,表示应用程序激活或失去焦点
- } break;
- case WM_PAINT: { // 处理 WM_PAINT 消息,通常在窗口需要重新绘制时触发
- PAINTSTRUCT Paint; // 定义一个 PAINTSTRUCT 结构体,保存绘制的信息
- // 调用 BeginPaint 开始绘制,并获取设备上下文 (HDC),同时填充 Paint 结构体
- HDC DeviceContext = BeginPaint(hwnd, &Paint);
- // 获取当前绘制区域的左上角坐标
- win32_window_dimension Dimension = Win32GetWindowDimension(hwnd);
- Win32DisplayBufferInWindow(DeviceContext, Dimension.Width, Dimension.Height,
- GlobalBackbuffer);
- // 调用 EndPaint 结束绘制,并释放设备上下文
- EndPaint(hwnd, &Paint);
- } break;
- default: { // 对于不处理的消息,调用默认的窗口过程
- Result = DefWindowProc(hwnd, Message, wParam, LParam);
- // 调用默认窗口过程处理消息
- } break;
- }
- return Result; // 返回处理结果
- }
- internal void Win32ClearBuffer(win32_sound_output *SoundOutput) {
- VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址
- DWORD Region1Size; // 第一段区域的大小(字节数)
- VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址
- DWORD Region2Size; // 第二段区域的大小(字节数)
- if (SUCCEEDED(GlobalSecondaryBuffer->Lock(
- 0, // 缓冲区偏移量,指定开始锁定的字节位置
- SoundOutput->SecondaryBufferSize, // 锁定的字节数,指定要锁定的区域大小
- &Region1, // 输出,返回锁定区域的内存指针(第一个区域)
- &Region1Size, // 输出,返回第一个锁定区域的实际字节数
- &Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)
- &Region2Size, // 输出,返回第二个锁定区域的实际字节数
- 0 // 标志,控制锁定行为(如从光标位置锁定等)
- ))) {
- int8 *DestSample = (int8 *)Region1; // 将第一段区域指针转换为 16
- // 位整型指针,准备写入样本数据
- // 循环写入样本到第一段区域
- for (DWORD ByteIndex = 0; ByteIndex < Region1Size; ++ByteIndex) {
- *DestSample++ = 0;
- }
- for (DWORD ByteIndex = 0; ByteIndex < Region2Size; ++ByteIndex) {
- *DestSample++ = 0;
- }
- GlobalSecondaryBuffer->Unlock(Region1, Region1Size, //
- Region2, Region2Size);
- }
- }
- internal void Win32FillSoundBuffer(win32_sound_output *SoundOutput, DWORD ByteToLock,
- DWORD BytesToWrite, game_sound_output_buffer *SourceBuffer) {
- VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址
- DWORD Region1Size; // 第一段区域的大小(字节数)
- VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址
- DWORD Region2Size; // 第二段区域的大小(字节数)
- if (SUCCEEDED(GlobalSecondaryBuffer->Lock(
- ByteToLock, // 缓冲区偏移量,指定开始锁定的字节位置
- BytesToWrite, // 锁定的字节数,指定要锁定的区域大小
- &Region1, // 输出,返回锁定区域的内存指针(第一个区域)
- &Region1Size, // 输出,返回第一个锁定区域的实际字节数
- &Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)
- &Region2Size, // 输出,返回第二个锁定区域的实际字节数
- 0 // 标志,控制锁定行为(如从光标位置锁定等)
- ))) {
- // int16 int16 int16
- // 左 右 左 右 左 右 左 右 左 右
- DWORD Region1SampleCount =
- Region1Size / SoundOutput->BytesPerSample; // 计算第一段区域中的样本数量
- int16 *DestSample = (int16 *)Region1; // 将第一段区域指针转换为 16
- // 位整型指针,准备写入样本数据
- int16 *SourceSample = SourceBuffer->Samples;
- // 循环写入样本到第一段区域
- for (DWORD SampleIndex = 0; SampleIndex < Region1SampleCount; ++SampleIndex) {
- *DestSample++ = *SourceSample++; // 左声道
- *DestSample++ = *SourceSample++; // 右声道
- SoundOutput->RunningSampleIndex++;
- }
- DWORD Region2SampleCount =
- Region2Size / SoundOutput->BytesPerSample; // 计算第二段区域中的样本数量
- DestSample = (int16 *)Region2; // 将第二段区域指针转换为 16
- // 位整型指针,准备写入样本数据
- // 循环写入样本到第二段区域
- for (DWORD SampleIndex = 0; SampleIndex < Region2SampleCount; ++SampleIndex) {
- // 使用相同逻辑生成方波样本数据
- *DestSample++ = *SourceSample++; // 左声道
- *DestSample++ = *SourceSample++; // 右声道
- SoundOutput->RunningSampleIndex++;
- }
- // 解锁音频缓冲区,将数据提交给音频设备
- GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2, Region2Size);
- }
- }
- // 处理并分派待处理的消息
- internal void Win32ProcessPendingMessages(game_controller_input *KeyboardController) {
- MSG Message; // 声明一个 MSG 结构体,用于接收消息
- // 使用 PeekMessage 从消息队列中获取消息
- while (PeekMessage(
- &Message, // 指向一个 `MSG` 结构的指针。`PeekMessage` 将在 `lpMsg`
- // 中填入符合条件的消息内容。
- 0, // `hWnd`
- // 为`NULL`,则检查当前线程中所有窗口的消息;如果设置为特定的窗口句柄,则只检查该窗口的消息。
- 0, // 用于设定消息类型的范围
- 0, // 用于设定消息类型的范围
- PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。
- )) {
- // 根据消息的不同类型进行处理
- switch (Message.message) {
- case WM_QUIT: { // 退出消息
- GlobalRunning = false; // 设置全局标志为 false,停止程序
- } break;
- // 处理键盘相关的消息
- case WM_SYSKEYDOWN:
- case WM_SYSKEYUP:
- case WM_KEYDOWN:
- case WM_KEYUP: {
- uint64 VKCode = Message.wParam; // 获取虚拟键码
- // 判断键盘按键在前一状态时是否已按下
- bool32 WasDown = ((Message.lParam & (1 << 30)) != 0);
- // 判断键盘按键当前是否被按下
- bool32 IsDown = ((Message.lParam & (1 << 31)) == 0);
- // 如果按键的状态发生变化(即按下/松开),则处理键盘输入
- if (IsDown != WasDown) {
- // 针对不同的按键处理对应操作
- if (VKCode == 'W') { // 按下 'W' 键时的处理
- Win32ProcessKeyboardMessage(&KeyboardController->MoveUp, IsDown);
- } else if (VKCode == 'A') { // 按下 'A' 键时的处理
- Win32ProcessKeyboardMessage(&KeyboardController->MoveLeft, IsDown);
- } else if (VKCode == 'S') { // 按下 'S' 键时的处理
- Win32ProcessKeyboardMessage(&KeyboardController->MoveDown, IsDown);
- } else if (VKCode == 'D') { // 按下 'D' 键时的处理
- Win32ProcessKeyboardMessage(&KeyboardController->MoveRight, IsDown);
- } else if (VKCode == 'Q') { // 按下 'Q' 键时,处理左肩按钮
- Win32ProcessKeyboardMessage(&KeyboardController->LeftShoulder, IsDown);
- } else if (VKCode == 'E') { // 按下 'E' 键时,处理右肩按钮
- Win32ProcessKeyboardMessage(&KeyboardController->RightShoulder, IsDown);
- } else if (VKCode == VK_UP) { // 按下上箭头时,处理上键
- Win32ProcessKeyboardMessage(&KeyboardController->ActionUp, IsDown);
- } else if (VKCode == VK_DOWN) { // 按下下箭头时,处理下键
- Win32ProcessKeyboardMessage(&KeyboardController->ActionDown, IsDown);
- } else if (VKCode == VK_LEFT) { // 按下左箭头时,处理左键
- Win32ProcessKeyboardMessage(&KeyboardController->ActionLeft, IsDown);
- } else if (VKCode == VK_RIGHT) { // 按下右箭头时,处理右键
- Win32ProcessKeyboardMessage(&KeyboardController->ActionRight, IsDown);
- } else if (VKCode == VK_ESCAPE) { // 按下 'ESC' 键时,退出程序
- GlobalRunning = false;
- } else if (VKCode == VK_SPACE) {
- }
- #if GAME_INTERNAL
- else if (VKCode == 'P') {
- if (IsDown) {
- // 按下P键时的处理(此处暂无特定处理)
- GlobalPause = !GlobalPause;
- }
- }
- #endif
- }
- // 如果按下了 Alt + F4,退出程序
- bool32 AltKeyWasDown = (Message.lParam & (1 << 29));
- // 判断 Alt 键是否被按下
- if ((VKCode == VK_F4) && AltKeyWasDown) {
- // 如果按下的是 F4 键并且 Alt 键按下,退出程序
- GlobalRunning = false;
- }
- break;
- }
- // 其他消息的处理(如鼠标、窗口消息等)
- default: {
- TranslateMessage(&Message); // 翻译消息,如果是键盘消息需要翻译
- DispatchMessage(&Message); // 分派消息,调用窗口过程处理消息
- break;
- }
- }
- }
- }
- // 获取当前的高精度计时器值
- inline LARGE_INTEGER Win32GetWallClock(void) {
- LARGE_INTEGER Result;
- // 调用 Windows API QueryPerformanceCounter 来获取当前的计时器值
- QueryPerformanceCounter(&Result);
- return Result;
- }
- // 计算两个时间点之间经过的秒数
- inline real32 Win32GetSecondsElapsed(LARGE_INTEGER Start, LARGE_INTEGER End) {
- // 计算经过的时钟周期差,转换为秒
- real32 Result = ((real32)(End.QuadPart - Start.QuadPart) / (real32)GlobalPerfCountFrequency);
- return Result;
- }
- // 这个函数用于在屏幕缓冲区上绘制一条垂直线
- // 它通过直接操作像素内存来实现绘图,因此非常高效
- internal void Win32DebugDrawVertical(win32_offscreen_buffer *Buffer, //
- int X, // 垂直线的X坐标
- int Top, // 垂直线的起始Y坐标
- int Bottom, // 垂直线的结束Y坐标
- uint32 Color) { // 垂直线的颜色
- // 确保Top的值不超出屏幕缓冲区的顶部边界
- if (Top < 0) {
- Top = 0; // 若Top小于0,则将其限制为0
- }
- // 确保Bottom的值不超出屏幕缓冲区的底部边界
- if (Bottom > Buffer->Height) {
- Bottom = Buffer->Height; // 若Bottom超出高度,则将其限制为屏幕的最大高度
- }
- // 确保X坐标在屏幕缓冲区的宽度范围内
- if ((X >= 0) && (X < Buffer->Width)) { // 注意条件为 X < Buffer->Width,而非 <=
- // 计算起始像素的位置,位于屏幕缓冲区内存的 (X, Top)
- uint8 *Pixel = ((uint8 *)Buffer->Memory + // 缓冲区内存的起始地址
- X * Buffer->BytesPerPixel + // 加上X列的偏移量
- Top * Buffer->Pitch); // 加上Y行的偏移量
- // 遍历从Top到Bottom的每个像素行,绘制垂直线
- for (int Y = Top; Y < Bottom; ++Y) {
- // 设置当前像素的颜色,颜色格式假设为0xAARRGGBB(32位颜色)
- *(uint32 *)Pixel = Color;
- // 移动指针到下一行的同一列
- Pixel += Buffer->Pitch;
- }
- }
- }
- // 这个函数用于绘制声音缓冲区的标记
- inline void Win32DrawSoundBufferMarker(win32_offscreen_buffer *Buffer, //
- [[maybe_unused]] win32_sound_output *SoundOutput, //
- real32 C, int PadX, int Top, int Bottom, DWORD Value,
- uint32 Color) {
- real32 XReal32 = (C * (real32)Value); // 根据声音输出值计算X坐标
- int X = PadX + (int)XReal32; // 加上边距,得到最终X坐标
- // 绘制指定位置的垂直线
- Win32DebugDrawVertical(Buffer, X, Top, Bottom, Color);
- }
- #if GAME_INTERNAL
- // 这个函数用于调试时在屏幕上同步显示音频缓冲区的状态和标记
- internal void Win32DebugSyncDisplay(
- win32_offscreen_buffer *Buffer, // 用于绘制的屏幕缓冲区
- int MarkerCount, // 标记数量
- win32_debug_time_marker *Markers, // 包含所有标记的数组
- int CurrentMarkerIndex, // 当前正在处理的标记索引
- win32_sound_output *SoundOutput, // 声音输出信息(例如缓冲区大小)
- [[maybe_unused]] real32 TargetSecondsPerFrame) { // 每帧目标时间,未使用
- // 定义绘制区域的边距和高度
- int PadX = 16; // 左边距
- int PadY = 16; // 上边距
- int lineHeight = 64; // 每条显示线的高度
- // 计算每个音频缓冲字节对应的像素宽度
- real32 C = (real32)Buffer->Width / (real32)SoundOutput->SecondaryBufferSize;
- // 遍历所有标记
- for (int MarkerIndex = 0; //
- MarkerIndex < MarkerCount; // 根据标记数量进行循环
- ++MarkerIndex) {
- win32_debug_time_marker *ThisMarker = &Markers[MarkerIndex]; // 当前标记指针
- // 确保每个光标值都在音频缓冲区范围内,避免无效数据或越界
- Assert(ThisMarker->OutputPlayCursor < SoundOutput->SecondaryBufferSize);
- Assert(ThisMarker->OutputWriteCursor < SoundOutput->SecondaryBufferSize);
- Assert(ThisMarker->OutputLocation < SoundOutput->SecondaryBufferSize);
- Assert(ThisMarker->OutputByteCount < SoundOutput->SecondaryBufferSize);
- Assert(ThisMarker->FlipPlayCursor < SoundOutput->SecondaryBufferSize);
- Assert(ThisMarker->FlipWriteCursor < SoundOutput->SecondaryBufferSize);
- // 初始化当前标记的绘制区域
- int Top = PadY; // 区域顶部
- int Bottom = PadY + lineHeight; // 区域底部
- // 定义各种状态指针的颜色
- DWORD PlayColor = 0xFFFFFFFF; // 白色 - 播放光标
- DWORD WriteColor = 0xFFFF0000; // 红色 - 写入光标
- DWORD ExpectedFlipColor = 0xFF00FF00; // 绿色 - 预期翻转光标
- DWORD PlayWindowColor = 0xFFFF00FF; // 紫色 - 播放窗口光标
- // 如果当前标记是正在处理的标记,绘制额外信息
- if (CurrentMarkerIndex == MarkerIndex) {
- Top += lineHeight + PadY; // 向下偏移绘制区域
- Bottom += lineHeight + PadY;
- int FirstTop = Top; // 记录顶部位置以用于多条线绘制
- // 绘制 OutputPlayCursor
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->OutputPlayCursor,
- PlayColor); // 白色表示播放光标
- // 绘制 OutputWriteCursor
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->OutputWriteCursor,
- WriteColor); // 红色表示写入光标
- Top += lineHeight + PadY; // 向下偏移
- Bottom += lineHeight + PadY;
- // 绘制 OutputLocation
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->OutputLocation,
- PlayColor); // 白色表示当前写入位置
- // 绘制 OutputLocation + OutputByteCount(写入范围结束位置)
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->OutputLocation + ThisMarker->OutputByteCount,
- WriteColor); // 红色表示写入范围结束位置
- Top += lineHeight + PadY; // 再次向下偏移
- Bottom += lineHeight + PadY;
- // 绘制 ExpectedFlipCursor
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, FirstTop, Bottom,
- ThisMarker->ExpectedFlipCursor,
- ExpectedFlipColor); // 绿色表示预期翻转位置
- }
- // 绘制 FlipPlayCursor
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->FlipPlayCursor, PlayColor); // 白色表示播放光标
- // 绘制 FlipPlayCursor + 播放窗口偏移
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->FlipPlayCursor + 480 * SoundOutput->BytesPerSample,
- PlayWindowColor); // 紫色表示播放窗口
- // 绘制 FlipWriteCursor
- Win32DrawSoundBufferMarker(Buffer, SoundOutput, C, PadX, Top, Bottom,
- ThisMarker->FlipWriteCursor,
- WriteColor); // 红色表示写入光标
- }
- }
- #endif
- int CALLBACK WinMain(HINSTANCE hInst, [[maybe_unused]] HINSTANCE hInstPrev, //
- [[maybe_unused]] PSTR cmdline, [[maybe_unused]] int cmdshow) {
- LARGE_INTEGER PerfCountFrequencyResult;
- QueryPerformanceFrequency(&PerfCountFrequencyResult);
- GlobalPerfCountFrequency = PerfCountFrequencyResult.QuadPart;
- // NOTE: 将Windows调度器的粒度设置为1毫秒,以使我们的 `sleep()` 函数更加精细化。
- UINT DesiredSchedulerMS = 1;
- bool32 SleepIsGranular = (timeBeginPeriod(DesiredSchedulerMS) == TIMERR_NOERROR);
- Win32LoadXInput(); // 加载 XInput 库,用于处理 Xbox 控制器输入
- WNDCLASS WindowClass = {}; // 初始化窗口类结构,默认值为零
- // 使用大括号初始化,所有成员都被初始化为零(0)或 nullptr
- Win32ResizeDIBSection(&GlobalBackbuffer, 1280,
- 720); // 调整 DIB(设备独立位图)大小
- // WindowClass.style:表示窗口类的样式。通常设置为一些 Windows
- // 窗口样式标志(例如 CS_HREDRAW, CS_VREDRAW)。
- WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
- // CS_HREDRAW 当窗口的宽度发生变化时,窗口会被重绘。
- // CS_VREDRAW 当窗口的高度发生变化时,窗口会被重绘
- // WindowClass.lpfnWndProc:指向窗口过程函数的指针,窗口过程用于处理与窗口相关的消息。
- WindowClass.lpfnWndProc = Win32MainWindowCallback;
- // WindowClass.hInstance:指定当前应用程序的实例句柄,Windows
- // 应用程序必须有一个实例句柄。
- WindowClass.hInstance = hInst;
- // WindowClass.lpszClassName:指定窗口类的名称,通常用于创建窗口时注册该类。
- WindowClass.lpszClassName = "gameWindowClass"; // 类名
- // TODO:我们如何在 Windows 上可靠地查询这个?
- // 定义显示器的刷新率(每秒钟刷新次数)
- #define MonitorRefreshHz 60 // 显示器刷新率为 60Hz(每秒钟刷新 60 次)
- // 定义游戏更新的频率(每秒钟更新次数),它是显示器刷新率的一半
- #define GameUpdateHz ((MonitorRefreshHz) / 2) // 游戏更新频率为显示器刷新率的一半,即 30Hz
- real32 TargetSecondsPerFrame = 1.0f / (real32)GameUpdateHz;
- if (RegisterClass(&WindowClass)) { //
- // 如果窗口类注册成功
- HWND Window = CreateWindowEx(
- 0, // 创建窗口,使用扩展窗口风格
- WindowClass.lpszClassName, // 窗口类的名称,指向已注册的窗口类
- "game", // 窗口标题(窗口的名称)
- WS_OVERLAPPEDWINDOW | WS_VISIBLE, // 窗口样式:重叠窗口(带有菜单、边框等)并且可见
- CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(X坐标)
- CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(Y坐标)
- CW_USEDEFAULT, // 窗口的初始宽度:使用默认宽度
- CW_USEDEFAULT, // 窗口的初始高度:使用默认高度
- 0, // 父窗口句柄(此处无父窗口,传0)
- 0, // 菜单句柄(此处没有菜单,传0)
- hInst, // 当前应用程序的实例句柄
- 0 // 额外的创建参数(此处没有传递额外参数)
- );
- // 如果窗口创建成功,Window 将保存窗口的句柄
- if (Window) { // 检查窗口句柄是否有效,若有效则进入消息循环
- // 图像测试
- win32_sound_output SoundOutput = {}; // 初始化声音输出结构体
- // 音频测试
- SoundOutput.RunningSampleIndex = 0; // 样本索引
- SoundOutput.SamplesPerSecond = 48000; // 采样率:每秒采样48000次
- SoundOutput.BytesPerSample = sizeof(int16) * 2; // 一个样本的大小
- SoundOutput.SecondaryBufferSize =
- SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample; // 缓冲区大小
- // 计算音频输出的延迟样本数
- // TODO:去掉 LatencySampleCount
- SoundOutput.LatencySampleCount = 3 * (SoundOutput.SamplesPerSecond / GameUpdateHz);
- SoundOutput.SafetyBytes =
- (SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample / GameUpdateHz) / 3;
- int16 *Samples =
- (int16 *)VirtualAlloc(0, 48000 * 2 * sizeof(int16), MEM_RESERVE | MEM_COMMIT,
- PAGE_READWRITE); //[48000 * 2];
- Win32InitDSound(Window, SoundOutput.SamplesPerSecond,
- SoundOutput.SecondaryBufferSize); // 初始化 DirectSound
- Win32ClearBuffer(&SoundOutput);
- GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
- GlobalRunning = true;
- LARGE_INTEGER LastCounter = Win32GetWallClock(); // 保留上次计数器的值
- #if GAME_INTERNAL
- // 设置基地址为 2
- // TB(此基地址通常用于模拟大规模内存空间或防止与操作系统其他内存地址重叠)
- LPVOID BaseAddress = 0;
- // LPVOID BaseAddress =Terabytes((uint64)2);
- #else
- // 设置基地址为 0
- LPVOID BaseAddress = 0;
- #endif
- // 初始化游戏内存结构体
- game_memory GameMemory = {};
- // 设置持久存储区的大小为 64 MB
- GameMemory.PermanentStorageSize = Megabytes(64);
- // 设置临时存储区的大小为 4 GB
- GameMemory.TransientStorageSize = Gibabytes((uint64)4);
- // 计算总内存需求
- uint64 TotalSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize;
- // 使用 VirtualAlloc 来分配内存,基地址是 BaseAddress,内存大小为
- // TotalSize
- // 分配的内存具有 'MEM_RESERVE | MEM_COMMIT' 属性,并且设置为可读写的内存
- GameMemory.PermanentStorage =
- (int16 *)VirtualAlloc(BaseAddress, TotalSize, MEM_RESERVE | MEM_COMMIT, //
- PAGE_READWRITE);
- // 设置临时存储区的起始位置在持久存储之后
- GameMemory.TransientStorage =
- (uint8 *)GameMemory.PermanentStorage + GameMemory.PermanentStorageSize;
- // 获取当前 CPU 时钟周期数,通常用于性能测量或时间标记
- int64 LastCycleCount = __rdtsc();
- LARGE_INTEGER FlipWallClock = Win32GetWallClock();
- // 如果有输入样本且成功分配了内存
- if (Samples && GameMemory.PermanentStorage && GameMemory.TransientStorage) {
- // 初始化输入结构体
- game_input Input[2] = {};
- // 将 NewInput 指向当前帧的输入
- game_input *NewInput = &Input[0];
- // 将 OldInput 指向上一帧的输入
- game_input *OldInput = &Input[1];
- #if GAME_INTERNAL
- // 调试时间标记索引初始化为 0
- int DebugTimeMarkerIndex = 0;
- // 初始化一个大小为 GameUpdateHz / 2 的调试时间标记数组,
- // 用于存储音频播放和写入光标的时间标记,数组大小是基于游戏更新频率的一半。
- win32_debug_time_marker DebugTimeMarkers[GameUpdateHz / 2] = {0};
- // TODO: 特别处理启动时的初始化逻辑
- // 这里的 TODO
- // 表示在游戏启动时可能需要做一些特殊的初始化操作,可能是与音频或其他系统组件相关。
- DWORD AudioLatencyBytes = 0;
- real32 AudioLatencySeconds = 0;
- bool32 SoundIsValid = false;
- #endif
- while (GlobalRunning) { // 启动一个无限循环,等待和处理消息
- game_controller_input *OldKeyboardController = GetController(OldInput, 0);
- game_controller_input *NewKeyboardController = GetController(NewInput, 0);
- // TODO: 我们不能把所有东西都置零,因为上下状态会不正确!!!
- game_controller_input ZeroController = {};
- NewKeyboardController->IsConnected = true;
- *NewKeyboardController = ZeroController;
- for (int ButtonIndex = 0;
- ButtonIndex < ArrayCount(NewKeyboardController->Buttons); ++ButtonIndex) {
- NewKeyboardController->Buttons[ButtonIndex].EndedDown =
- OldKeyboardController->Buttons[ButtonIndex].EndedDown;
- }
- Win32ProcessPendingMessages(NewKeyboardController);
- if (!GlobalPause) {
- // TODO: 我们应该频繁的轮询吗
- // 需要避免轮询已断开连接的控制器,以避免在旧版库中造成 XInput
- // 帧延迟。
- uint64 MaxControllerCount = XUSER_INDEX_ANY;
- if (MaxControllerCount > ArrayCount(NewInput->Controllers) - 1) {
- MaxControllerCount = ArrayCount(NewInput->Controllers) - 1;
- }
- for (DWORD ControllerIndex = 0; ControllerIndex < MaxControllerCount;
- ControllerIndex++) {
- DWORD OurControllerIndex = ControllerIndex + 1;
- game_controller_input *OldController =
- GetController(OldInput, OurControllerIndex);
- game_controller_input *NewController =
- GetController(NewInput, OurControllerIndex);
- // 定义一个 XINPUT_STATE 结构体,用来存储控制器的状态
- XINPUT_STATE ControllerState;
- // 调用 XInputGetState 获取控制器的状态
- if (XInputGetState(ControllerIndex, &ControllerState) ==
- ERROR_SUCCESS) {
- NewController->IsConnected = true;
- // 如果获取控制器状态成功,提取 Gamepad 的数据
- // NOTE:
- // 获取方向键的按键状态
- // 获取当前控制器的 Gamepad 状态
- XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;
- // 判断方向键的按键状态
- bool Up = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP);
- bool Down = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN);
- bool Left = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT);
- bool Right = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT);
- // 输出方向键状态
- std::cout << " Up = " << Up << " Down = " << Down
- << " Right = " << Right << " Left = " << Left
- << std::endl;
- // 将新控制器的摇杆位置设置为基于旧控制器的状态
- NewController->StickAverageX = Win32ProcessXinputStickValue(
- Pad->sThumbLX, //
- XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
- NewController->StickAverageY = Win32ProcessXinputStickValue(
- Pad->sThumbLY, //
- XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
- // 输出新控制器的摇杆平均值
- std::cout << NewController->StickAverageX
- << NewController->StickAverageY << std::endl;
- if ((NewController->StickAverageX != 0.0f) ||
- (NewController->StickAverageY != 0.0f)) {
- // 设置新控制器为模拟模式
- NewController->IsAnalog = true;
- }
- // 如果按下方向键,则根据方向修改摇杆的 X 或 Y 坐标值
- // 检测 D-Pad(方向键)向上按钮是否被按下
- if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP) {
- NewController->StickAverageY =
- 1.0f; // 将摇杆的 Y 轴设为最大值(向上)
- NewController->IsAnalog = false; // 设置控制器为非模拟模式
- }
- // 检测 D-Pad(方向键)向下按钮是否被按下
- if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN) {
- NewController->StickAverageY =
- -1.0f; // 将摇杆的 Y 轴设为最小值(向下)
- NewController->IsAnalog = false; // 设置控制器为非模拟模式
- }
- // 检测 D-Pad(方向键)向左按钮是否被按下
- if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT) {
- NewController->StickAverageX =
- 1.0f; // 将摇杆的 X 轴设为最大值(向左)
- NewController->IsAnalog = false; // 设置控制器为非模拟模式
- }
- // 检测 D-Pad(方向键)向右按钮是否被按下
- if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) {
- NewController->StickAverageX =
- -1.0f; // 将摇杆的 X 轴设为最小值(向右)
- NewController->IsAnalog = false; // 设置控制器为非模拟模式
- }
- // 设置阈值用于处理摇杆偏移
- real32 Threshold = 0.5f;
- // 根据摇杆的 X 坐标值触发左右移动
- Win32ProcessXInputDigitalButton(
- (NewController->StickAverageX < -Threshold) ? 1 : 0, //
- &OldController->MoveLeft, // 左移
- 1, //
- &NewController->MoveLeft);
- Win32ProcessXInputDigitalButton(
- (NewController->StickAverageX > Threshold) ? 1 : 0, //
- &OldController->MoveRight, // 右移
- 1, //
- &NewController->MoveRight);
- // 根据摇杆的 Y 坐标值触发上下移动
- Win32ProcessXInputDigitalButton(
- (NewController->StickAverageY < -Threshold) ? 1 : 0, //
- &OldController->MoveDown, // 下移
- 1, //
- &NewController->MoveDown);
- Win32ProcessXInputDigitalButton(
- (NewController->StickAverageY > Threshold) ? 1 : 0, //
- &OldController->MoveUp, // 上移
- 1, //
- &NewController->MoveUp);
- // 处理 A 按钮的数字输入
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前帧的按钮状态
- &OldController->ActionDown, // 传入上一帧的 A 按钮状态
- XINPUT_GAMEPAD_A, // A 按钮的位掩码,用于判断 A 按钮是否被按下
- &NewController->ActionDown); // 更新新控制器中的 A 按钮状态
- // 处理 B 按钮的数字输入
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前帧的按钮状态
- &OldController->ActionRight, // 传入上一帧的 B 按钮状态
- XINPUT_GAMEPAD_B, // B 按钮的位掩码,用于判断 B 按钮是否被按下
- &NewController->ActionRight); // 更新新控制器中的 B 按钮状态
- // 处理 X 按钮的数字输入
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前帧的按钮状态
- &OldController->ActionLeft, // 传入上一帧的 X 按钮状态
- XINPUT_GAMEPAD_X, // X 按钮的位掩码,用于判断 X 按钮是否被按下
- &NewController->ActionLeft); // 更新新控制器中的 X 按钮状态
- // 处理左肩按钮的数字输入
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前帧的按钮状态
- &OldController->LeftShoulder, // 传入上一帧的左肩按钮状态
- XINPUT_GAMEPAD_LEFT_SHOULDER, // 左肩按钮的位掩码,用于判断左肩按钮是否被按下
- &NewController->LeftShoulder); // 更新新控制器中的左肩按钮状态
- // 处理右肩按钮的数字输入
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前帧的按钮状态
- &OldController->RightShoulder, // 传入上一帧的右肩按钮状态
- XINPUT_GAMEPAD_RIGHT_SHOULDER, // 右肩按钮的位掩码,用于判断右肩按钮是否被按下
- &NewController->RightShoulder); // 更新新控制器中的右肩按钮状态
- // 处理 Start 按钮的状态
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前控制器的按钮状态
- &OldController->Start, // 传入旧控制器的状态
- XINPUT_GAMEPAD_START, // 按钮类型:Start 按钮
- &NewController->Start); // 更新新控制器的状态
- // 处理 Back 按钮的状态
- Win32ProcessXInputDigitalButton(
- Pad->wButtons, // 获取当前控制器的按钮状态
- &OldController->Back, // 传入旧控制器的状态
- XINPUT_GAMEPAD_BACK, // 按钮类型:Back 按钮
- &NewController->Back); // 更新新控制器的状态
- } else {
- NewKeyboardController->IsConnected = false;
- }
- }
- /*
- 这是声音输出计算的工作原理:
- -
- 我们定义一个安全值(`SafetyBytes`),表示游戏更新循环可能会变化的样本数量(假设最多2毫秒)。
- - 写入音频时,我们根据播放光标的位置,预测下一个帧边界时播放光标的位置。
- - 判断写入光标是否在预测目标位置之前(加上安全范围)。
- - 如果是,则目标填充位置是预测的帧边界加上一个完整的帧长度。
- -
- 如果写入光标已经超过目标位置,则假设无法完美同步音频,这种情况下会写入一帧的音频数据,并加上安全值保护样本。
- - 目标是低延迟情况下实现音频同步,但在高延迟情况下保证不会出现声音中断。
- */
- // 准备绘制缓冲区,传递到游戏更新和渲染函数中
- game_offscreen_buffer Buffer = {};
- Buffer.Memory = GlobalBackbuffer.Memory;
- Buffer.Width = GlobalBackbuffer.Width;
- Buffer.Height = GlobalBackbuffer.Height;
- Buffer.Pitch = GlobalBackbuffer.Pitch;
- // 调用游戏的更新和渲染逻辑,填充缓冲区
- GameUpdateAndRender(&GameMemory, NewInput, &Buffer);
- // 声音处理部分
- // 声明两个变量,分别表示音频缓冲区的播放光标和写入光标
- DWORD PlayCursor; // 播放光标:当前音频硬件正在播放的位置
- DWORD WriteCursor; // 写入光标:硬件允许写入新音频数据的位置
- // 获取当前时间点,作为当前帧结束的时间
- LARGE_INTEGER AudioWallClock = Win32GetWallClock();
- // 计算当前帧的时长(以毫秒为单位)
- real32 FromBeginToAudioSeconds =
- Win32GetSecondsElapsed(FlipWallClock, AudioWallClock);
- // 获取音频缓冲区的当前播放位置和写入位置
- if (GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor) ==
- DS_OK) {
- // 如果成功获取了音频缓冲区的当前位置
- if (!SoundIsValid) {
- /*
- 如果声音状态无效(例如程序刚启动或是首次运行音频逻辑):
- - 使用写入光标的当前位置作为基准,初始化运行样本索引。
- - 将写入光标的位置除以每个样本的字节数,以确定对应的样本索引。
- */
- SoundOutput.RunningSampleIndex =
- WriteCursor / SoundOutput.BytesPerSample;
- SoundIsValid = true; // 设置声音状态为有效
- }
- DWORD TargetCursor = 0; // 目标写入位置
- DWORD BytesToWrite = 0; // 需要写入的字节数
- // 计算需要锁定的字节位置,基于当前运行的样本索引
- DWORD ByteToLock =
- ((SoundOutput.RunningSampleIndex * SoundOutput.BytesPerSample) %
- SoundOutput.SecondaryBufferSize);
- // 计算每帧需要的字节数(基于采样率和帧率)
- DWORD ExpectedSoundBytesPerFrame =
- (SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample) /
- GameUpdateHz;
- // 计算距离下一个翻转(flip)操作剩余的时间,以秒为单位
- real32 SecondsLeftUntilFlip =
- (TargetSecondsPerFrame - FromBeginToAudioSeconds);
- // 计算从当前时刻到下一个翻转操作之间,预计需要处理的音频字节数
- // TargetSecondsPerFrame 表示每帧的目标时间,
- // FromBeginToAudioSeconds 表示从开始到当前时刻的音频时间,
- // ExpectedSoundBytesPerFrame 是每帧预期的音频字节数。
- // SecondsLeftUntilFlip 是剩余的时间,通过比例计算剩余字节数。
- [[maybe_unused]] DWORD ExpectedBytesUntilFlip =
- (DWORD)((SecondsLeftUntilFlip / TargetSecondsPerFrame) *
- (real32)ExpectedSoundBytesPerFrame);
- // 预测当前帧边界时的播放光标位置
- DWORD ExpectedFrameBoundaryByte =
- PlayCursor + ExpectedSoundBytesPerFrame;
- // 确保写入光标位置是安全的(考虑缓冲区环绕)
- DWORD SafeWriteCursor = WriteCursor;
- if (SafeWriteCursor < PlayCursor) {
- SafeWriteCursor +=
- SoundOutput
- .SecondaryBufferSize; // 修正光标位置以防止缓冲区回绕
- }
- Assert(SafeWriteCursor >= PlayCursor);
- SafeWriteCursor += SoundOutput.SafetyBytes; // 加入安全保护字节范围
- // 判断音频卡的延迟是否足够低
- bool32 AudioCardIsLowLatency =
- (SafeWriteCursor < ExpectedFrameBoundaryByte);
- if (AudioCardIsLowLatency) {
- /*
- 如果音频卡延迟较低:
- - 将目标写入光标设置为下一帧边界加上一个完整的帧长度。
- */
- TargetCursor =
- ExpectedFrameBoundaryByte + ExpectedSoundBytesPerFrame;
- } else {
- /*
- 如果音频卡延迟较高:
- - 将目标写入光标设置为写入光标位置,加上一个帧长度和安全字节数。
- */
- TargetCursor = WriteCursor + ExpectedSoundBytesPerFrame +
- SoundOutput.SafetyBytes;
- }
- // 确保目标光标位置在环绕缓冲区内
- TargetCursor = TargetCursor % SoundOutput.SecondaryBufferSize;
- // 计算需要写入的字节数
- if (ByteToLock > TargetCursor) {
- /*
- 如果锁定位置在目标位置之后:
- -
- 写入从锁定位置到缓冲区末尾的字节数,再加上从缓冲区开头到目标位置的字节数。
- */
- BytesToWrite =
- (SoundOutput.SecondaryBufferSize - ByteToLock) + TargetCursor;
- } else {
- /*
- 如果锁定位置在目标位置之前:
- - 写入从锁定位置到目标位置之间的字节数。
- */
- BytesToWrite = TargetCursor - ByteToLock;
- }
- // 设置音频缓冲区结构
- game_sound_output_buffer SoundBuffer = {};
- SoundBuffer.SamplesPerSecond =
- SoundOutput.SamplesPerSecond; // 每秒采样数
- SoundBuffer.SampleCount =
- BytesToWrite / SoundOutput.BytesPerSample; // 需要写入的样本数
- SoundBuffer.Samples = Samples; // 指向样本数据的指针
- // 调用游戏逻辑获取需要填充的声音样本数据
- GameGetSoundSamples(&GameMemory, &SoundBuffer);
- #if GAME_INTERNAL
- // DWORD TestPlayCursor;
- // DWORD TestWriteCursor;
- // GlobalSecondaryBuffer->GetCurrentPosition(&TestPlayCursor, //
- // &TestWriteCursor);
- win32_debug_time_marker *Marker =
- &DebugTimeMarkers[DebugTimeMarkerIndex];
- Marker->OutputPlayCursor = PlayCursor;
- Marker->OutputWriteCursor = WriteCursor;
- Marker->OutputByteCount = BytesToWrite;
- Marker->OutputLocation = ByteToLock;
- Marker->ExpectedFlipCursor = ExpectedFrameBoundaryByte;
- // 定义未封装的写指针,用于计算逻辑上的写指针位置
- DWORD UnwrappedWriteCursor = WriteCursor;
- // 如果写指针在播放指针之前(循环缓冲区重绕的情况),
- // 则将写指针逻辑上移到缓冲区的后面,以便计算差值
- if (UnwrappedWriteCursor < PlayCursor) {
- UnwrappedWriteCursor += SoundOutput.SecondaryBufferSize;
- }
- // 计算写指针和播放指针之间的字节数,
- // 这表示写指针和播放指针的距离,可以用来确定可写区域大小。
- // 注意:由于WriteCursor可能已经逻辑上被"展开",直接相减是安全的
- AudioLatencyBytes = UnwrappedWriteCursor - PlayCursor;
- AudioLatencySeconds =
- ((real32)AudioLatencyBytes / (real32)SoundOutput.BytesPerSample) /
- (real32)SoundOutput.SamplesPerSecond;
- char TextBuffer[255];
- _snprintf_s(TextBuffer, sizeof(TextBuffer), //
- "BTL:%u,TC%u,BTW:%u - PC:%u WC:%u DELTA:%u (%fs)\n", //
- ByteToLock, //
- TargetCursor, //
- BytesToWrite, //
- PlayCursor, //
- WriteCursor, //
- AudioLatencyBytes, //
- AudioLatencySeconds);
- OutputDebugStringA(TextBuffer);
- #endif
- Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite,
- &SoundBuffer);
- // 计算需要锁定的字节位置,基于当前样本索引和每样本字节数
- }
- // 获取当前时间作为工作结束的时间点
- LARGE_INTEGER WorkCounter = Win32GetWallClock();
- // 计算从上一帧到当前帧实际经过的时间
- real32 WorkSecondsElapsed =
- Win32GetSecondsElapsed(LastCounter, WorkCounter);
- real32 SecondsElapsedForFrame = WorkSecondsElapsed;
- // 如果当前帧消耗的时间少于目标帧时间(目标帧时间由目标帧率决定),则需要等待
- if (SecondsElapsedForFrame < TargetSecondsPerFrame) {
- // 如果系统的睡眠粒度已被设置为足够小(如 1 毫秒)
- if (SleepIsGranular) {
- // 计算还需要等待的时间,并将其转换为毫秒
- DWORD SleepMS = (DWORD)(1000.0f * (TargetSecondsPerFrame -
- SecondsElapsedForFrame));
- // 调用 Sleep 函数让线程休眠,以节省 CPU 资源
- if (SleepMS > 0) {
- Sleep(SleepMS);
- }
- }
- while (SecondsElapsedForFrame < TargetSecondsPerFrame) {
- // 重新计算从上一帧到当前帧实际经过的时间
- SecondsElapsedForFrame =
- Win32GetSecondsElapsed(LastCounter, Win32GetWallClock());
- }
- } else {
- // TODO:
- // 丢失帧率(当前帧消耗的时间超过了目标帧时间,可能需要记录或调整以优化性能)
- // TODO: 写日志(可以将帧率丢失的原因记录到日志文件中,便于后续分析)
- }
- // 这个地方需要渲染一下不然是黑屏a
- {
- HDC DeviceContext = GetDC(Window);
- win32_window_dimension Dimension = Win32GetWindowDimension(Window);
- RECT WindowRect;
- GetClientRect(Window, &WindowRect);
- #if GAME_INTERNAL
- // 在调试模式下同步显示音频缓冲区状态
- Win32DebugSyncDisplay(
- &GlobalBackbuffer, // 传入全局后备缓冲区,用于显示
- ArrayCount(DebugTimeMarkers), // 传入调试时间标记的数量
- DebugTimeMarkers, // 传入调试时间标记数组
- DebugTimeMarkerIndex + 1,
- &SoundOutput, // 传入声音输出结构,包含当前音频信息
- TargetSecondsPerFrame); // 目标每帧的秒数,用于同步帧率
- #endif
- // 在窗口中显示当前的缓冲区内容
- Win32DisplayBufferInWindow(
- DeviceContext, // 设备上下文,用于渲染到窗口
- Dimension.Width, // 窗口的宽度
- Dimension.Height, // 窗口的高度
- GlobalBackbuffer); // 全局后备缓冲区,包含要显示的图像
- ReleaseDC(Window, DeviceContext);
- }
- #if GAME_INTERNAL
- {
- if (GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, //
- &WriteCursor) //
- == DS_OK) { // 如果获取位置成功
- Assert(DebugTimeMarkerIndex < ArrayCount(DebugTimeMarkers));
- // 在调试模式下记录音频缓冲区的状态
- win32_debug_time_marker *Marker =
- &DebugTimeMarkers[DebugTimeMarkerIndex];
- // 记录当前的播放光标和写入光标
- Marker->FlipPlayCursor = PlayCursor;
- Marker->FlipWriteCursor = WriteCursor;
- }
- }
- #endif
- int64 EndCycleCount = __rdtsc();
- int64 CyclesElapsed = EndCycleCount - LastCycleCount;
- // 获取当前时间点,作为当前帧结束的时间
- LARGE_INTEGER EndCounter = Win32GetWallClock();
- // 计算当前帧的时长(以毫秒为单位)
- real32 MillisecondPerFrame =
- 1000.0f * Win32GetSecondsElapsed(LastCounter, EndCounter);
- // 计算帧率(Frames Per Second, FPS)
- // GlobalPerfCountFrequency 表示计时器的频率,CyclesElapsed
- // 表示当前帧的时钟周期数
- real32 FPS = (real32)GlobalPerfCountFrequency / (real32)CyclesElapsed;
- // 计算每帧的百万时钟周期数(MegaCycles Per Frame, MCPF)
- // 即每帧的时钟周期数除以 1,000,000,用于观察 CPU 消耗
- real32 MCPF = (real32)CyclesElapsed / (1000.0f * 1000.0f);
- // 将计算出的帧时间、帧率和百万时钟周期数格式化为字符串
- char PrintBuffer[256];
- sprintf_s(PrintBuffer, "%fms/f, %ff/s, %fmc/f\n", //
- MillisecondPerFrame, FPS, MCPF);
- // OutputDebugString(PrintBuffer);
- game_input *Temp = NewInput;
- NewInput = OldInput;
- OldInput = Temp;
- LastCounter = EndCounter;
- LastCycleCount = EndCycleCount;
- #if GAME_INTERNAL
- ++DebugTimeMarkerIndex;
- // 如果标记索引超出了数组范围,重新从头开始
- if (DebugTimeMarkerIndex == ArrayCount(DebugTimeMarkers)) {
- DebugTimeMarkerIndex = 0;
- }
- #endif
- }
- }
- } else {
- }
- } else { // 如果窗口创建失败
- // 这里可以处理窗口创建失败的逻辑
- // 比如输出错误信息,或退出程序等
- // TODO:
- }
- } else { // 如果窗口类注册失败
- // 这里可以处理注册失败的逻辑
- // 比如输出错误信息,或退出程序等
- // TODO:
- }
- return 0;
- }
复制代码 额外的VLC 方便再播放对应时间知道对应的内容
介绍
在脚本文件的目次添加time_display.lua 文件
命令行打开敲入下面命令
- vlc --extraintf=luaintf{intf="time_display"} -vv
复制代码
time_display代表文件名

怎么调试


相干API 参考https://code.videolan.org/videolan/vlc/-/tree/master/share/lua
下面是相干的Lua脚本
time_display.lua
- -- 将 "looper_custom_time" 脚本文件复制到 VideoLAN\VLC\lua\intf 文件夹中-- 激活它:-- vlc --extraintf=luaintf{intf="time_display"} -vv
- -- -vv 方便调试 vlc菜单 Tools(工具)->Messages(消息)打开能看到调试消息-- 读取的文件-- 00:21:58 开始修改音频输出方法的代码-- 00:22:48 找到最小的盼望音频延迟-- 00:27:31 使用量纲分析转换为秒-- 00:31:34 根据音频延迟写入声音-- 00:34:14 关于在早期阶段保持代码灵活且稍微混乱的精彩旁白-- 00:37:00 确定音频写入的位置-- 00:46:27 如何处理低延迟场景-- 读到的内容-- time = 00:21:58, message = 开始修改音频输出方法的代码-- time = 00:22:48, message = 找到最小的盼望音频延迟-- time = 00:27:31, message = 使用量纲分析转换为秒-- time = 00:31:34, message = 根据音频延迟写入声音-- time = 00:34:14, message = 关于在早期阶段保持代码灵活且稍微混乱的精彩旁白-- time = 00:37:00, message = 确定音频写入的位置-- time = 00:46:27, message = 如何处理低延迟场景-- -- 定义目标时间点(hh:mm:ss 格式)及对应消息-- local targetMessages = {-- time = 00:21:58, message = 开始修改音频输出方法的代码-- time = 00:22:48, message = 找到最小的盼望音频延迟-- time = 00:27:31, message = 使用量纲分析转换为秒-- time = 00:31:34, message = 根据音频延迟写入声音-- time = 00:34:14, message = 关于在早期阶段保持代码灵活且稍微混乱的精彩旁白-- time = 00:37:00, message = 确定音频写入的位置-- time = 00:46:27, message = 如何处理低延迟场景-- }-- 单独的函数,打印 targetMessages 表的内容function printTargetMessages(targetMessages) Log("Loaded target messages:") for _, target in ipairs(targetMessages) do Log("time = " .. target.time .. ", message = " .. target.message) endend-- 读取文件并解析目标时间和操作-- 调试输出路径和文件打开情况function loadTargetMessages(filePath) -- 里面不能打印log 不然会有问题 local targetMessages = {} local file = io.open(filePath, "r") if file then -- Log("File opened successfully") for line in file:lines() do local time, message = line:match("^(%d+:%d+:%d+)%s*(.*)$") if time and message then -- 按照时间字符串解析操作 table.insert(targetMessages, { time = time, message = message }) else end end file:close() else end return targetMessagesend-- 用于记录已触发的目标时间-- 文件路径修改成之间的路径local targetMessages = loadTargetMessages("C:/Users/16956/Documents/game/day20/game/Q&A.md")local triggeredTargets = {}function Looper() local loops = 0 -- counter of loops -- 加载目标时间和操作 while true do -- 调试打印读取的文件的内容 打开后台打印的数据太多 -- printTargetMessages(targetMessages) if vlc.volume.get() == -256 then break end -- inspired by syncplay.lua; kills vlc.exe process in Task Manager if vlc.playlist.status() == "stopped" then -- no input or stopped input loops = loops + 1 Log(loops) Sleep(1) else -- playing, paused if vlc.playlist.status() == "playing" then -- showFinalTime() checkTargetMessages() Sleep(1) elseif vlc.playlist.status() == "paused" then showFinalTime() Sleep(0.3) else -- unknown status Log("unknown") Sleep(1) end end endend-- 将 hh:mm:ss 格式的时间字符串转换为秒数function parseTime(timeStr) local hours, minutes, seconds = timeStr:match("^(%d+):(%d+):(%d+)$") hours = tonumber(hours) or 0 minutes = tonumber(minutes) or 0 seconds = tonumber(seconds) or 0 return hours * 3600 + minutes * 60 + secondsendlocal lastTarget = nillocal lastTimePassed = nillocal currentMessage = nil-- 查抄目标时间并显示相干消息function checkTargetMessages() local timePassed = math.floor(getTimePassed()) -- 获取当前时间(秒) -- Log("checkTargetMessages") local closestTarget = nil local closestDifference = math.huge -- 初始化一个非常大的值作为最靠近的时间差 for _, target in ipairs(targetMessages) do local targetSeconds = parseTime(target.time) * 1000 * 1000 -- 将 hh:mm:ss 转为秒 -- 确保目标时间小于或等于当前时间 if targetSeconds <= timePassed then local difference = timePassed - targetSeconds -- 计算当前时间与目标时间的差值 -- 找到最靠近的目标时间 if difference < closestDifference then closestTarget = target closestDifference = difference currentMessage = closestTarget.message end end end if closestTarget == lastTarget and closestTarget ~= nil and absoluteDifference(timePassed, lastTimePassed) > 5 * 1000 * 1000 then Log("diff" .. absoluteDifference(timePassed, lastTimePassed)) triggeredTargets[closestTarget.time] = false end -- 如果找到了最靠近的目标时间,显示消息 if closestTarget and not triggeredTargets[closestTarget.time] then -- 显示最靠近的消息 vlc.osd.message(closestTarget.message, vlc.osd.channel_register(), "top-left", 5000000) -- 标记该时间点已触发 triggeredTargets[closestTarget.time] = true Log("lastTarget ~= nil before") if lastTarget ~= nil and lastTarget ~= closestTarget then Log("lastTarget ~= nil == true") triggeredTargets[lastTarget.time] = false end lastTarget = closestTarget; end lastTimePassed = timePassed;endfunction absoluteDifference(x, y) if x > y then return x - y else return y - x endendfunction Log(lm) vlc.msg.info("[looper_intf] " .. lm)endfunction showFinalTime() Log("showFinalTime") local timePassed = getTimePassed() local formattedTime = formatTime(timePassed) Log(formattedTime) vlc.osd.message("Current Time: " .. formattedTime, vlc.osd.channel_register(), "top-right", 1200000) vlc.osd.message((currentMessage ~= nil) and currentMessage or "", vlc.osd.channel_register(), "top-left", 1200000)end-- 将相对时间(秒)转换为时:分:秒格式function formatTime(microseconds) local seconds = math.floor(microseconds / (1000 * 1000)) local hours = math.floor(seconds / 3600) local minutes = math.floor((seconds % 3600) / 60) local secs = seconds % 60 return string.format("%02d:%02d:%02d", hours, minutes, secs)endfunction getTimePassed() local input = vlc.object.input() if input then local time = vlc.var.get(input, "time") or 0 Log("Raw Time Passed: " .. time) return time -- 此处保留毫秒 else return 0 endendfunction Sleep(st) -- seconds vlc.misc.mwait(vlc.misc.mdate() + st * 1000000)endLooper()
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |