ym2149-sndh-replayer
SNDH file parser and Atari ST machine emulation for YM2149 chiptune playback.
Overview
This crate provides playback support for SNDH files, a popular format for Atari ST chiptune music. SNDH files contain native Motorola 68000 machine code that must be executed on an emulated Atari ST to produce audio.
Features
- ICE! 2.4 Decompression: Many SNDH files are compressed with ICE! packer
- SNDH Header Parsing: Extract metadata (title, author, year, subsong info)
- 68000 CPU Emulation: Via the
r68kcrate with cycle-accurate timing - MFP 68901 Timer Emulation: For SID voice and timer-based effects
- STE DAC Emulation: DMA audio support for STe-specific SNDH files (50kHz mode with averaging)
- YM2149 Sound Chip: Using
ym2149crate for cycle-accurate emulation - ChiptunePlayer Trait: Unified interface compatible with other replayers
Install
[]
= "0.9"
Usage
use ;
// Load SNDH file
let data = read?;
let mut player = load_sndh?;
println!;
println!;
println!;
// Initialize first subsong
player.init_subsong?;
player.play;
// Generate audio samples
let mut buffer = vec!; // ~20ms at 44100Hz
player.generate_samples_into;
Rendering to i16
For direct audio output, use render_i16:
let mut buffer = vec!;
let loop_count = player.render_i16;
SNDH Format
SNDH is a standard format for Atari ST music that embeds original 68000 replay code:
| Offset | Description |
|---|---|
| +0 | BRA instruction (jump over header) |
| +12 | "SNDH" magic |
| +16 | Tag-based metadata (TITL, COMM, YEAR, ##, etc.) |
| Entry+0 | Init routine (D0 = subsong number) |
| Entry+4 | Exit/cleanup routine |
| Entry+8 | Play routine (called at player rate) |
Supported Tags
TITL- Song titleCOMM- Composer/authorYEAR- Year of creation##nn- Number of subsongs!#nn- Default subsongTA/TB/TC/TD- Timer and replay rateTIME- Duration per subsong (in seconds)HDNS- End of header marker
Architecture
┌─────────────────────────────────────────┐
│ SndhPlayer │
├─────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ AtariMachine │ │
│ │ ┌───────────┐ ┌─────────────────┐ │ │
│ │ │ M68000 │ │ AtariMemory │ │ │
│ │ │ CPU │ │ ┌─────────────┐ │ │ │
│ │ │ (r68k) │ │ │ YM2149 │ │ │ │
│ │ └───────────┘ │ │ (ym2149) │ │ │ │
│ │ │ ├─────────────┤ │ │ │
│ │ │ │ MFP68901 │ │ │ │
│ │ │ │ (timers) │ │ │ │
│ │ │ ├─────────────┤ │ │ │
│ │ │ │ 4MB RAM │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
Emulation Details
The Atari ST machine emulation provides:
- 4MB RAM at 0x000000 - 0x3FFFFF
- YM2149 PSG at 0xFF8800 - 0xFF88FF
- MFP 68901 at 0xFFFA00 - 0xFFFA25
- STE DAC at 0xFF8900 - 0xFF893F (DMA audio with microwire volume control)
- Basic GEMDOS/XBIOS traps for malloc and timer setup
Timer interrupts (used by SID voice effects) are handled by executing the interrupt handler code at audio sample rate.
Hardware Accuracy
This emulator aims for high accuracy to correctly replay even the most demanding SNDH files. The following optimizations bring the emulation to approximately 98-99% hardware accuracy for SNDH audio output.
CPU Emulation (r68k)
The 68000 CPU is emulated via a customized r68k backend with the following enhancements:
| Feature | Description |
|---|---|
| Cycle Granularity | 4-cycle boundary alignment matching Atari ST GLUE/MMU wait states |
| Exception Cycles | Correct cycle counts for interrupts (44 cycles) and TRAPs (34 cycles) |
| YM2149 Access Latency | Additional cycles for PSG register access timing |
| Cycle Counter API | add_cycles() method for modeling external delays |
MFP 68901 Timer Emulation
The MFP timer system provides cycle-accurate interrupt generation:
| Feature | Description |
|---|---|
| FP16 Clock Precision | High-precision lookup table using 16-bit fixed-point math eliminates cumulative rounding errors. Uses exact ratio 3125/960 for CPU-to-MFP clock conversion |
| Dual-Mode Architecture | Separate legacy (sample-based) and cycle-accurate timer states for seek compatibility |
| Relative Cycle Tracking | cycles_until_fire uses delta-based tracking instead of absolute cycles |
| Phase Preservation | Virtual cycle accumulation during seek preserves timer phase relationships. Multi-timer effects (SID voice, digidrum) maintain correct phase after seeking |
| Prescale Switch Delay | Per MC68901 manual: changing prescaler while running causes indeterminate 1-200 timer clock delay. Modeled as ~100 clocks |
| Cycle-Accurate Counter Read | TxDR reads return the actual countdown value based on CPU cycle, not just the last sampled value |
| State Consistency | Clean reset of all timer states after seek (counters, pending flags, in-service flags) |
| Interrupt Latency | Models MFP-internal propagation delay (~10 cycles). CPU-side latency is implicit through instruction-boundary checking |
Nested Interrupt Support
Full MFP interrupt priority handling enables complex multi-timer drivers:
| Feature | Description |
|---|---|
| Priority Levels | GPI7=15, Timer A=13, Timer B=8, Timer C=5, Timer D=4 |
| Nesting | Higher-priority interrupts can preempt lower-priority handlers |
| Stack Protection | Maximum nesting depth of 4 prevents stack overflow |
| In-Service Tracking | Proper acknowledge/end-of-interrupt handling per MFP specification |
STE DMA Audio
Complete STE sound DMA emulation with bus contention modeling:
| Feature | Description |
|---|---|
| Sample Rates | 6.25 kHz, 12.5 kHz, 25 kHz, 50 kHz |
| Mono/Stereo | Both modes supported |
| 50kHz Averaging | Special handling for Tao MS3/Quartet-style 4-voice interleaved output |
| Bus Contention | DMA transfers steal ~8 CPU cycles per sample, affecting timer-relative timing |
| Microwire Interface | LMC1992 volume/bass/treble control |
Timing Model
┌─────────────────────────────────────────────────────────────┐
│ CPU Execution Loop │
├─────────────────────────────────────────────────────────────┤
│ 1. Execute instruction (r68k with Musashi cycle tables) │
│ 2. Add DMA bus contention cycles (if STE DAC active) │
│ 3. Check MFP timer fire + latency threshold │
│ 4. Dispatch interrupt if: │
│ - Timer fired (cycle-accurate check) │
│ - Priority > current handler (nested interrupt support) │
│ - Nesting depth < 4 │
│ 5. Add exception cycles (44) on interrupt entry │
│ 6. Execute handler, RTE adds 20 cycles (r68k native) │
└─────────────────────────────────────────────────────────────┘
Accuracy Comparison
| Component | Basic Emulation | This Implementation |
|---|---|---|
| YM2149 Writes | Sample-rate | Cycle-accurate queue |
| MFP Timers | Integer math | FP16 precision LUT |
| MFP Prescale Switch | Instant | ~100 clock delay modeled |
| MFP Counter Read | Last sample | Cycle-accurate value |
| Interrupts | Single-level | Nested with priorities |
| Exception Cycles | Ignored | 44/34/20 cycles modeled |
| Interrupt Latency | Instant | 10+ cycles (variable) |
| DMA Contention | None | ~8 cycles per transfer |
| Seek Support | State corruption | Phase-preserving sync |
Remaining Gaps (for 100%)
For reference, these features are not emulated but rarely affect SNDH playback:
- CPU prefetch queue (affects only cycle-exact raster effects)
- Cycle-exact bus arbitration (sub-instruction timing)
- Blitter interaction (not used in audio code)
- GLUE/MMU exact wait state patterns
Related Crates
- ym2149 - Core YM2149 chip emulation
- ym2149-common - Common traits
- ym2149-ym-replayer - YM file playback
- ym2149-arkos-replayer - Arkos Tracker playback
- ym2149-ay-replayer - AY file playback
Credits
Based on the sndh-player C++ implementation by Arnaud Carré (Leonard/Oxygene).
ICE! 2.4 depacker based on the public domain C implementation by Hans Wessels.
68000 emulation via the r68k crate by Martin Helgesson, with custom cycle-accuracy enhancements.
License
MIT License - see LICENSE.