65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2023年5月7日

CPOL

4分钟阅读

viewsIcon

9768

如何为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 播放器来说,结果已经足够好了。

下载次数

另请参阅

© . All rights reserved.