Rust에서 YM2149, 파트 1: 사이클 정확도 에뮬레이터 구축

발행: (2025년 12월 10일 오후 03:51 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

개요

특정 사운드는 한 세대의 컴퓨팅을 정의했습니다. Atari ST, ZX Spectrum, Amstrad CPC, 혹은 MSX와 함께 자라셨다면 바로 알 수 있을 겁니다: 밝고, 윙윙거리는 사각파가 멜로디에 겹쳐져 마치 그럴 자격도 없는데도 살아있는 듯한 느낌을 주었습니다. 그 사운드는 단일 칩, 즉 Yamaha YM2149 혹은 거의 동일한 형제 칩인 General Instrument AY‑3‑8910에서 나왔습니다.

저는 지난 1년 동안 ym2149‑rs.org를 만들었습니다. 이 프로젝트는 이 칩을 에뮬레이션하고 해당 칩용으로 만든 음악을 재생하기 위한 완전한 Rust 생태계입니다. 주말 실험으로 시작했지만, 사이클‑정확 에뮬레이터, 7가지 곡 포맷 재생기, Bevy 게임 엔진 플러그인, 터미널 플레이어, 그리고 브라우저 데모까지 포함하는 훨씬 큰 프로젝트가 되었습니다.

이것은 4부 시리즈 중 1부이며, 프로젝트를 다룹니다. 먼저 칩 자체와 정확히 에뮬레이션하기 위해 필요한 것들을 살펴보겠습니다.

시대를 정의한 칩

YM2149는 겉보기엔 놀라울 정도로 단순합니다:

  • 세 개의 사각파 톤 생성기
  • 하나의 노이즈 생성기
  • 10가지 형태를 가진 하드웨어 엔벨로프 유닛
  • 이들을 결합하는 믹서

그게 전부입니다.

하지만 단순함은 창의성을 낳습니다. 1980~1990년대 데모씬 뮤지션들은 제한된 자원을 교묘히 조작함으로써 칩이 원래 설계되지 않은 사운드를 만들어낼 수 있음을 발견했습니다. 예시:

  • 빠른 엔벨프 조작으로 추가 파형을 생성 (Commodore 64의 더 강력한 칩에서 유래한 “SID voice” 기법).
  • 톤 주기에 맞춰 엔벨프 재시작을 동기화해 독특한 “Sync Buzzer” 효과를 구현.
  • 볼륨 레지스터 남용을 통한 샘플 재생으로 “digi‑drums”를 구현.

이것들은 버그가 아니라 하드웨어를 깊이 이해한 프로그래머가 발견한 기능이었습니다.

“사이클‑정확” 에뮬레이션이란?

대부분의 오디오 에뮬레이터는 레지스터 값을 읽고 출력을 근사합니다. 이는 캐주얼 청취에는 충분하지만, YM2149 음악을 특징짓는 타이밍‑의존 효과를 재현하지 못합니다.

실제 칩은 마스터 클럭을 8로 나눈 주파수(대부분의 시스템에서 약 250 kHz)로 동작합니다. 매 틱마다:

  • 톤 카운터가 감소하고 0이 되면 출력이 토글됩니다.
  • 노이즈 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)

ZX Spectrum용으로 SNDH와 비슷하지만, Z80 머신 코드를 포함하고 있어 Z80 CPU 에뮬레이터, 다른 메모리 맵, 그리고 별도 I/O 포트가 필요합니다.

GIST 파일 (.snd)

Atari ST 시대의 덜 알려진 포맷입니다. GIST(그래픽, 이미지, 사운드, 텍스트)는 짧고 강렬한 효과음(레이저 사격, 폭발, 메뉴 비프, 아이템 획득 등)을 만들기 위한 사운드‑이펙트 편집기였습니다. 음악 포맷과 달리 GIST 파일은 악기 정의와 엔벨프 시퀀스를 사용해 단일 사운드 이펙트를 기술합니다. 파일 크기는 매우 작지만(몇 바이트 수준) 놀라울 정도로 표현력이 풍부합니다. 재생기는 다중 채널 재생을 지원하므로, 여러 채널을 사용한 효과도 올바르게 재생됩니다.

네 가지 포맷군(플러스 YMT 트래커 변형 및 GIST 사운드 이펙트) 모두를 지원하려면, YM2149 에뮬레이터가 가장 아래에 위치하고, 포맷‑특화 재생기가 중간에, 애플리케이션이 위에 놓이는 계층형 구조가 필요했습니다.

아키텍처: 누수가 없는 레이어

생태계는 엄격히 구분된 레이어로 구성되며, 각 레이어는 단일 책임을 가집니다:

  • ym2149-common – 공유 트레이트와 타입.
  • ym2149-core – 사이클‑정확 칩 에뮬레이션.
  • 재생기 – YM, SNDH, AY, Arkos, GIST 등, 코어 위에 구축.
  • 애플리케이션 – CLI 플레이어, Bevy 플러그인, WASM 데모, 재생기를 활용.

ym2149‑rs architecture diagram

Back to Blog

관련 글

더 보기 »

SpecKit, Rust 및 Bevy를 활용한 게임 개발

brkrs — 재미있고 플레이 가능한 brick‑breaker 게임 및 학습 놀이터 brkrs는 Rust 🦀로 작성된 실제 플레이 가능한 Breakout/Arkanoid‑style 게임입니다 https://rust-lang.or...

커널 Rust 실험의 끝

기사 URL: https://lwn.net/Articles/1049831/ 댓글 URL: https://news.ycombinator.com/item?id=46213585 점수: 66 댓글: 22