Files
tinymod/THEORY.md
Jason b84677a01e Document TinyMOD architecture and functionality
Added detailed documentation on TinyMOD's architecture and functionality, including Paula emulation, resampling, and ModPlayer logic.
2026-04-23 20:37:27 +10:00

4.9 KiB
Raw Permalink Blame History

TinyMOD is a sophisticated Protracker module player that prioritizes "accuracy" over performance. Unlike modern players that use simple linear interpolation to play samples, this code emulates the physical behavior of the Amigas Paula custom chip.

Here is a detailed breakdown of the principles of operation, divided into three layers: the Paula Emulation, the ModPlayer Logic, and the Resampling/Filtering.


1. The Paula Emulation Layer (The "Hardware")

The core of this code is the Paula class. In a real Amiga, the Paula chip didn't have a fixed sample rate. Instead, it changed the speed at which it read from memory to change the pitch.

The Pulse-Width Modulation (PWM) Emulation

Inside Voice::Render, you see this logic:

if (PWMCnt < Volume) buffer[i] += Cur.F32;
PWMCnt = (PWMCnt + 1) & 0x3f;

This is a very clever way to emulate the Amiga's volume control. Instead of just multiplying the sample by a volume fraction (0.0 to 1.0), the code uses a high-speed counter (PWMCnt). If the counter is below the Volume value (064), the signal is "on." This mimics how some hardware digital-to-analog converters (DACs) handle amplitude.

The Timing (Paula Rate)

The code operates internally at PAULARATE (approx. 3.74 MHz). This represents the master clock of the Amiga.

  • DivCnt: This acts as a countdown timer. When it hits zero, the next 8-bit sample is fetched from memory.
  • Period: This is the value from the MOD file that determines pitch. A smaller period means the counter hits zero faster, resulting in a higher pitch.

2. The Resampling & FIR Filtering (The "Magic")

Because the internal "Paula" speed is 3.7 MHz and your sound card likely runs at 48 kHz, the code must downsample the audio. If you simply picked every 78th sample, the audio would sound terrible due to aliasing (high-frequency noise folding back into the audible range).

The Sinc Filter

To solve this, kb implemented a Finite Impulse Response (FIR) Filter using a Windowed Sinc function.

  • FIRMem: In the Paula constructor, it pre-calculates a Sinc table windowed with a Hamming window.
  • RingBuf: The player renders the raw, "noisy" 3.7 MHz audio into a large ring buffer.
  • The Filter Loop: In Paula::Render, the code slides the FIR window over the ring buffer. It performs a convolution (multiplying sample values by the Sinc weights). This acts as a near-perfect low-pass filter, removing all the high-frequency garbage and leaving only a clean, "Amiga-accurate" sound.

3. The ModPlayer Layer (The "Sequencer")

The ModPlayer class is the "brain" that reads the Protracker data (notes and effects) and tells the Paula class what to do.

Period and Vibrato Tables

  • BasePTable: Contains the Amiga "Periods" for notes (C-1 to B-5).
  • PTable: A pre-computed table for all 16 "Finetune" settings. This prevents the code from having to do complex math during playback.
  • VibTable: Pre-computed Sine, Sawtooth, and Square waves for the Vibrato and Tremolo effects.

The Tick System

Music in a MOD file is not measured in milliseconds but in Ticks.

  • Speed: Defines how many ticks are in a "Row" (usually 6).
  • Tick 0: This is where "trigger" events happen (starting a new note, changing a sample).
  • Ticks 1+: This is where "continuous" effects happen (pitch slides, vibrato, volume slides).

Effect Handling

The Tick() function is a massive switch statement that handles the Protracker command set:

  • Effect 3: Tone Portamento (sliding from one note to another).
  • Effect 9: Sample Offset (starting a sample from the middle).
  • Effect E (14): The "Special" sub-commands like Pattern Loop or Retrigger.

4. Data Flow Summary

  1. Initialization: ModPlayer parses the MOD file, prepares sample loops, and pre-calculates the Period and Vibrato tables. Paula calculates the FIR filter coefficients.
  2. The Request: The Sound API (DirectSound/PortAudio) asks for a buffer of audio (e.g., 480 words).
  3. The Sequencing: ModPlayer::Render checks if it's time for a new Tick. If so, it calls Tick(), updating the Volume and Period for the 4 hardware voices.
  4. Emulation: Paula::Render sees how many output samples are needed. It calculates the "step" (how far to move in the 3.7 MHz buffer for every 48 kHz output sample).
  5. Filtering: The FIR filter convolutes the raw high-speed data into the output buffer, applying the MasterSeparation (Stereo pan) and MasterVolume.
  6. Output: The resulting floating-point data is sent to the speakers.

Why is this code unique?

Most players use Linear Interpolation. Its fast but "blurs" the sound. This code emulates the Zero-Order Hold (the "stepping" nature of the DAC) and then uses a high-quality brick-wall filter to clean it up. This results in the "crisp" yet "warm" sound characteristic of real Amiga hardware.