# 12　资源、声音与电影：CD-ROM 时代的内容管线

一套 RTS 的代码不会凭空产生士兵图像、配乐和真人过场。它必须把制作好的素材变成可分发的文件，在磁盘、内存、解压缓冲和播放设备之间持续搬运。本章沿着“找文件—解码—呈现”的实际路径阅读，解释 MIX、SHP、战区图块、WSA、AUD 和 VQA 如何合作。

本章依据指定提交静态分析；资源名称来自加载代码，不等于仓库已经附带相应商业游戏素材。不同年代的工具、DOS 与 Windows 实现并存，不能仅因函数同名就视为同一版本。

## 12.1 MIX 是资源索引容器，不是统一图片格式

游戏初始化分别注册 `REDALERT.MIX`、`LOCAL.MIX`、分辨率资源包，以及 `MAIN.MIX`、`CONQUER.MIX`、影片、音乐和语音包。一部分包整体缓存，一部分只注册索引并留在磁盘读取。代码对 `GENERAL.MIX`、`MOVIES1/2.MIX`、`SPEECH.MIX` 有不缓存的安排，对 `CONQUER/SOUNDS/RUSSIAN/ALLIES` 有后续缓存调用。[资源包注册](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2950-L3000)、[主资源和媒体包](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L3029-L3092)、[缓存调用](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L3398-L3407)。

这样安排很容易理解：枪声需要低延迟反复播放，整部电影则不值得占据有限内存。MIX 提供打包、索引和统一查找，包内文件仍各有格式。不能说“从 MIX 解压出一切”，因为 MIX 的读取路径并不要求其整个数据区经过通用压缩；SHP、AUD、VQA 会分别决定自己的压缩方式。

