我的电脑里有一个名叫GAME的文件夹,里面存着十几张BMP图片、几段MIDI音乐、几十个WAV音效,还有一个我用C语言写的小程序。这些素材是从一张1998年的游戏光盘里扒出来的,程序则是当时为了在电脑上播放这些素材而写的。这个文件夹保存至今,因为我始终觉得,亲手让这些静态的像素和音频活起来,比直接玩游戏更有意思。
BMP图片主要是游戏中的场景和人物立绘。用十六进制编辑器打开就能看到,文件头是标准的54字节,接着是768字节的调色板,然后是像素数据。每张图都是640×480分辨率,256色,所以每个像素只占一个字节,整张图带调色板大约307KB。但游戏里存的BMP是倒序的,也就是第一行数据在文件末尾,直接读出来显示会上下颠倒。我在程序里用了一个循环,把像素数组按照行号翻转后才交给绘图函数。最初用SetPixel一个个点来画,一张图要好几秒,后来改用StretchDIBits直接整块画,几乎瞬间完成。
MIDI音乐是游戏里的背景配乐,文件不大,几KB到几十KB。我尝试自己解析标准MIDI文件。打开文件头,看到“MThd”和“MTrk”标识,格式是1,有六条音轨。每条音轨由一系列事件组成,每个事件前面有一个增量时间,用变长整数表示。我写了一个读取变长整数的函数,然后逐个解析音符开、音符关、控制变换这些事件。当时我对MIDI事件并不熟悉,对照着网上的文档,硬是把音符编号和强度提取出来,再用midiOutShortMsg发送给声卡自带的合成器。当第一段旋律在喇叭里响起来时,我确认那些十六进制数字确实是音乐而不是乱码。
WAV音效则更加直观。每个文件都是RIFF格式,开头是“RIFF”四个字符,接着是“fmt”和“data”块。我读取其中的采样率、位深和声道数,然后用waveOutWrite函数把data块里的波形数据送出去。有一个脚步声的WAV,采样率22050Hz,16位单声道,时长0.4秒,数据大小约17KB。我把它反复播放,用来测试程序里的循环。为了把MIDI和WAV同时播放,我在SetTimer的回调函数里分别调用midiOutShortMsg和waveOutWrite,虽然两个设备的时钟不同步,但听起来还有模有样。
自己编的程序主体是一个素材查看器。窗口左侧列出BMP文件的文件名,点击后右侧显示图片。图片上我预先定义了十几个矩形区域,每个区域对应一个WAV音效。比如点击人物头部区域,会播放一声喊叫,点击脚部区域,会播放脚步声。程序里还加了一个菜单项,可以打开MIDI文件,播放过程中会显示当前音符的编号和力度,用一个简单的滚动条来表示。这些功能现在看起来微不足道,但当年写下来,涉及了文件解析、设备控制、窗口消息处理等多个方面。程序源代码有三千多行,编译出来的可执行文件只有几十KB。

这些素材本身并不珍贵,但通过自己写的代码,它们被重新组织成一种可以交互的小世界。BMP图片里的人物站在一片草地上,我点击地面,响起脚步声,同时背景MIDI旋律缓缓推进。整个过程没有游戏引擎,没有现成的播放器,所有逻辑都在自己写的循环里。这种满足感来自于彻底理解每一个字节的用途,以及让硬件按照自己的命令工作。至今我仍然喜欢打开那个文件夹,运行那个老程序,听一听二十多年前的声音。