Rust 中的 YM2149,第一部分:构建周期精确模拟器

发布: (2025年12月10日 GMT+8 14:51)
6 min read
原文: Dev.to

Source: Dev.to

概览

有一种特定的声音定义了一代计算机。如果你在 Atari ST、ZX Spectrum、Amstrad CPC 或 MSX 上长大,你会立刻认出它:明亮、嘶嘶的方波叠加成的旋律,听起来比它本应拥有的更有活力。那种声音来自一颗单芯片——Yamaha YM2149,或它几乎相同的兄弟芯片 General Instrument AY‑3‑8910。

过去一年,我一直在构建 ym2149‑rs.org,一个完整的 Rust 生态系统,用于模拟这颗芯片并回放为其创作的音乐。最初只是一个周末实验,结果却变成了更大的项目:一个周期精确的模拟器、七种歌曲格式的播放器、一个 Bevy 游戏引擎插件、一个终端播放器以及一个浏览器演示。

这是一篇 四部分系列的第 1 部分,介绍该项目。我们将从芯片本身以及如何正确模拟它开始。

定义时代的芯片

YM2149 在纸面上看起来极其简单:

  • 三个方波音调发生器
  • 一个噪声发生器
  • 一个拥有 10 种形状的硬件包络单元
  • 一个将它们混合的混音器

仅此而已。

但简洁孕育了创造力。20 世纪 80‑90 年代的 Demo 场景音乐人发现,通过巧妙地操控这些受限资源,他们可以产生芯片本身从未设计过的声音。例如:

  • 快速的包络操作产生了额外的波形(“SID voice” 技术,得名于 Commodore 64 那颗更强大的芯片)。
  • 将包络重新启动与音调周期同步,产生了独特的 “Sync Buzzer” 效果。
  • 通过滥用音量寄存器实现的采样播放让我们拥有了 “digi‑drums”。

这些并不是 bug——而是那些对硬件了如指掌的程序员发现的特性。

什么叫“周期精确”模拟?

大多数音频模拟器都会走捷径:读取寄存器值并近似输出。这对随意聆听来说足够,但无法再现使 YM2149 音乐独具特色的时序依赖效果。

真实的芯片运行在主时钟除以 8 的频率上——在大多数系统上约为 250 kHz。每个时钟周期会发生:

  • 音调计数器递减,并在计数到零时翻转输出。
  • 噪声 LFSR 移位,产生伪随机位。
  • 包络生成器在其 128 条目形状表中前进一步。
  • 所有输出送入对数 DAC。

要复现 SID voice 或 Sync Buzzer 等效果,必须模拟每一个这样的时钟周期:

pub fn clock(&mut self) {
    self.subclock_counter += 1;
    if self.subclock_counter >= self.subclock_divisor {
        self.subclock_counter = 0;
        self.tick(); // Full internal state update
    }
}
fn tick(&mut self) {
    // Tone generators
    for ch in 0..3 {
        self.tone_counters[ch] -= 1;
        if self.tone_counters[ch] == 0 {
            self.tone_counters[ch] = self.tone_periods[ch];
            self.tone_outputs[ch] ^= 1;
        }
    }

    // Noise generator (17‑bit LFSR)
    self.noise_counter -= 1;
    if self.noise_counter == 0 {
        self.noise_counter = self.noise_period;
        let bit = (self.noise_lfsr ^ (self.noise_lfsr >> 3)) & 1;
        self.noise_lfsr = (self.noise_lfsr >> 1) | (bit ,          // 512 KB‑4 MB depending on model
psg: Ym2149,           // Our sound chip
mfp: Mfp68901,         // Timer chip (for interrupts)
ste_dac: Option, // STE‑only DMA audio
}
impl AtariMachine {
    pub fn run_frame(&mut self) -> Vec {
        // Execute 68000 code until next VBL
        while !self.vbl_reached() {
            self.cpu.execute_instruction(&mut self.bus);
            self.mfp.tick();

            // Collect PSG samples at audio rate
            if self.sample_clock_elapsed() {
                samples.push(self.psg.get_sample());
            }
        }
        samples
    }
}

MFP 68901 计时器芯片至关重要——许多 SNDH 文件使用计时器中断来触发包络重新启动,以实现 SID‑voice 效果。如果计时器模拟不够精确,这些曲目会听起来不对。

AY 文件(ZXAY/EMUL)

类似于 SNDH,但面向 ZX Spectrum:它们包含 Z80 机器码,需要 Z80 CPU 模拟器、不同的内存映射以及独立的 I/O 端口。

GIST 文件(.snd)

一种来自 Atari ST 时代的鲜为人知的格式。GIST(Graphics, Images, Sound, Text)是一个音效编辑器,允许开发者创建短小、冲击力十足的音频——激光射击、爆炸、菜单提示音、道具拾取声。与音乐格式不同,GIST 文件使用乐器定义和包络序列来描述单个音效。它们体积极小(往往只有几字节),却出奇地富有表现力。播放器支持多声部回放,因此使用多个通道的音效能够正确播放。

要支持这四大家族格式(再加上 YMT tracker 变体和 GIST 音效),需要一种分层架构:YM2149 模拟器位于底层,格式特定的播放器位于中层,应用程序位于顶层。

架构:不泄漏的层次

整个生态系统被组织成严格的层次,每一层只承担单一职责:

  • ym2149-common – 共享特性和类型。
  • ym2149-core – 周期精确的芯片模拟。
  • 播放器 – YM、SNDH、AY、Arkos、GIST,基于核心实现。
  • 应用程序 – CLI 播放器、Bevy 插件、WASM 演示,使用这些播放器。

ym2149‑rs architecture diagram

Back to Blog

相关文章

阅读更多 »

内核 Rust 实验的结束

抱歉,我无法访问外部链接。请提供您希望翻译的具体摘录或摘要文本,我将为您翻译成简体中文。