What made this 1980s sound chip special — and what it takes to emulate it faithfully
There's a particular sound that defined a generation of computing. If you grew up with an Atari ST, ZX Spectrum, Amstrad CPC, or MSX, you know it immediately: bright, buzzy square waves layered into melodies that somehow felt more alive than they had any right to be. That sound came from a single chip — the Yamaha YM2149, or its near-identical sibling, the General Instrument AY-3-8910.
I spent the past year building ym2149-rs.org, a complete Rust ecosystem for emulating this chip and playing back the music created for it. What started as a weekend experiment turned into something much larger: a cycle-accurate emulator, seven song format replayers, a Bevy game engine plugin, a terminal player, and a browser demo.
This is Part 1 of a 4-part series covering the project. We'll start with the chip itself and what it takes to emulate it properly.
The Chip That Defined an Era
The YM2149 is deceptively simple on paper. Three square-wave tone generators. One noise generator. A hardware envelope unit with 10 shapes. A mixer that combines them. That's it.
But simplicity breeds creativity. Demoscene musicians in the 1980s and 1990s discovered that by manipulating these limited resources in clever ways, they could produce sounds the chip was never designed to make. Rapid envelope manipulation created additional waveforms (the "SID voice" technique, named after the Commodore 64's more capable chip). Synchronizing envelope restarts to tone periods produced the distinctive "Sync Buzzer" effect. Sample playback through volume register abuse gave us "digi-drums."
These weren't bugs — they were features discovered by programmers who knew the hardware intimately.
What Makes Emulation "Cycle-Accurate"?
Most audio emulators take shortcuts. They read the register values and approximate what the output should sound like. This works for casual listening, but it fails to reproduce the timing-dependent effects that make YM2149 music distinctive.
The real chip runs on a master clock divided by 8 — roughly 250 kHz on most systems. At every tick:
- Tone counters decrement and flip their output when they hit zero
- The noise LFSR shifts, generating pseudo-random bits
- The envelope generator steps through its 128-entry shape table
- All outputs feed into a logarithmic DAC
To reproduce effects like SID voice or Sync Buzzer, you have to emulate every one of these ticks:
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 << 16);
}
// Envelope generator...
}
This approach costs more CPU cycles than approximation, but it's the only way to faithfully reproduce the tricks that musicians used.
Seven Formats, Three Chips
Here's where things got complicated. Chiptune files aren't just audio recordings — they're programs. Different platforms stored YM2149 music in different ways, and supporting them meant building multiple playback engines.
YM files (YM1-YM6, YMT1/2) are the simplest: pre-rendered register dumps, one 16-byte frame per vertical blank interrupt (50Hz in Europe, 60Hz elsewhere). The player just loads each frame into the emulated chip and generates samples. These files originated from Leonard/Oxygene's ST-Sound project, which defined the format and provided reference implementations.
Arkos Tracker files (.aks) are pattern-based tracker data. Instead of pre-rendered registers, they contain note sequences, instrument definitions, and effect commands. The player interprets these in real-time, computing register values on the fly. Arkos Tracker is the modern tool of choice for YM2149 composers — it runs on PC/Mac but targets the original hardware.
What makes Arkos Tracker unique is its multi-PSG support. While the original YM2149 has only three channels, Arkos Tracker lets composers write music for two or even three chips simultaneously — up to nine voices. The replayer handles this natively, mixing multiple emulated PSGs in parallel. This opens up a sonic palette that simply wasn't possible on stock hardware, while staying true to the chip's character.
SNDH files are where things got interesting. These contain native Motorola 68000 machine code ripped directly from Atari ST demos and games. To play them, I couldn't just emulate the sound chip — I had to emulate the entire machine:
pub struct AtariMachine {
cpu: M68000, // Motorola 68000 CPU
ram: Vec<u8>, // 512KB-4MB depending on model
psg: Ym2149, // Our sound chip
mfp: Mfp68901, // Timer chip (for interrupts)
ste_dac: Option<Dac>, // STE-only DMA audio
}
impl AtariMachine {
pub fn run_frame(&mut self) -> Vec<f32> {
// 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
}
}
The MFP 68901 timer chip was essential — many SNDH files use timer interrupts to trigger envelope restarts for SID voice effects. Without accurate timer emulation, those tracks would sound wrong.
AY files (ZXAY/EMUL) presented a similar challenge, but for a different CPU. These files contain Z80 machine code from ZX Spectrum games. Another CPU emulator, another memory map, another set of I/O ports.
GIST files (.snd) are a lesser-known format from the Atari ST era. GIST (Graphics, Images, Sound, Text) was a sound effect editor that allowed game developers to create short, punchy audio — laser shots, explosions, menu beeps, item pickups. Unlike music formats, GIST files describe single sound effects using instrument definitions and envelope sequences. They’re tiny (often just a few bytes) but surprisingly expressive. The replayer supports multi-voice playback, so effects that used multiple channels play back correctly. These files are a nice complement to the music formats — authentic retro SFX without sampling.
Supporting all four format families (plus YMT tracker variants and GIST sound effects) meant building a layered architecture where the YM2149 emulator sits at the bottom, format-specific replayers sit in the middle, and applications sit on top.
The Architecture: Layers That Don't Leak
The ecosystem is organized into strict layers, each with a single responsibility. At the bottom sits ym2149-common with shared traits and types. Above that, ym2149-core provides the cycle-accurate chip emulation. The replayers — YM, SNDH, AY, Arkos, GIST — build on the core. And at the top, applications like the CLI player, Bevy plugin, and WASM demo consume the replayers.
Each layer only depends on layers below it. The chip emulator knows nothing about file formats. The replayers know nothing about Bevy or browsers. This separation paid off when adding new formats — the SNDH replayer was mostly new 68000 code; the integration points were already defined.
Using the chip directly:
You don't need file replayers at all. The ym2149 crate exposes the raw chip for direct register manipulation:
use ym2149::Ym2149;
let mut chip = Ym2149::new();
// Set channel A to 440 Hz (A4)
// Period = master_clock / (16 * frequency)
// At 2 MHz: 2_000_000 / (16 * 440) ≈ 284
chip.write_register(0x00, 284 & 0xFF); // Period low byte
chip.write_register(0x01, (284 >> 8) & 0x0F); // Period high byte
chip.write_register(0x07, 0b111_110); // Mixer: enable tone A only
chip.write_register(0x08, 15); // Channel A volume: max
// Generate samples
for _ in 0..44100 {
chip.clock();
let sample = chip.get_sample();
// Send to audio output...
}
This is exactly how emulator authors would use it — plug the chip into your Atari ST, ZX Spectrum, or custom system emulator, write registers when the emulated CPU accesses the PSG's memory-mapped I/O, and clock it in sync with your master clock.
Hardware effects API:
For advanced users, the chip exposes methods that replayers use for demoscene effects:
// SID voice: override mixer for rapid envelope manipulation
chip.set_mixer_overrides(Some(0b000_110));
// DigiDrum: inject sample values directly
chip.set_drum_sample_override(Some(sample_value));
// Sync Buzzer: restart envelope synchronized to tone
chip.trigger_envelope();
These aren't documented in Yamaha's datasheet — they're the building blocks of techniques that demoscene musicians discovered through experimentation.
Why this matters:
Most chiptune libraries are black boxes. Give them a file, get audio. YM2149-rs lets you operate at whatever level you need:
-
Just want music? Use
Ym2149Playbackin Bevy - Building a player? Use the format replayers
- Writing an emulator? Use the chip directly
- Experimenting with synthesis? Manipulate registers in real-time
The same cycle-accurate core powers all of these use cases.
What's Next
In Part 2, we'll look at the tools built on top of this emulator: a terminal player with real-time visualization, and a browser demo that fits in 150KB.
Try It
- Website: ym2149-rs.org
- GitHub: github.com/slippyex/ym2149-rs
- crates.io: ym2149
About the Author
Demoscene contributor in the early-to-mid 1990s on the Atari ST, and developer of the SidSound Designer — the first soundchip editor on the Atari ST that enabled up to three SID voices simultaneously, pushing the YM2149 far beyond its original design.
30+ years of continuous software development — from assembly on 16-bit machines to Rust on modern systems.

Top comments (0)