为IBM PC XT开发PC扬声器MIDI播放器






2.60/5 (2投票s)
如何为IBM PC XT开发PC扬声器MIDI播放器
在我使用一台 12MHz NEC V20 运行的 PC XT 进行实验时,我尝试使用 James Allwright 编写的 MIDIPLAY 工具通过 PC 扬声器播放 MIDI 文件。我之前使用过这个播放器,并且能够在我的 286、386 和 486 机器上通过 PC 扬声器很好地播放各种 MIDI 文件。然而,在我的 XT 机器上,在加载文件几秒钟后,它只发出一个短促的蜂鸣声然后就退出了。快速查看代码后,我怀疑问题出在负责播放声音的 `NOTE.A` 文件中的汇编代码。这段代码在 `MIDIPLAY.C` 文件中的一个 while 循环中被调用,逐个音符地播放 MIDI.
while (place != NULL) {
note(place->pitch, place->start);
place = place->next;
};
这是实际播放音符的汇编代码
dosound:
in al, 61h
push ax
or al, 03h
out 61h, al ; turn speaker on
xor si, si
mov ax, [bp+4] ; timer interval
push ax
mov al, 0B6h
out 43h, al
pop ax
out 42h, al
mov al, ah
out 42h, al ; set up interval
call delay
pop ax
out 61h, al ; turn speaker off
endtune:
mov sp, bp ; restore stack pointer
pop bp
ret
delay:
mov cx, word [bp+8] ; high word
mov dx, word [bp+6] ; low word
mov ah, 86h
int 15h
ret
上述汇编代码依赖于 `AH=86h/INT 15h` 调用,在将音符频率写入 PC 扬声器端口 61h 后生成适当的延迟。这项功能最初是在运行 80286 CPU 的 IBM PC/AT(也称为 5170)上实现的。在 PC XT 上,此中断未实现,导致 MIDI 播放器失败,因为每个音符之后都没有延迟。
我设法使用作者最初使用的 Personal C Compiler 编译了代码。为了避免指定包含路径,我只是将所有源文件复制到示例文件夹中,并使用以下批处理文件来编译和运行 MIDI 播放器。
del midiplay.exe
del midifile.o
del midiplay.o
del note.o
pcc midiplay.c
pcc midifile.c
pcca note.a
pccl midiplay.o midifile.o note.o
midiplay.exe test.mid
批处理文件会删除所有先前的构建输出,使用 PCC 编译 C 文件,使用 PCCA 编译 `NOTE.A` 汇编代码,并使用 PCCL 将编译输出链接起来以生成最终的 `.EXE` 文件。尽管有一些链接器警告,但编译成功,生成的 `MIDIPLAY.EXE` 可以用来播放 MIDI 文件。
然而,要修复代码使其能在 XT 上运行并非易事。由于修改代码使用 `INT 21H` 获取系统日期/时间或读取 8253 可编程间隔定时器计数器并手动计算延迟将非常复杂,我选择了更简单的方法:将代码移植到 Turbo C 并使用 `conio.h` 中的 `sound()` 和 `nosound()` 函数。为此,我研究了原始代码,并将 `MIDIFILE.H` 和 `MIDIFILE.C` 合并到一个单一的 `MIDIPLAY.C` 文件中,同时删除了不必要的函数头。在此过程中,我遇到了原始 MIDIPLAY 代码中的旧式 C 语法,它要求方法变量类型需要单独声明。
void
write16bit(data)
int data;
{
}
这是现代等效代码
void write16bit(int data)
{
}
在 PCC 中,要在另一个方法中使用前向声明的方法,必须再次声明该方法,不带任何参数,类似于声明变量的方式。以下代码显示了方法 `WriteVarLen` 在使用前如何再次声明。
int
mf_write_midi_event(delta_time, type, chan, data, size)
long delta_time;
int chan,type;
int size;
char *data;
{
void WriteVarLen();
WriteVarLen(delta_time);
...
}
方法 `WriteVarLen` 如下所示
void
WriteVarLen(value)
long value;
{
....
}
幸运的是,Turbo C++ 3.00 对这种语法没有问题,并且可以很好地编译我修改过的代码。存在对未实现方法的引用,例如 `clearkey()` 和 `flushbuffer()`,这最初会导致链接器错误,但在删除这些引用后,原始代码仍然可以正常工作。
使用 Turbo C++ 的声音函数,我将音符播放代码更改为以下内容:
while (place != NULL) {
if (place->pitch == -1)
{
nosound();
}
else {
sound(1193180 / place->pitch);
delay(place->start / 1000);
}
place = place->next;
if (kbhit())
break;
};
nosound();
原始代码接受微秒级的延迟(`place->start` 参数),需要将其转换为毫秒才能用于 `delay()`。对于音符频率,我们还需要将 `place->pitch`(最初是运行在 1.19318 MHz 的定时器芯片的输入值)转换回原始音符频率值。此外,作者使用 `-1` 表示不播放声音,这可以改为 Turbo C 的 `nosound()` 函数。最后,使用 `kbhit()` 函数来在按下键时立即终止播放。
进行此更改后,MIDIPLAY 就可以从命令行进行测试了。各种命令行参数,例如 `-t` 和 `-c` 来指定要播放的音轨或通道,在处理大型 MIDI 文件时会很有用,这些功能也是可用的。
经过 extensive 测试,修改后的 MIDI 播放器在 PC XT、286、386 和 486 甚至更新的机器上都能很好地工作,只要 PC 扬声器存在。它甚至能在我的 Core i5 机器上运行,该机器以 16 位模式启动 DOS — 由于我们使用的是 Turbo C 而非 Turbo Pascal,因此没有遇到运行时错误 200。在我收集的 MIDI 文件中,大多数 MIDI 文件都可以通过这个播放器读取并播放(或者更准确地说,发出蜂鸣声),除了少数非常复杂的文件。唯一的缺点是代码会在播放前将整个文件读入内存,对于非常大的 MIDI 文件(例如大于可用 DOS 内存的文件)会有问题。尽管如此,我认为对于 PC 扬声器 MIDI 播放器来说,结果已经足够好了。
下载次数
- MIDIPLAY 的原始版本 附带源代码
- Personal C Compiler。MIDIPLAY 的原始源代码在同一个文件夹中,可以使用 `buildrun.bat` 进行构建和运行。
- 修改后的 MIDIPLAY 源代码和二进制文件 附带了一些用于测试的 MIDI 文件,兼容 PC XT。
- Turbo C++ 3.0 二进制文件。您可以在 `BIN` 文件夹中通过运行 `TC.EXE /B MIDIPLAY.C` 来构建修改后的 MIDIPLAY 代码。