`MixFileClass` 的普通头记录文件数和数据总量，每个子项提供 CRC、偏移和大小。扩展格式先检测两个短整数：第一个为零代表扩展头，第二个中的位表示是否有摘要、头是否加密。构造函数通过 `FileStraw` 读取；需要解密时串接 `PKStraw`，读完头与索引后记录 `DataStart`。[MIX 构造](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.CPP#L168-L253)、[字段含义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.H#L73-L115)。

尤其要分清加密范围：头文件明确说明加密针对包头，内嵌文件数据本身不因此加密。`Cache` 在读取整个数据区时可以串接 `SHAStraw`，随后比较 20 字节摘要；这项验证出现在缓存路径，并不意味着每次任意流式读取都重新校验整包。[头加密边界](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.H#L80-L88)、[Cache 的读取与摘要比较](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.CPP#L375-L454)。存档的加密压缩链是另一套问题，见[存档与二进制格式](14-save-load-and-binary-formats.md)。

## 12.2 请求一个文件名，系统到底怎样找到它

`Offset` 把名称转大写，计算 CRC，依注册的 MIX 链表逐个检索，各包的索引使用 `bsearch`。找到条目后，如果包已缓存，返回 `Data + Offset` 指针；未缓存则返回磁盘偏移，加上该包的数据起点。[MixFileClass::Offset](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.CPP#L526-L580)。因此包注册顺序会影响同名资源的优先级；索引本身使用 CRC 标识而不是必须保存完整路径字符串。

`CCFileClass::Open` 再把三种情况统一起来：

| 实际位置 | 打开方式 | 后续 Read 做什么 |
|---|---|---|
| 磁盘上存在独立同名文件 | 优先按普通文件打开 | 调用底层文件读取 |
| 文件在已缓存的 MIX | 保存内存 Buffer 与位置 | 从内存复制并推进位置 |
| 文件在未缓存的 MIX | 打开包文件，设 Bias 起点与长度 | 把包的一段伪装成独立文件 |

独立文件优先是代码明确保留的升级覆盖机制。调用者可以继续写 `CCFileClass("XXX.AUD")`，而无需知道它藏在光盘哪个包里。[CCFileClass::Open](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CCFILE.CPP#L417-L482)、[CCFileClass::Read](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CCFILE.CPP#L178-L221)。

但 `MFCD::Retrieve` 是另一种更窄的接口：它只是经 `Offset` 取驻留指针，不负责把任意缺失资源从磁盘分配加载进来。调用点使用它之前，通常已确保相应包处于缓存状态。把 Retrieve 和 CCFile 的能力混为一谈，会误解很多资源加载顺序。[Retrieve](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MIXFILE.CPP#L278-L283)。

`Straw` 的比喻是“向上游吸取字节”；`Pipe` 则是把字节推向下游。组合后，文件访问、摘要、解密、解压可以使用相似接口。以 `LCWStraw::Get` 为例，它先消耗内部缓冲，空了再读取压缩块头与块体，解压出下一段。**这证明项目具有可组合的数据流设施，不代表每种资源都必须穿过 LCWStraw。** SHP 和 VQA 的许多路径直接调用 LCW 解码。[Straw 链接与读取](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/STRAW.CPP#L93-L141)、[LCWStraw::Get](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LCWSTRAW.CPP#L126-L179)。

## 12.3 素材种类与运行时用途

| 名称或类型 | 游戏中的用途 | 已确认的加载/解释入口 |
|---|---|---|
| `.MIX` | 多种资源的打包与查找 | MixFileClass、CCFileClass |
| `.SHP` | 单位、建筑、图标、光标等多帧图像 | 类型初始化 → Build_Frame → CC_Draw_Shape |
| 战区模板、地形图像 | 地面瓦片、树木等 | TemplateTypeClass::Init、各类型 Init |
| `.PAL` | 战区调色板 | DisplayClass::Init_Theater |
| `.CPS` | 标题或界面图，也可承载调色映射数据 | Load_Picture / Load_Uncompress |
| `.WSA` | 界面、阵营选择或成绩页动画 | Open_Animation / Animate_Frame |
| `.AUD`，部分 `.Vxx/.Rxx` | 效果、EVA 语音、音乐、不同口音回应 | Sound_Effect / Speak_AI / ThemeClass |
| `.VQA`，Windows 另见 `.VQP` | 过场电影及其插值调色支持 | Play_Movie → VQA 库 |

这里“模板”常被社区叫作 TMP 格式，但不能把格式简称当成固定文件后缀：本代码按 `Theaters[theater].Suffix` 组成资源名称，战区类型还限制哪些模板可用。Windows 在载入模板后调用 `Register_Icon_Set`，为图块缓存登记，再从图块集头取宽高。[TemplateTypeClass::Init](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CDATA.CPP#L3122-L3146)。单位普通图和附加炮塔等则直接请求 `.SHP`。[UnitTypeClass 图形加载](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/UDATA.CPP#L1080-L1142)。

`.PAL` 载入后还会生成阴影和半透明查表，资源内容会影响实际渲染结果。[战区调色初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L375-L455)。`.CPS` 的标题用途与调色映射用途可在 `Load_Title_Page` 相邻代码看到；低层压缩图读取有 NOCOMPRESS、LCW 等分支，而不是按扩展名简单照搬到屏幕。[标题图入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2511-L2539)、[通用图解压分派](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/IFF/LOAD.CPP#L330-L377)。

## 12.4 SHP：一帧可以由另一帧恢复出来

多方向单位和多阶段动画需要很多帧，但相邻帧通常相似。`Build_Frame` 把帧分为关键帧与差分关联帧：关键帧通过 LCW 恢复完整图像；其他帧先定位参考关键帧，再依序应用 XOR 差分，直到得到所需帧。这是“共享基础图像 + 存变化”的压缩思路。[Windows Build_Frame 解码](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/2KEYFRAM.CPP#L315-L414)、[DOS Build_Frame](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/KEYFRAME.CPP#L132-L282)。

LCW 和 XOR 在这里分工不同。LCW 消除字节流内部的重复：代码支持直接复制字面数据、从已输出数据引用、重复填充等操作，`0x80` 结束流。XOR 差分则表达“相对于已有画面哪些值需要改变”。不能把它们统称为一种压缩，也不应把 LCW 自动等同于 ZIP、LZW 或视频的运动预测。[库内 LCW_Uncompress 操作码与解码分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/IFF/LCWUNCMP.ASM#L75-L150)。

还要区分实现来源：Windows 图像库的 `IFF/MAKEFILE` 和电影辅助库的 `VQM32/MAKEFILE` 都列入各自目录的 `lcwuncmp.obj`，对应 `LCWUNCMP.ASM`。主游戏技术库则编译 `CODE/LCW.CPP`，其接口叫 `LCW_Uncomp`；仓库另存的 `CODE/LCWUNCMP.CPP` 未列入这份主清单。不能因为名字相似，就把它当作所有媒体调用的唯一实现。[图像库对象清单](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/IFF/MAKEFILE#L78-L87)、[电影辅助库对象清单](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQM32/MAKEFILE#L54-L65)、[主技术库 LCW 对象](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MAKEFILE#L373-L388)。

Windows 的 `2KEYFRAM.CPP` 为已经解开的帧保存缓存。如果对应槽位已有数据，直接返回其位置；否则完成解码，复制到普通或战区专用的大形状缓冲，并保存绘图头及偏移。战区图像与通用单位分开，便于战区切换时管理资源寿命。[帧缓存查找](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/2KEYFRAM.CPP#L255-L312)、[缓存写入](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/2KEYFRAM.CPP#L417-L477)。

这里不是仅凭文件名猜测运行分支：主 `MAKEFILE` 的 WIN32 对象列表选 `2KEYFRAM.OBJ/2KEYFBUF.OBJ`，非 Windows 分支选 `KEYFRAME.OBJ/KEYFBUFF.OBJ`。[构建明确选择](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MAKEFILE#L401-L425)。同名 `Build_Frame` 的返回值约定在调用处也不同。移植时若随便把一个版本替换成另一个，可能把“解压长度”误用为“帧指针”。

具体观察例子：坦克第一次向某个方向转动时，要恢复对应 SHP 帧；之后再次使用该帧，在缓存启用且有效时可省去解码。画面上的阵营颜色不必各存一套帧，因为最后绘制仍可做调色重映射。这把存储压缩、内存缓存和颜色复用结合起来。实际节约多少内存、是否出现首次显示卡顿，本次未运行测量。

## 12.5 WSA：适合连续播放的小动画

`Open_Animation` 读取动画头、帧偏移和可选的 768 字节调色板，计算目标缓冲、差分缓冲与整个文件驻留需要的大小。它可依据用户标志和内存大小选择从磁盘读取或保留文件数据，还支持直接画到目标页和先在内部缓冲合成。[Open_Animation](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/WSA/WSA.CPP#L171-L214)、[内存布局和驻留选择](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/WSA/WSA.CPP#L246-L408)。

`Animate_Frame` 从当前帧到指定帧的距离出发，比较正向、反向或环绕需要应用多少个差分。XOR 的可逆性使同一差分可以帮助前进或后退，不必每次都从第一帧开始。初次调用还要区分“首帧相对黑底”与“首帧相对已有图片”的情况。[Animate_Frame](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/WSA/WSA.CPP#L464-L649)。

游戏中的 `CHOOSE.WSA`、成绩页动画通过这个接口播放。因而“所有动画都是 SHP”并不成立：战场单位要按当前方向/动作任意取帧，界面动画则更适合围绕连续帧状态推进。[阵营选择动画入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INTRO.CPP#L133-L148)、[成绩页 WSA 入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCORE.CPP#L421-L433)。

## 12.6 声音有三种不同的使用策略

**短音效与单位回应。** `Sound_Effect` 先过滤静音和无音频状态，结合选项音量。常规文件用 `.AUD`；某些单位回应根据盟军/苏军、步兵/车辆及 variation 选择 `.V00…V03` 或 `.R00…R03`。取得缓存资源指针后，用优先级和音量调用 `Play_Sample`。[Sound_Effect 文件选择](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/AUDIO.CPP#L393-L471)。

坐标版本对不在当前视图的声源计算距离衰减及横向 `pan_value`，然后下传。注意具体源码边界：Windows `Play_Sample_Handle` 的声像参数没有名字，所读实现未使用该参数，因此不能据上层计算直接断言这一 Windows 路径真的实现了左右定位。它体现了接口保留与后端行为不完全一致的问题。[坐标音效](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/AUDIO.CPP#L330-L361)、[Windows 播放签名](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDIO.CPP#L1628-L1642)。

**EVA 语音。** `Speak` 只有一个等待槽 `SpeakQueue`，会排除正在播放或已等待的同一句话；它不是无限长度消息队列。`Speak_AI` 等当前缓冲停止后，加载下一条 `.AUD` 到轮换语音缓冲并以较高优先级播放。这让“建造完成”“资金不足”等播报不必全部并发。[Speak / Speak_AI](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/AUDIO.CPP#L643-L719)。

这里还有一个值得读者练习查证的静态疑点：函数注释说会复用此前载入的语音，但 `SpeechRecord` 的搜索之后，局部 `speech` 仍为 NULL，没有看到为它赋缓存指针的语句，随后总进入重新加载分支。这是该函数体的事实；是否有特定版本修复、是否对实际播放造成可观察影响，尚未通过运行验证。

**背景音乐。** `ThemeClass` 维护当前曲目与 Pending。每轮检查播放状态，必要时选择下一首；`Play_Song` 调用 `File_Stream_Sample_Vol`，音乐不需要整曲先装入内存。流式播放器分配可重复使用的文件缓冲，预载开头，并以回调继续供给数据。[ThemeClass::AI](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/THEME.CPP#L197-L215)、[Play_Song](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/THEME.CPP#L334-L374)、[文件流播放](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDIO.CPP#L556-L618)。

## 12.7 AUD 解码与真实的播放设备

AUD 头带采样率、长度、标志和压缩方式。Windows 播放器根据这些值设置采样追踪结构；遇到声道数、位深或采样率不同，还会重新配置 DirectSound 缓冲。压缩算法不是由 `.AUD` 扩展名唯一确定的。[Play_Sample_Handle](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDIO.CPP#L1660-L1750)。

`Sample_Copy` 对未压缩数据直接复制；对 WESTWOOD/SOS 压缩块，先读压缩大小、展开大小和 magic。两者大小相等就直接复制，否则放入暂存缓冲后分别调用 `Decompress_Frame` 或 SOS 解码器。数据块可能跨两个输入缓冲，`Simple_Copy` 负责拼接，因此流式读取不必刚好落在压缩块边界。[Sample_Copy](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDINT.CPP#L165-L248)。

同时可播放音效槽位有限。`Get_Free_Sample_Handle` 先找空槽；没有空槽时，找优先级不高于新音效的槽并停止旧音效，若没有合适槽则放弃播放。代码还试图为音乐保留通道。战场越热闹，并不意味着所有声音都会无条件完整叠加。[音效槽与抢占](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDIO.CPP#L1476-L1523)。

Windows 音频通过定时回调维护缓冲，初始化处使用 `timeSetEvent`；该处创建音频线程的调用被注释掉，所以不能看到 `Sound_Thread` 函数便宣称此构建必然运行专用线程。[回调实现](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDIO.CPP#L893-L919)、[定时器启用](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/AUDIO/SOUNDIO.CPP#L1145-L1155)。相较之下，`WWFLAT32` 保留 DOS 寄存器保护和中断相关机制。共享高层 API，使游戏逻辑少关心设备；平台代码依然有实质区别。

## 12.8 VQA：电影不是一连串完整 BMP

`Play_Movie` 是游戏与电影库的边界：在通常 VQA 路径中排除编辑器和非单机模式，组成 `.VQA` 文件名，设置音频选项，初始化 `VQAHandle`，调用 Open/Play/Close/Free，并处理鼠标、调色板和中断退出后的屏幕恢复。Windows 还有 `.VQP` 插值调色支持；`MPEGMOVIE` 条件下，DVD 播放会更早尝试另一条电影路径，因此不能把所有历史发行配置都说成只有 VQA。[Play_Movie](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2825-L2988)。

库不必自己了解 MIX。游戏把 `MixFileHandler` 作为 I/O 回调，库请求 OPEN/READ/SEEK/CLOSE，回调转给 `CCFileClass`。这正是为什么大型电影可以放在未缓存的 MIX 中顺序读取。[MixFileHandler](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2604-L2678)。

`VQA_Open` 检查 `FORM` 和 `WVQA` 标识；影片头携带帧数、宽高、块宽高、帧率、码本组大小、码本项数和音频规格。**块尺寸、帧率来自头部，而不是应被写死为一种固定配置。**[VQA_Open](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/LOADER.CPP#L151-L213)、[VQAHeader](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/VQAFILE.H#L54-L97)。

VQ 即向量量化，这里的直观理解是：准备一份小图块字典，画面各位置只写“选字典第几项”。该字典叫 codebook，指向其条目的索引叫 pointers。相邻帧可以沿用同一码本，并用部分码本块渐进更新。源码按 IFF chunk 分派完整/部分码本 `CBF*/CBP*`、调色板 `CPL*`、指针 `VPT*` 及声音 `SND*`。[VQA_LoadFrame 的 chunk 分派](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/LOADER.CPP#L750-L868)、[完整码本加载](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/LOADER.CPP#L2088-L2119)。

解码分两层：`Prepare_Frame` 在必要时用 LCW 解开码本、调色板和指针字节；随后 `DrawFrame_Buffer` 调用选定的 `UnVQ`，利用码本与指针生成实际像素。**LCW 负责字节级重复，VQ 负责图块表示；它们可以叠加。** 最后设置待展示标志、调用用户回调，并移到下一帧节点。[Prepare_Frame](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/DRAWER.CPP#L753-L792)、[DrawFrame_Buffer](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/DRAWER.CPP#L2150-L2244)。

示意例子：一帧中的大片墙面与军服区域重复，编码器可以让许多位置引用相同小图块。下一帧人物只动了一点，大部分码本仍可复用。这解释了这种表示适合低带宽播放；仓库中的播放器能够证明解码结构，但不能单凭它还原每部过场原始素材怎样拍摄、调色或选择码本。

## 12.9 音画同步、缓冲和丢帧

`VQA_Play` 启动音频和计时器，循环进行 LoadFrame、DrawFrame、User_Update。缓冲不足时暂不绘制，不把没有加载好的节点当有效画面。[VQA_Play](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/TASK.CPP#L242-L417)。

`Select_Frame` 根据 `VQA_GetTime` 和绘制帧率算出应该显示哪一帧；太早则等待，落后则在规则允许时跳帧，同时照顾关键帧和调色板变化。源码还有至少维持一定显示频度的判断。它追求的是画面跟上媒体时间，不能简单理解成“游戏循环每跑一次就播下一张”。[Select_Frame](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/DRAWER.CPP#L589-L707)。

影片声音与普通 AUD 有相通理念，但容器和读取函数不同：`SND1` 读取 ZAP 头并调用 `AudioUnzap`；`SND2` 使用 ADPCM 状态和 `VQA_sosCODECDecompressData`。Windows `VQA_OpenAudio` 能使用传入的 DirectSound 对象，也有自行创建对象和缓冲的路径。[SND1](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/LOADER.CPP#L2637-L2718)、[SND2](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/LOADER.CPP#L2772-L2853)、[电影 DirectSound 初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQA32/AUDIO.CPP#L1226-L1318)。

`VQ` 与 `WINVQ` 保存媒体库及观看工具相关代码，`VQA32` 是播放器主体，`VQM32` 含 LCW、音频和图形辅助代码，`VPLAY32/VQAVIEW` 提供播放、查看相关程序。应把它们理解为支持游戏的媒体子工程，不要与主模拟逻辑混作一套，也不要把 `OLD` 或重复 INCLUDE 当成独立的新游戏功能。

## 12.10 从制作素材到游戏：仓库能证明多少

可以从源码直接串出一条生产链：音频工具读取 WAV 等输入，生成游戏可播放的音频块；MIX 工具将文件整理成索引和数据区；游戏按资源用途加载或流式读取；解码器将素材变成像素与采样；显示/声音后端把它们交给设备。

`TOOLS/AUDIOMAK/AUDIOMAK.CPP` 能读取 WAV 的 `fmt` 与 `data` 块，记录采样率/声道/位深，分小块调用 `Compress_Frame`，写出压缩大小、原大小、magic 和压缩数据，最后回填头。它还保留 Sonarc 历史分支；不要把这个旧工具枚举直接当作所有运行时 AUD 编码的完整清单。[AUDIOMAK 转换](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/AUDIOMAK/AUDIOMAK.CPP#L494-L617)。

`TOOLS/MIX/MIXFILE.CPP` 则先安排数据顺序和偏移，再按 CRC 排序索引，最后写头及数据；这一顺序正好匹配运行时“二分查索引、按偏移取数据”的需求。[MIX 工具写包](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/TOOLS/MIX/MIXFILE.CPP#L497-L573)。目录还包含 ANIMATE、KEYFRAME、ICONCOMP 等工具可执行文件，但仅凭文件存在不足以断言其每项能力、运行兼容性或完整构建方法。

对爱好者最有启发的制作原理是：最终游戏不直接消费所有美术原稿，而消费为目标硬件设计的运行时表示。多帧图像共享差分、阵营共享调色、地面共享瓦片、电影共享码本、长音频共享流式缓冲。素材管线和运行时引擎共同决定游戏的容量、速度与视觉风格。

## 源码阅读路线与尚未验证处

先读 `CCFILE.CPP::Open` 和 `MIXFILE.CPP::Offset/Cache`，解决“字节从哪里来”；再读 `2KEYFRAM.CPP::Build_Frame` 与 WSA 的 `Animate_Frame`，解决“图像怎样恢复”；然后从 `AUDIO.CPP`、`THEME.CPP` 追到平台库；最后读 `Play_Movie → MixFileHandler → VQA_Open/LoadFrame → Prepare_Frame/UnVQ`。

本章未实际播放商业素材，未运行老工具，也未验证声卡、显卡驱动或光盘环境。SHP 帧缓存是否开启取决于初始化条件，具体 VQA 块尺寸、采样参数、压缩比例需要读取实际素材才能确认。`Speak_AI` 缓存赋值缺失和 Windows 声像参数未使用属于已指出的静态代码现象，其用户可见影响仍需要针对构建版本复现。边界检查也必须按实现区分：`CODE/LCW.CPP::LCW_Uncomp` 未使用长度参数；图像库和电影辅助库的汇编 `LCW_Uncompress` 则读取 length，并可见输出长度裁剪，不能把前者的行为推广到所有 SHP、WSA、VQA 解压路径。输出裁剪也不等于已经完整验证输入长度、回拷范围和所有损坏数据；本章没有将任何实现认定为经过安全验证的通用解析库。[技术库 C++ 解码器](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LCW.CPP#L72-L109)、[图像库长度裁剪](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/IFF/LCWUNCMP.ASM#L57-L150)、[电影辅助库长度处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WINVQ/VQM32/LCWUNCMP.ASM#L84-L139)。
