ZX Spectrum 系统之旅:声音

Hacker News Top 新闻

摘要

关于 ZX Spectrum 声音编程的技术综述,涵盖 1 位蜂鸣器和 AY-3-8910 芯片,并提供演奏 C 大调音阶的示例。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/03 19:34

# ZX Spectrum 系统之旅:声音 来源:https://bumbershootsoft.wordpress.com/2026/08/01/zx-spectrum-system-tour-sound/ 既然打靶场已经完成,现在是时候转向我们最初 ZX Spectrum 之旅中尚未涉及的部分:声音。与其他平台上已有的内容相比,这里并没有太多全新的东西;但这反过来意味着我们解决方案的核心原则已经预先准备好了;我们只需要加上 Spectrum 特有的装饰即可。 主线 Spectrum 系统提供两种声音选项: - 一个由软件控制的 1 位蜂鸣器。所有系统上都存在,其底层硬件与我们之前在 [Apple II](https://bumbershootsoft.wordpress.com/2016/09/05/making-music-on-the-apple-ii/) 或 [PC 扬声器](https://bumbershootsoft.wordpress.com/2016/12/10/beyond-beep-boop-mastering-the-pc-speaker/) 上看到的并无不同。 - 一颗 General Instrument AY-3-8910 合成器芯片,从 128K Spectrum 起的所有型号都有,并且可作为 16K 和 48K 版本的周边设备。它们与 [Atari ST 中的那颗](https://bumbershootsoft.wordpress.com/2021/05/02/atari-st-the-ay-3-8910-sound-chip/)基本相同,但时钟不同,而且我们没有内置的 BIOS 调用来与之通信。 这是我们概览式考察的一部分,就像我们的[文本模式](https://bumbershootsoft.wordpress.com/2026/05/30/zx-spectrum-system-tour-text-mode/)和[位图模式](https://bumbershootsoft.wordpress.com/2026/06/13/programming-the-zx-spectrums-bitmap-display/)考察一样,所以我们的目标不会太高;我们将在本文末尾用三个程序来演奏一段 C 大调音阶。 ## 通过 BIOS 使用蜂鸣器 1 位蜂鸣器与 Apple II 的非常相似;I/O 端口上的**`$10`**位可以被置位或清零,以设置扬声器的状态。通过精确计时该位的翻转间隔来产生特定音调。我们稍后会讲到实现这一点的代码,但我们可以把这件事往后放,因为*我们不必自己动手*——BASIC 中的**`BEEP`**命令已经处理了这件事,它通过转发到一个通用的例程来实现,而我们可以自己利用这个例程。**`BEEPER`**例程位于内存地址**`$03B5`**(https://skoolkid.github.io/rom/asm/03B5.html),老实说它比我为 6502 系统编写的版本更聪明。**`HL`**寄存器以 4 个周期(一条空操作指令的长度)为单位指定延迟时间,而它获取这种精度的方式,与我们用于可编程、周期精确的 6502 延迟的技术颇为相似。**`DE`**寄存器指定音符的持续时间,这有点不方便,因为它实际上指定的是音符应包含的波长数量。长度相同但音高不同的音符需要不同的持续时间代码。 不过,**`BEEP`**命令本身依赖于以距中央 C 的半音数来表示音符,以及以秒为单位的持续时间。我们可以查看它自己的实现,位于**`$03F8`**,来了解如何获得我们需要的值。这是通过系统的浮点引擎完成的,所以我们实际上可以直接从注释中得到两个简洁的公式: - 对于频率为 *f* Hz 的音调,频率代码`HL`由公式 *437500/f – 30.125* 提供。 - 对于频率为 *f* Hz、持续时间为 *t* 秒的音符,持续时间代码`DE`简化为 *f×t*。 用几行 Python 就可以将这些公式应用到 C 大调音阶的频率上,得到供例程使用的频率代码和持续时间代码。下面是我们播放程序: ``` org $7000 ld b,8 ; Play 8 notes ld hl,scale ; Load from pointer loop: push bc ld c,(hl) ; Load frequency into BC inc hl ld b,(hl) inc hl ld e,(hl) ; Load duration into HL inc hl ld d,(hl) inc hl push hl ; Stash pointer... ld h,b ; ... Copy BC to HL... ld l,c call $03b5 ; ... and let the ROM do the rest pop hl pop bc djnz loop ret scale: dw $066a,$0082,$05b3,$0092,$0511,$00a4,$04c6,$00ae dw $043d,$00c3,$03c4,$00dc,$0357,$00f6,$0325,$0105 ``` 这和我最初的 Apple II 声音播放例程一样属于“半波长”系统,但它精细得多的时序控制意味着音调的整体精度要*好得多*。基本上,我在最初那篇文章中面临的折衷在这里完全不存在,我们拥有专用 PSG 所应有的那种控制力。老实说,这真的很棒。这里唯一真正的缺点就是,像这样在两边都需要自己进行除法运算有点麻烦;如果我们想要一个管理任意频率和持续时间音调的通用例程,就需要自带除法器。不过,我们很快就会看到,在 16KB Spectrum 上我们没有其他选择,只能依赖这个例程。 不过,我们有点操之过急了。让我们先看看如何手动完成,然后再真正遇到在 16K 系统上会碰到的问题。 ## 直接使用蜂鸣器 BIOS 例程足够好了,如果要我自己写一个振荡器函数,我希望它运行在一个完全不同的原理上。幸运的是,我正好有这样的原理:我受 SID 和 Ensoniq 芯片内部原理启发而创建的[基于频率而非半波长的 16 位系统](https://bumbershootsoft.wordpress.com/2024/10/19/a-musical-interlude/)。由于 Z80 内置了对 16 位运算的支持,这应该比 6502 代码直接得多。 基本思想是反复将频率代码加到一个运行中的计数器上,并在每次计数器溢出时切换扬声器。棘手之处在于确保我们不会干扰其他任何东西,因为 I/O 端口**`$FE`**身兼三职: - 低三位设置边框颜色。我们希望保留**`BORDCR`**系统变量中的任何值。 - **`$08`**位控制的是音频输出,但那是针对磁带机的控制,而非扬声器。这些操作会以某种方式干扰电压,我在[平台指南](https://bumbershootsoft.wordpress.com/platform-guide-sinclair-zx-spectrum-and-friends/)中链接的 [Korth 文档](https://problemkaputt.de/zxdocs.htm)详细介绍了这些细节,但我们只需像系统 ROM 那样,始终强制该位为开启状态。 - **`$10`**位才是我们真正要操作的位,用于控制扬声器音频。 我们首先直接实现这一切,并尽量少用分支: ``` sound: ld hl,0 ; Counter starts at 0 ld a,($5c48) ; BORDCR and $38 ; Extract border color rrca ; and shift to low 3 bits rrca rrca or $08 ; Disable tape audio di ; Don't get interrupted by IRQ push af .loop: pop af add hl,de ; Add frequency to counter jr nc,1F ; If it overflows... xor $10 ; ... toggle speaker output out ($fe),a 1 dec bc ; De

相似文章

在Amiga上零CPU占用播放Atari ST音乐

Hacker News Top

本文详细介绍了一种在Amiga上播放Atari ST音乐的技术,通过利用Amiga的PAULA芯片模拟YM2149音效芯片,实现零CPU占用,使CPU能够专注于其他任务,如渲染图形。

Boriel BASIC

Hacker News Top

Boriel BASIC 是一款现代开源的 BASIC 编译器 SDK,主要为 ZX Spectrum 设计,提供增强功能、整数类型以及内联汇编支持,适用于复古游戏开发。