From 1f17cf04b468e0f46b54ab330889b643077b9253 Mon Sep 17 00:00:00 2001
From: Jason <154414066+bou-samra@users.noreply.github.com>
Date: Fri, 24 Apr 2026 19:36:30 +1000
Subject: [PATCH] Refactor: modularize code into separate files with enhanced
documentation
- Split paula.cpp and modplayer.cpp into separate compilation units
- Created config.h for centralized configuration constants
- Created types.h for type definitions and utility functions
- Added comprehensive comments throughout all files
- Improved code organization with clear section dividers
- Maintained all original functionality and logic
---
Makefile | 44 +++-
README.md | 165 ++++++++++---
src/config.h | 32 +++
src/main.cpp | 282 ++++++++++++++++++++++
src/modplayer.cpp | 598 ++++++++++++++++++++++++++++++++++++++++++++++
src/modplayer.h | 154 ++++++++++++
src/paula.cpp | 192 +++++++++++++++
src/paula.h | 88 +++++++
src/types.h | 147 ++++++++++++
9 files changed, 1660 insertions(+), 42 deletions(-)
create mode 100644 src/config.h
create mode 100644 src/main.cpp
create mode 100644 src/modplayer.cpp
create mode 100644 src/modplayer.h
create mode 100644 src/paula.cpp
create mode 100644 src/paula.h
create mode 100644 src/types.h
diff --git a/Makefile b/Makefile
index 5d46159..0fbd854 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,40 @@
-## build TinyMOD
-## jbs - paragonsoft
+# =================== TinyMOD Makefile ===================
+# Build configuration for TinyMOD MOD player
-all: tinymod
+# === Compiler Settings ===
+CC = g++
+CFLAGS = -O2 -Wall -Wextra
-tinymod: tinymod.cpp
-## g++ -o tinymod tinymod.cpp
- g++ -o tinymod `pkg-config --libs alsa` tinymod.cpp -lm -L . -l:libportaudio.a
+# === Source Files ===
+SOURCES = src/main.cpp src/paula.cpp src/modplayer.cpp
+OBJECTS = $(SOURCES:.cpp=.o)
+TARGET = tinymod
+
+# === Libraries ===
+# PortAudio library (static link)
+LIBS = -L. -l:libportaudio.a -lm
+
+# === PortAudio Configuration ===
+# Optional: link with ALSA for Linux
+PORTAUDIO_LIBS = $(shell pkg-config --libs alsa 2>/dev/null)
+LIBS += $(PORTAUDIO_LIBS)
+
+# === Build Rules ===
+all: $(TARGET)
+
+# Link object files to create executable
+$(TARGET): $(OBJECTS)
+ $(CC) -o $@ $^ $(LIBS)
+ @echo "Build complete: $(TARGET)"
+
+# Compile source files to object files
+src/%.o: src/%.cpp
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+# Clean build artifacts
clean:
- rm tinymod
+ rm -f $(OBJECTS) $(TARGET)
+ @echo "Clean complete"
+
+# Phony targets (not actual files)
+.PHONY: all clean
diff --git a/README.md b/README.md
index 354258f..0e3c29f 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,139 @@
-# TinyMOD
-Written by Tammo "kb" Hinrichs in 2007
-This source code is hereby placed into the public domain. Use, distribute,
-modify, misappropriate and generally abuse it as you wish. Giving credits
-would be nice of course.
+# TinyMOD - Amiga MOD File Player
-This player includes an Amiga Paula chip "emulation" that faithfully recreates
-how it sounds when a sample is resampled using a master clock of 3.5 MHz. Yes,
-rendering at this rate and downsampling to the usual 48KHz takes quite a bit
-of CPU. Feel free to replace this part with some conventional mixing routines
-if authenticity isn't your goal and you really need Protracker MOD support
-for any other reason...
+TinyMOD is a high-fidelity Amiga MOD (Protracker format) file player that authentically recreates the sound characteristics of original Amiga hardware through software emulation of the Paula audio chip.
-The code should be pretty portable, all OS/platform dependent stuff is
-at the top. Code for testing is at the bottom.
+## Features
-You'll need some kind of sound output that calls back the player providing
-it a stereo interleaved single float buffer to write into (0dB=1.0).
+- **Authentic Paula Chip Emulation**: Faithfully replicates the Amiga Paula chip sound by operating at the original master clock rate (3.5 MHz) and downsampling to standard output rates
+- **Protracker Support**: Full support for MOD file format with all standard effects
+- **High-Quality Resampling**: Uses windowed-sinc FIR filtering for excellent audio quality
+- **4-Channel Audio**: Stereo output with proper channel mixing and panning
+- **Effect Processing**: Complete MOD effect support including:
+ - Vibrato and tremolo
+ - Pitch slides and portamento
+ - Volume slides
+ - Arpeggio
+ - Pattern looping and breaking
+ - And more...
-## Changelog:
+## Building
-2024-03-09: (Jason Bou-Samra)
-* changes to main() routine to make executable from Linux command line interface
-* modularised paula and modplayer classes into seperate files
-* added sound output using port audio
-* sprinkled comments throughout source code, and general source tidyup
-* created makefile
+### Requirements
-2007-12-07: (Tammo "kb" Hinrichs)
-* fixed 40x and 4x0 vibrato effects (jogeir - tiny tunes)
-* fixed pattern loop (olof gustafsson - pinball illusions)
-* fixed fine volslide down (olof gustafsson - pinball illusions)
-* included some external header files
-* cleanups
+- GCC/G++ compiler
+- PortAudio library (libportaudio.a)
+- ALSA development libraries (for Linux)
+- Make
-2007-12-06: (Tammo "kb" Hinrichs)
-* first "release". Note to self: Don't post stuff on pouet.net when drunk.
+### Compilation
-## Compilation
-type `make` or `g++ -o tinymod pkg-config --libs alsa tinymod.cpp -lm -L . -l:libportaudio.a` on the command line
+```bash
+make
+```
-## Author(s)
-Tammo "kb" Hinrichs
-Jason Bou-Samra
+The Makefile will:
+- Compile the modular source files
+- Link with PortAudio and system libraries
+- Create the `tinymod` executable
+
+### Cleanup
+
+```bash
+make clean
+```
+
+## Usage
+
+### Playing a MOD File
+
+```bash
+./tinymod music.mod
+```
+
+### Getting Help
+
+```bash
+./tinymod --help
+```
+
+### About
+
+```bash
+./tinymod --about
+```
+
+## Architecture
+
+The codebase is organized into modular components:
+
+### Core Modules
+
+- **`src/types.h`**: Type definitions, memory utilities, and mathematical functions
+- **`src/config.h`**: Centralized configuration constants
+- **`src/paula.h`/`src/paula.cpp`**: Amiga Paula chip emulator
+- **`src/modplayer.h`/`src/modplayer.cpp`**: MOD file parser and playback engine
+- **`src/main.cpp`**: Command-line interface and audio system integration
+
+### Design Principles
+
+- **Modular Design**: Each component has a single responsibility
+- **Well-Documented**: Extensive comments explaining algorithms and MOD format details
+- **Portable**: Platform-independent code with PortAudio for audio I/O
+- **Authentic**: Preserves original Paula chip behavior and MOD effect processing
+
+## Technical Details
+
+### Paula Chip Emulation
+
+The Paula emulator processes audio at the original Amiga clock rate (3,740,000 Hz) and applies:
+- PWM (Pulse Width Modulation) for sample playback
+- Ring buffer for sample storage
+- Windowed-sinc FIR filter for high-quality resampling
+
+### MOD Format Support
+
+Supports the following MOD file variants:
+- Standard MOD (4 channels, 16-32 samples)
+- M.K. format (32 samples)
+- FLT4 (Startrekker, 32 samples)
+- M!K! (extended patterns, 32 samples)
+
+### Effect Processing
+
+All major Protracker effects are implemented:
+- 0x: Arpeggio
+- 1x: Slide up
+- 2x: Slide down
+- 3x: Tone portamento
+- 4x: Vibrato
+- 5x: Tone portamento + volume slide
+- 6x: Vibrato + volume slide
+- 7x: Tremolo
+- 9x: Sample offset
+- Ax: Volume slide
+- Bx: Position jump
+- Cx: Set volume
+- Dx: Pattern break
+- Ex: Extended effects
+- Fx: Set speed/BPM
+
+## Authors
+
+- **Tammo "kb" Hinrichs** - Original Paula emulator implementation (2007)
+- **Jason Bou-samra** - Code refactoring, modularization, and PortAudio integration (2024)
+
+## License
+
+This software is released into the **public domain**. Use, distribute, and modify freely without restriction.
+
+## Notes
+
+- High-quality Paula emulation requires significant CPU resources due to the 3.5 MHz -> 48 KHz resampling ratio
+- Playback duration is configurable via NUM_SECONDS constant in config.h
+- Audio output can be customized through PortAudio configuration
+
+## References
+
+- Protracker MOD Format Documentation
+- Amiga Hardware Specifications
+- Paula Chip Audio Hardware Documentation
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..387408b
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,32 @@
+// =================== Configuration Constants ===================
+// All configuration defines for TinyMOD in one central location
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+// === Audio Configuration ===
+#define NUM_SECONDS (1000) // Duration to play (in seconds)
+#define SAMPLE_RATE (96000) // Playback sample rate (Hz)
+#define FRAMES_PER_BUFFER (0x10000) // Audio buffer size (65536 frames)
+
+// === Paula Chip Emulation ===
+// These constants define the Amiga Paula chip parameters
+const int PAULARATE = 3740000; // Paula chip master clock (~3.546895MHz DAC base clock)
+const int OUTRATE = 48000; // Output/playback rate (48KHz)
+const int OUTFPS = 50; // Frames per second (50Hz - PAL)
+
+// === Paula Ring Buffer ===
+const int PAULA_RBSIZE = 4096; // Paula ring buffer (circular buffer) size
+const int PAULA_FIR_WIDTH = 512; // Finite Impulse Response (FIR) filter width
+
+// === MOD Format Constants ===
+const int MOD_CHANNELS = 4; // Standard MOD files have 4 channels
+const int MOD_SAMPLES = 32; // Maximum 32 samples per MOD file
+const int MOD_PATTERNS = 128; // Maximum 128 patterns per MOD file
+const int MOD_PATTERN_ROWS = 64; // 64 rows per pattern
+const int MOD_PATTERN_SIZE = 1024; // 1024 bytes per pattern
+
+// === Utility Macros ===
+#define cls() printf("\033[H\033[J") // ANSI escape codes to clear screen
+
+#endif // CONFIG_H
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..4c1c1d7
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,282 @@
+// =================== TinyMOD Player - Main Entry Point ===================
+// TinyMOD: An Amiga MOD file player with authentic Paula chip emulation
+// Supports playback of Protracker MOD files with full effect support
+//
+// Authors:
+// Tammo "kb" Hinrichs - Original Paula emulator (2007)
+// Jason Bou-samra - Refactoring and PortAudio integration (2024)
+//
+// This program is released into the public domain.
+// Use, distribute, modify as you wish.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "types.h"
+#include "config.h"
+#include "paula.h"
+#include "modplayer.h"
+
+// =================== Audio Configuration Constants ===================
+const int SAMPLE_RATE_INTERNAL = 96000; // Internal Paula emulation rate
+const int SAMPLE_RATE_OUTPUT = 48000; // Output audio sample rate
+
+// =================== Utility Functions ===================
+
+// Load MOD file from disk into memory
+// Returns pointer to allocated memory containing file data
+// Sets file_size to the number of bytes read
+sU8 *load_mod_file(const char *filename, size_t &file_size)
+{
+ // Open file for reading in binary mode
+ FILE *fh = fopen(filename, "rb");
+ if (!fh)
+ {
+ perror("fopen");
+ return NULL;
+ }
+
+ // Get file size using stat()
+ struct stat sb;
+ if (stat(filename, &sb) == -1)
+ {
+ perror("stat");
+ fclose(fh);
+ return NULL;
+ }
+
+ file_size = sb.st_size;
+
+ // Allocate memory for file (4MB max)
+ if (file_size > 4 * 1024 * 1024)
+ {
+ fprintf(stderr, "Error: MOD file too large (max 4MB)\n");
+ fclose(fh);
+ return NULL;
+ }
+
+ sU8 *mod = (sU8 *)malloc(file_size);
+ if (!mod)
+ {
+ perror("malloc");
+ fclose(fh);
+ return NULL;
+ }
+
+ // Read file into memory
+ if (fread(mod, file_size, 1, fh) != 1)
+ {
+ perror("fread");
+ free(mod);
+ fclose(fh);
+ return NULL;
+ }
+
+ fclose(fh);
+ return mod;
+}
+
+// PortAudio error handler
+// Prints error information and terminates program
+void handle_pa_error(PaError err)
+{
+ if (err == paNoError)
+ return;
+
+ fprintf(stderr, "PortAudio Error: %s\n", Pa_GetErrorText(err));
+
+ // Print additional host API error information if available
+ if (err == paUnanticipatedHostError)
+ {
+ const PaHostErrorInfo *hostErrorInfo = Pa_GetLastHostErrorInfo();
+ fprintf(stderr, "Host API Error: #%ld\n", hostErrorInfo->errorCode);
+ fprintf(stderr, "Host API: %d\n", hostErrorInfo->hostApiType);
+ fprintf(stderr, "Details: %s\n", hostErrorInfo->errorText);
+ }
+
+ Pa_Terminate();
+ exit(1);
+}
+
+// Print usage information
+void print_usage(const char *program_name)
+{
+ printf("Usage: %s [|OPTION]\n\n", program_name);
+ printf("OPTIONS:\n");
+ printf(" --about Display about message\n");
+ printf(" --help Display this help message\n");
+}
+
+// Print about information
+void print_about()
+{
+ printf("TinyMOD - Amiga MOD File Player\n\n");
+ printf("An Amiga MOD file player that replicates the authentic sound\n");
+ printf("characteristics of an Amiga via Paula chip emulation.\n\n");
+ printf("Authors:\n");
+ printf(" Tammo \"kb\" Hinrichs - Paula emulator (2007)\n");
+ printf(" Jason Bou-samra - Refactoring and integration (2024)\n\n");
+ printf("Released into the public domain.\n");
+}
+
+// =================== Main Program ===================
+int main(int argc, const char **argv)
+{
+ // === Check Command Line Arguments ===
+ if (argc != 2)
+ {
+ print_usage(argv[0]);
+ return 1;
+ }
+
+ const char *filename = argv[1];
+
+ // Handle --about option
+ if (!strcmp(filename, "--about"))
+ {
+ print_about();
+ return 0;
+ }
+
+ // Handle --help option
+ if (!strcmp(filename, "--help"))
+ {
+ print_usage(argv[0]);
+ return 0;
+ }
+
+ // === Load MOD File ===
+ printf("Loading MOD file: %s\n", filename);
+ size_t mod_size = 0;
+ sU8 *mod_data = load_mod_file(filename, mod_size);
+
+ if (!mod_data)
+ {
+ fprintf(stderr, "Error: Failed to load MOD file\n");
+ return 1;
+ }
+
+ printf("Loaded %zu bytes\n", mod_size);
+
+ // === Initialize PortAudio ===
+ printf("Initializing PortAudio...\n");
+ PaError err = Pa_Initialize();
+ if (err != paNoError)
+ handle_pa_error(err);
+
+ // === Configure Output Stream ===
+ PaStreamParameters outputParameters;
+ outputParameters.device = Pa_GetDefaultOutputDevice();
+
+ if (outputParameters.device == paNoDevice)
+ {
+ fprintf(stderr, "Error: No default output device found\n");
+ Pa_Terminate();
+ free(mod_data);
+ return 1;
+ }
+
+ outputParameters.channelCount = 2; // Stereo output
+ outputParameters.sampleFormat = paFloat32; // 32-bit float samples
+ outputParameters.suggestedLatency =
+ Pa_GetDeviceInfo(outputParameters.device)->defaultLowOutputLatency;
+ outputParameters.hostApiSpecificStreamInfo = NULL;
+
+ // === Open Audio Stream ===
+ PaStream *stream;
+ err = Pa_OpenStream(
+ &stream,
+ NULL, // No input
+ &outputParameters,
+ SAMPLE_RATE_OUTPUT, // Output sample rate
+ FRAMES_PER_BUFFER, // Frames per buffer
+ paClipOff, // Don't clip output
+ NULL, // No callback
+ NULL); // No user data
+
+ if (err != paNoError)
+ handle_pa_error(err);
+
+ // === Start Audio Stream ===
+ err = Pa_StartStream(stream);
+ if (err != paNoError)
+ handle_pa_error(err);
+
+ // === Initialize MOD Player and Paula Emulator ===
+ Paula paula; // Create Paula emulator instance
+ ModPlayer player(&paula, mod_data); // Create MOD player with MOD file
+
+ // === Display Playback Information ===
+ cls(); // Clear screen
+ printf("TinyMOD - Amiga MOD File Player\n");
+ printf("================================\n\n");
+ printf("Currently playing: %s\n", player.Name);
+ printf("Duration: %d seconds\n", NUM_SECONDS);
+ printf("Sample rate: %d Hz (Paula: %d Hz)\n", SAMPLE_RATE_OUTPUT, SAMPLE_RATE_INTERNAL);
+ printf("\nPress Ctrl+C to stop\n\n");
+
+ // === Calculate Playback Parameters ===
+ sInt nwrite = FRAMES_PER_BUFFER / 2; // Samples per buffer
+ sInt buffer_count = (NUM_SECONDS * SAMPLE_RATE_OUTPUT) / FRAMES_PER_BUFFER;
+
+ // === Allocate Audio Buffers ===
+ sF32 *mixbuffer = (sF32 *)malloc(nwrite * 2 * sizeof(sF32));
+ if (!mixbuffer)
+ {
+ fprintf(stderr, "Error: Failed to allocate audio buffer\n");
+ Pa_CloseStream(stream);
+ Pa_Terminate();
+ free(mod_data);
+ return 1;
+ }
+
+ // === Main Playback Loop ===
+ printf("Playing...\n");
+ for (int i = 0; i < buffer_count; i++)
+ {
+ // Render MOD file audio
+ player.RenderProxy(&player, mixbuffer, nwrite);
+
+ // Write audio to stream
+ err = Pa_WriteStream(stream, mixbuffer, nwrite);
+ if (err != paNoError)
+ {
+ fprintf(stderr, "Warning: Write error - %s\n", Pa_GetErrorText(err));
+ }
+
+ // Print progress
+ if ((i + 1) % 10 == 0)
+ {
+ printf(".");
+ fflush(stdout);
+ }
+ }
+
+ printf("\n\n");
+
+ // === Shutdown Audio ===
+ err = Pa_StopStream(stream);
+ if (err != paNoError)
+ handle_pa_error(err);
+
+ // Allow stream to finish draining
+ Pa_Sleep(1000);
+
+ err = Pa_CloseStream(stream);
+ if (err != paNoError)
+ handle_pa_error(err);
+
+ Pa_Terminate();
+
+ // === Cleanup ===
+ free(mixbuffer);
+ free(mod_data);
+
+ printf("Playback complete. Goodbye!\n");
+ return 0;
+}
diff --git a/src/modplayer.cpp b/src/modplayer.cpp
new file mode 100644
index 0000000..2da01fc
--- /dev/null
+++ b/src/modplayer.cpp
@@ -0,0 +1,598 @@
+// =================== MOD File Player Implementation ===================
+// Implementation of MOD file parsing and playback
+
+#include "modplayer.h"
+#include
+#include
+
+// =================== Static Data Initialization ===================
+// Base period table for MOD format
+// Period values define the playback rate (frequency) of samples
+// Lower period = higher frequency = higher pitch
+sInt ModPlayer::BasePTable[61] = {
+ 0, // Dummy entry
+ // C-0 to B-0 (Octave 0)
+ 1712, 1616, 1525, 1440, 1357, 1281, 1209, 1141, 1077, 1017, 961, 907,
+ // C-1 to B-1 (Octave 1)
+ 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453,
+ // C-2 to B-2 (Octave 2)
+ 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226,
+ // C-3 to B-3 (Octave 3)
+ 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113,
+ // C-4 to B-4 (Octave 4)
+ 107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57,
+};
+
+// Period and vibrato tables (filled in by constructor)
+sInt ModPlayer::PTable[16][60];
+sInt ModPlayer::VibTable[3][15][64];
+
+// =================== Sample::Prepare ===================
+void ModPlayer::Sample::Prepare()
+{
+ // MOD files store multi-byte values in big-endian format
+ // Convert to native byte order (usually little-endian on modern systems)
+ sSwapEndian(Length); // 16-bit value: swap bytes
+ sSwapEndian(LoopStart); // 16-bit value: swap bytes
+ sSwapEndian(LoopLen); // 16-bit value: swap bytes
+
+ // Clamp finetune to valid range (-8 to +7)
+ Finetune &= 0x0f; // Keep only lower 4 bits
+ if (Finetune >= 8)
+ Finetune -= 16; // Convert from unsigned to signed
+}
+
+// =================== Pattern Constructor ===================
+ModPlayer::Pattern::Pattern()
+{
+ // Zero out all event data
+ sZeroMem(this, sizeof(Pattern));
+}
+
+// =================== Pattern::Load ===================
+// Parse pattern data from MOD file
+// Each note event is 4 bytes: (sample/period_hi, period_lo, effect, parameter)
+void ModPlayer::Pattern::Load(sU8 *ptr)
+{
+ for (sInt row = 0; row < 64; row++)
+ {
+ for (sInt ch = 0; ch < 4; ch++)
+ {
+ Event &e = Events[row][ch];
+
+ // Parse sample number (upper 4 bits of byte 0 + upper 4 bits of byte 2)
+ e.Sample = (ptr[0] & 0xf0) | (ptr[2] >> 4);
+
+ // Parse effect type (lower 4 bits of byte 2)
+ e.FX = ptr[2] & 0x0f;
+
+ // Parse effect parameter (byte 3)
+ e.FXParm = ptr[3];
+
+ // Parse note/period (bytes 0,1)
+ // Convert period value to note number using period table
+ e.Note = 0;
+ sInt period = (sInt(ptr[0] & 0x0f) << 8) | ptr[1];
+
+ if (period)
+ {
+ // Find closest matching note in period table
+ sInt bestd = sAbs(period - BasePTable[0]);
+ for (sInt i = 1; i <= 60; i++)
+ {
+ sInt d = sAbs(period - BasePTable[i]);
+ if (d < bestd)
+ {
+ bestd = d;
+ e.Note = i;
+ }
+ }
+ }
+
+ ptr += 4; // Move to next note event
+ }
+ }
+}
+
+// =================== Chan Constructor ===================
+ModPlayer::Chan::Chan()
+{
+ sZeroMem(this, sizeof(Chan));
+}
+
+// =================== Chan::GetPeriod ===================
+// Calculate Paula period from note number and finetune
+sInt ModPlayer::Chan::GetPeriod(sInt offs, sInt fineoffs)
+{
+ // Apply finetune offset
+ sInt ft = FineTune + fineoffs;
+
+ // Normalize finetune to -8 to +7 range
+ while (ft > 7)
+ {
+ offs++; // Increase octave
+ ft -= 16;
+ }
+ while (ft < -8)
+ {
+ offs--; // Decrease octave
+ ft += 16;
+ }
+
+ // Look up period from table using note + octave offset
+ return Note ? (PTable[ft & 0x0f][sClamp(Note + offs - 1, 0, 59)]) : 0;
+}
+
+// =================== Chan::SetPeriod ===================
+void ModPlayer::Chan::SetPeriod(sInt offs, sInt fineoffs)
+{
+ if (Note)
+ Period = GetPeriod(offs, fineoffs);
+}
+
+// =================== ModPlayer::CalcTickRate ===================
+// Calculate samples per tick based on BPM
+// Formula: samples = (125 * SAMPLE_RATE) / (BPM * OUTFPS)
+void ModPlayer::CalcTickRate(sInt bpm)
+{
+ TickRate = (125 * OUTRATE) / (bpm * OUTFPS);
+}
+
+// =================== ModPlayer::TrigNote ===================
+// Trigger a note: start playing a sample on a channel
+void ModPlayer::TrigNote(sInt ch, const Pattern::Event &e)
+{
+ Chan &c = Chans[ch];
+ Paula::Voice &v = P->V[ch];
+ const Sample &s = Samples[c.Sample];
+ sInt offset = 0;
+
+ // Effect 9: Sample offset
+ if (e.FX == 9)
+ offset = c.FXBuf[9] << 8;
+
+ // Trigger note unless it's a portamento effect (3 or 5)
+ if (e.FX != 3 && e.FX != 5)
+ {
+ c.SetPeriod();
+
+ // Handle looping vs. one-shot samples
+ if (s.LoopLen > 1)
+ // Looping sample
+ v.Trigger(SData[c.Sample], 2 * (s.LoopStart + s.LoopLen), 2 * s.LoopLen, offset);
+ else
+ // One-shot sample
+ v.Trigger(SData[c.Sample], v.SampleLen = 2 * s.Length, 1, offset);
+
+ // Reset vibrato/tremolo position unless set to "don't retrigger"
+ if (!c.VibRetr)
+ c.VibPos = 0;
+ if (!c.TremRetr)
+ c.TremPos = 0;
+ }
+}
+
+// =================== ModPlayer::Reset ===================
+// Reset playback to beginning of song
+void ModPlayer::Reset()
+{
+ CalcTickRate(125); // Default BPM = 125
+ Speed = 6; // Default speed = 6 ticks per row
+ TRCounter = 0;
+ CurTick = 0;
+ CurRow = 0;
+ CurPos = 0;
+ Delay = 0;
+}
+
+// =================== ModPlayer::Tick ===================
+// Process one tick of MOD playback
+// This is called SPEED times per row
+// Handles note triggers, effect processing, and timing
+void ModPlayer::Tick()
+{
+ const Pattern &p = Patterns[PatternList[CurPos]];
+ const Pattern::Event *re = p.Events[CurRow];
+
+ // Process each of the 4 channels
+ for (sInt ch = 0; ch < 4; ch++)
+ {
+ const Pattern::Event &e = re[ch];
+ Paula::Voice &v = P->V[ch];
+ Chan &c = Chans[ch];
+ const sInt fxpl = e.FXParm & 0x0f; // Low nibble of effect parameter
+ sInt TremVol = 0; // Tremolo volume change
+
+ if (!CurTick) // First tick of row: trigger new notes and set up effects
+ {
+ // Set sample if specified
+ if (e.Sample)
+ {
+ c.Sample = e.Sample;
+ c.FineTune = Samples[c.Sample].Finetune;
+ c.Volume = Samples[c.Sample].Volume;
+ }
+
+ // Store effect parameter in buffer
+ if (e.FXParm)
+ c.FXBuf[e.FX] = e.FXParm;
+
+ // Trigger note (unless it's a portamento effect)
+ if (e.Note && (e.FX != 14 || ((e.FXParm >> 4) != 13)))
+ {
+ c.Note = e.Note;
+ TrigNote(ch, e);
+ }
+
+ // Handle various effects on first tick
+ switch (e.FX)
+ {
+ case 4: // Vibrato
+ case 6: // Vibrato + volume slide
+ if (c.FXBuf[4] & 0x0f)
+ c.VibAmpl = c.FXBuf[4] & 0x0f; // Low nibble = amplitude
+ if (c.FXBuf[4] & 0xf0)
+ c.VibSpeed = c.FXBuf[4] >> 4; // High nibble = speed
+ c.SetPeriod(0, VibTable[c.VibWave][(c.VibAmpl) - 1][c.VibPos]);
+ break;
+
+ case 7: // Tremolo (volume modulation)
+ if (c.FXBuf[7] & 0x0f)
+ c.TremAmpl = c.FXBuf[7] & 0x0f;
+ if (c.FXBuf[7] & 0xf0)
+ c.TremSpeed = c.FXBuf[7] >> 4;
+ TremVol = VibTable[c.TremWave][(c.TremAmpl) - 1][c.TremPos];
+ break;
+
+ case 12: // Set volume
+ c.Volume = sClamp(e.FXParm, 0, 64);
+ break;
+
+ case 14: // Special effects (Exx)
+ if (fxpl)
+ c.FXBuf14[e.FXParm >> 4] = fxpl;
+
+ switch (e.FXParm >> 4)
+ {
+ case 1: // Fine slide up
+ c.Period = sMax(113, c.Period - c.FXBuf14[1]);
+ break;
+ case 2: // Fine slide down
+ c.Period = sMin(856, c.Period + c.FXBuf14[2]);
+ break;
+ case 4: // Set vibrato waveform
+ c.VibWave = fxpl & 3;
+ if (c.VibWave == 3)
+ c.VibWave = 0;
+ c.VibRetr = fxpl & 4;
+ break;
+ case 5: // Set finetune
+ c.FineTune = fxpl;
+ if (c.FineTune >= 8)
+ c.FineTune -= 16;
+ break;
+ case 7: // Set tremolo waveform
+ c.TremWave = fxpl & 3;
+ if (c.TremWave == 3)
+ c.TremWave = 0;
+ c.TremRetr = fxpl & 4;
+ break;
+ case 9: // Retrigger note
+ if (c.FXBuf14[9] && !e.Note)
+ TrigNote(ch, e);
+ c.RetrigCount = 0;
+ break;
+ case 10: // Fine volume slide up
+ c.Volume = sMin(c.Volume + c.FXBuf14[10], 64);
+ break;
+ case 11: // Fine volume slide down
+ c.Volume = sMax(c.Volume - c.FXBuf14[11], 0);
+ break;
+ case 14: // Pattern delay
+ Delay = c.FXBuf14[14];
+ break;
+ }
+ break;
+
+ case 15: // Set speed/BPM
+ if (e.FXParm)
+ if (e.FXParm <= 32)
+ Speed = e.FXParm; // Set ticks per row
+ else
+ CalcTickRate(e.FXParm); // Set BPM
+ break;
+ }
+ }
+ else // Subsequent ticks: apply continuous effects
+ {
+ switch (e.FX)
+ {
+ case 0: // Arpeggio: cycle between note and two pitch variations
+ if (e.FXParm)
+ {
+ sInt no = 0;
+ switch (CurTick % 3)
+ {
+ case 1:
+ no = e.FXParm >> 4; // First variation
+ break;
+ case 2:
+ no = e.FXParm & 0x0f; // Second variation
+ break;
+ }
+ c.SetPeriod(no);
+ }
+ break;
+
+ case 1: // Slide up
+ c.Period = sMax(113, c.Period - c.FXBuf[1]);
+ break;
+
+ case 2: // Slide down
+ c.Period = sMin(856, c.Period + c.FXBuf[2]);
+ break;
+
+ case 3: // Tone portamento (slide to note)
+ case 5: // Tone portamento + volume slide
+ if (e.FX == 5)
+ {
+ // Volume slide
+ if (c.FXBuf[5] & 0xf0)
+ c.Volume = sMin(c.Volume + (c.FXBuf[5] >> 4), 0x40);
+ else
+ c.Volume = sMax(c.Volume - (c.FXBuf[5] & 0x0f), 0);
+ }
+ // Portamento
+ {
+ sInt np = c.GetPeriod();
+ if (c.Period > np)
+ c.Period = sMax(c.Period - c.FXBuf[3], np);
+ else if (c.Period < np)
+ c.Period = sMin(c.Period + c.FXBuf[3], np);
+ }
+ break;
+
+ case 4: // Vibrato
+ case 6: // Vibrato + volume slide
+ if (e.FX == 6)
+ {
+ // Volume slide
+ if (c.FXBuf[6] & 0xf0)
+ c.Volume = sMin(c.Volume + (c.FXBuf[6] >> 4), 0x40);
+ else
+ c.Volume = sMax(c.Volume - (c.FXBuf[6] & 0x0f), 0);
+ }
+ // Vibrato
+ c.SetPeriod(0, VibTable[c.VibWave][c.VibAmpl - 1][c.VibPos]);
+ c.VibPos = (c.VibPos + c.VibSpeed) & 0x3f;
+ break;
+
+ case 7: // Tremolo
+ TremVol = VibTable[c.TremWave][c.TremAmpl - 1][c.TremPos];
+ c.TremPos = (c.TremPos + c.TremSpeed) & 0x3f;
+ break;
+
+ case 10: // Volume slide
+ if (c.FXBuf[10] & 0xf0)
+ c.Volume = sMin(c.Volume + (c.FXBuf[10] >> 4), 0x40);
+ else
+ c.Volume = sMax(c.Volume - (c.FXBuf[10] & 0x0f), 0);
+ break;
+
+ case 11: // Position jump
+ if (CurTick == Speed - 1)
+ {
+ CurRow = -1;
+ CurPos = e.FXParm;
+ }
+ break;
+
+ case 13: // Pattern break
+ if (CurTick == Speed - 1)
+ {
+ CurPos++;
+ CurRow = (10 * (e.FXParm >> 4) + (e.FXParm & 0x0f)) - 1;
+ }
+ break;
+
+ case 14: // Special effects (Exx continued)
+ switch (e.FXParm >> 4)
+ {
+ case 6: // Pattern loop
+ if (!fxpl)
+ c.LoopStart = CurRow; // Set loop start
+ else if (CurTick == Speed - 1)
+ {
+ if (c.LoopCount < fxpl)
+ {
+ CurRow = c.LoopStart - 1;
+ c.LoopCount++;
+ }
+ else
+ c.LoopCount = 0;
+ }
+ break;
+
+ case 9: // Retrigger note
+ if (++c.RetrigCount == c.FXBuf14[9])
+ {
+ c.RetrigCount = 0;
+ TrigNote(ch, e);
+ }
+ break;
+
+ case 12: // Cut note
+ if (CurTick == c.FXBuf14[12])
+ c.Volume = 0;
+ break;
+
+ case 13: // Delay note
+ if (CurTick == c.FXBuf14[13])
+ TrigNote(ch, e);
+ break;
+ }
+ break;
+ }
+ }
+
+ // Apply tremolo to final volume and update Paula voice
+ v.Volume = sClamp(c.Volume + TremVol, 0, 64);
+ v.Period = c.Period;
+ }
+
+ // Advance tick counter and handle row/position advancement
+ CurTick++;
+ if (CurTick >= Speed * (Delay + 1))
+ {
+ CurTick = 0;
+ CurRow++;
+ Delay = 0;
+ }
+
+ // Advance to next pattern after 64 rows
+ if (CurRow >= 64)
+ {
+ CurRow = 0;
+ CurPos++;
+ }
+
+ // Loop back to beginning when reaching end of song
+ if (CurPos >= PositionCount)
+ CurPos = 0;
+}
+
+// =================== ModPlayer Constructor ===================
+// Load and parse MOD file
+ModPlayer::ModPlayer(Paula *p, sU8 *moddata) : P(p)
+{
+ // Build period table for all finetune values (-8 to +7)
+ // This adjusts the base periods by fractional semitones
+ for (sInt ft = 0; ft < 16; ft++)
+ {
+ // Convert finetune index to signed value
+ sInt rft = -((ft >= 8) ? ft - 16 : ft);
+
+ // Calculate frequency multiplier for this finetune
+ sF32 fac = sFPow(2.0f, sF32(rft) / (12.0f * 16.0f));
+
+ // Generate period table for this finetune
+ for (sInt i = 0; i < 60; i++)
+ PTable[ft][i] = sInt(sF32(BasePTable[i]) * fac + 0.5f);
+ }
+
+ // Build vibrato/tremolo waveform tables
+ // Three waveforms: sine, ramp, square
+ for (sInt ampl = 0; ampl < 15; ampl++)
+ {
+ sF32 scale = ampl + 1.5f; // Amplitude scaling
+ sF32 shift = 0; // DC offset
+
+ for (sInt x = 0; x < 64; x++)
+ {
+ // Waveform 0: Sine
+ VibTable[0][ampl][x] = sInt(scale * sFSin(x * sFPi / 32.0f) + shift);
+ // Waveform 1: Ramp down
+ VibTable[1][ampl][x] = sInt(scale * ((63 - x) / 31.5f - 1.0f) + shift);
+ // Waveform 2: Square
+ VibTable[2][ampl][x] = sInt(scale * ((x < 32) ? 1 : -1) + shift);
+ }
+ }
+
+ // === Parse MOD File ===
+ // Extract song name (first 20 bytes)
+ memcpy(Name, moddata, 20);
+ Name[20] = 0; // Null terminate
+ moddata += 20;
+
+ // Initialize sample array
+ SampleCount = 32; // Default to 32 samples
+ ChannelCount = 4; // MOD format always has 4 channels
+ Samples = (Sample *)(moddata - sizeof(Sample));
+ moddata += 15 * sizeof(Sample); // Skip first 15 sample headers
+
+ // Check MOD format tag (determines sample count)
+ sU32 &tag = *(sU32 *)(moddata + 130 + 16 * sizeof(Sample));
+ switch (tag)
+ {
+ case '.K.M': // M.K. (Michael Kleps) - standard 4-channel MOD
+ case '4TLF': // FLT4 (Startrekker 4 channel)
+ case '!K!M': // M!K! (more than 100 patterns)
+ SampleCount = 32; // These formats use 32 samples
+ break;
+ }
+
+ // Skip extra sample headers if needed
+ if (SampleCount > 16)
+ moddata += (SampleCount - 16) * sizeof(Sample);
+
+ // Prepare all samples (convert from big-endian format)
+ for (sInt i = 1; i < SampleCount; i++)
+ Samples[i].Prepare();
+
+ // Load song structure
+ PositionCount = *moddata; // Number of patterns in sequence
+ moddata += 2; // Skip unused byte
+ memcpy(PatternList, moddata, 128); // Load pattern order list
+ moddata += 128;
+
+ // Skip format tag if present
+ if (SampleCount > 15)
+ moddata += 4;
+
+ // Find highest pattern number used
+ PatternCount = 0;
+ for (sInt i = 0; i < 128; i++)
+ PatternCount = sMax(PatternCount, PatternList[i] + 1);
+
+ // Load all patterns
+ for (sInt i = 0; i < PatternCount; i++)
+ {
+ Patterns[i].Load(moddata);
+ moddata += 1024; // Each pattern is 1024 bytes
+ }
+
+ // Load sample data
+ sZeroMem(SData, sizeof(SData));
+ for (sInt i = 1; i < SampleCount; i++)
+ {
+ SData[i] = (sS8 *)moddata;
+ moddata += 2 * Samples[i].Length; // Samples are stored as words (2 bytes)
+ }
+
+ // Initialize playback state
+ Reset();
+}
+
+// =================== ModPlayer::Render ===================
+// Generate audio samples for playback
+sU32 ModPlayer::Render(sF32 *buf, sU32 len)
+{
+ while (len)
+ {
+ // Calculate how many samples to generate before next tick
+ sInt todo = sMin(len, TRCounter);
+
+ if (todo)
+ {
+ // Render Paula audio
+ P->Render(buf, todo);
+ buf += 2 * todo; // Stereo: 2 samples per frame
+ len -= todo;
+ TRCounter -= todo;
+ }
+ else
+ {
+ // Time for next MOD tick
+ Tick();
+ TRCounter = TickRate; // Reset counter for next tick
+ }
+ }
+ return 1;
+}
+
+// =================== ModPlayer::RenderProxy ===================
+// Static wrapper function for use as C-style callback
+sU32 ModPlayer::RenderProxy(void *parm, sF32 *buf, sU32 len)
+{
+ return ((ModPlayer *)parm)->Render(buf, len);
+}
diff --git a/src/modplayer.h b/src/modplayer.h
new file mode 100644
index 0000000..b00c39a
--- /dev/null
+++ b/src/modplayer.h
@@ -0,0 +1,154 @@
+// =================== MOD File Player ===================
+// Plays Amiga MOD (Protracker) format music files
+// Handles all MOD format parsing, effect processing, and timing
+
+#ifndef MODPLAYER_H
+#define MODPLAYER_H
+
+#include "types.h"
+#include "config.h"
+#include "paula.h"
+
+// =================== ModPlayer Class ===================
+// Represents a MOD file player with playback control and effect processing
+class ModPlayer
+{
+private:
+ // === Paula Reference ===
+ Paula *P; // Pointer to Paula emulator instance
+
+ // === Period & Frequency Tables ===
+ // These tables convert MOD note values to Paula periods
+ static sInt BasePTable[5 * 12 + 1]; // Base period table (5 octaves x 12 semitones + extra)
+ static sInt PTable[16][60]; // Period table for each finetune (-8 to +7)
+ static sInt VibTable[3][15][64]; // Vibrato/tremolo lookup tables
+
+ // === Playback State ===
+ sInt Speed; // Ticks per row (default 6)
+ sInt TickRate; // Number of samples per tick
+ sInt TRCounter; // Tick rate counter (samples remaining)
+
+ sInt CurTick; // Current tick within row (0 to Speed-1)
+ sInt CurRow; // Current pattern row (0-63)
+ sInt CurPos; // Current song position (pattern index)
+ sInt Delay; // Pattern delay in ticks
+
+ // === Sample Storage ===
+ sS8 *SData[32]; // Pointers to sample data
+ sInt SampleCount; // Number of samples in file
+ sInt ChannelCount; // Number of channels (always 4 for standard MOD)
+
+ // === Song Structure ===
+ sU8 PatternList[128]; // List of which patterns to play in which order
+ sInt PositionCount; // Number of positions in song
+ sInt PatternCount; // Number of unique patterns
+
+ // =================== Sample Structure ===================
+ // Represents a single instrument/sample in MOD format
+ struct Sample
+ {
+ char Name[22]; // Sample name (22 bytes in MOD format)
+ sU16 Length; // Sample length in words (1 word = 2 bytes)
+ sS8 Finetune; // Finetune value (-8 to +7)
+ sU8 Volume; // Default volume (0-64)
+ sU16 LoopStart; // Loop start position in words
+ sU16 LoopLen; // Loop length in words
+
+ // Convert sample data from big-endian MOD format to native format
+ // MOD files store multi-byte values in big-endian format
+ void Prepare();
+ } *Samples; // Pointer to sample array
+
+ // =================== Pattern Structure ===================
+ // Represents a 64-row pattern with 4 channels of note data
+ struct Pattern
+ {
+ // Single note event (one channel, one row)
+ struct Event
+ {
+ sInt Sample; // Sample number (0-31)
+ sInt Note; // Note number (0-60)
+ sInt FX; // Effect type (0-15)
+ sInt FXParm; // Effect parameter value
+ } Events[64][4]; // 64 rows x 4 channels
+
+ // Zero out pattern data
+ Pattern();
+
+ // Parse pattern data from MOD file format
+ void Load(sU8 *ptr);
+ } Patterns[128]; // Array of patterns
+
+ // =================== Channel State Structure ===================
+ // Maintains playback state for a single audio channel
+ struct Chan
+ {
+ sInt Note; // Current note number
+ sInt Period; // Current period (Paula playback rate)
+ sInt Sample; // Current sample number
+ sInt FineTune; // Current finetune value
+ sInt Volume; // Current volume (0-64)
+ sInt FXBuf[16]; // Effect command values (command 0-15)
+ sInt FXBuf14[16]; // Effect parameters for command 14 (special)
+ sInt LoopStart; // Pattern loop start row
+ sInt LoopCount; // Pattern loop counter
+ sInt RetrigCount; // Retrigger counter (for effect 9)
+ sInt VibWave; // Vibrato waveform (0-3)
+ sInt VibRetr; // Vibrato retrigger flag
+ sInt VibPos; // Vibrato position (0-63)
+ sInt VibAmpl; // Vibrato amplitude (1-15)
+ sInt VibSpeed; // Vibrato speed
+ sInt TremWave; // Tremolo waveform (0-3)
+ sInt TremRetr; // Tremolo retrigger flag
+ sInt TremPos; // Tremolo position (0-63)
+ sInt TremAmpl; // Tremolo amplitude (1-15)
+ sInt TremSpeed; // Tremolo speed
+
+ // Initialize channel state to all zeros
+ Chan();
+
+ // Calculate Paula period from note and finetune values
+ sInt GetPeriod(sInt offs = 0, sInt fineoffs = 0);
+
+ // Set Paula period
+ void SetPeriod(sInt offs = 0, sInt fineoffs = 0);
+ } Chans[4]; // Array of 4 channels
+
+ // =================== Playback Control ===================
+ // Calculate number of samples per tick based on BPM
+ // Higher BPM = faster playback
+ void CalcTickRate(sInt bpm);
+
+ // Trigger a note on a channel (start playing sample)
+ void TrigNote(sInt ch, const Pattern::Event &e);
+
+ // Reset playback state to beginning of song
+ void Reset();
+
+ // Process one "tick" of MOD playback
+ // Updates effects, advances notes, handles timing
+ void Tick();
+
+public:
+ // Song name from MOD file
+ char Name[21];
+
+ // ModPlayer constructor: load and initialize MOD file
+ // p: pointer to Paula emulator
+ // moddata: pointer to MOD file data in memory
+ ModPlayer(Paula *p, sU8 *moddata);
+
+ // =================== Audio Rendering ===================
+ // Render audio samples into buffer
+ // Called repeatedly by audio system to generate sound
+ // buf: output buffer for stereo samples (float, interleaved L/R)
+ // len: number of samples to generate
+ // Returns: number of samples generated
+ sU32 Render(sF32 *buf, sU32 len);
+
+ // Static callback function for audio systems
+ // Allows this to be used as a C-style callback
+ static sU32 __stdcall RenderProxy(void *parm, sF32 *buf, sU32 len);
+};
+
+#endif // MODPLAYER_H
diff --git a/src/paula.cpp b/src/paula.cpp
new file mode 100644
index 0000000..00a74ee
--- /dev/null
+++ b/src/paula.cpp
@@ -0,0 +1,192 @@
+// =================== Paula Chip Emulator Implementation ===================
+// Implementation of the Amiga Paula chip audio hardware emulator
+
+#include "paula.h"
+#include
+
+// =================== Voice::Render ===================
+// Render voice samples into output buffer using PWM
+void Paula::Voice::Render(sF32 *buffer, sInt samples)
+{
+ if (!Sample)
+ return; // No sample data, nothing to render
+
+ sU8 *smp = (sU8 *)Sample;
+ for (sInt i = 0; i < samples; i++)
+ {
+ if (!DivCnt)
+ {
+ // Load next sample: convert from 8-bit unsigned to 32-bit float
+ // XOR with 0x80 converts unsigned to signed format
+ // Shift left 15 bits and OR with mantissa to create float representation
+ Cur.U32 = ((smp[Pos] ^ 0x80) << 15) | 0x40000000;
+ Cur.F32 -= 3.0f; // Normalize to proper range
+
+ // Advance to next sample, handle looping
+ if (++Pos == SampleLen)
+ Pos -= LoopLen; // Jump back to loop start
+
+ DivCnt = Period; // Reset period counter
+ }
+
+ // PWM (Pulse Width Modulation) output
+ // Only output if PWM counter is below volume level
+ if (PWMCnt < Volume)
+ buffer[i] += Cur.F32;
+
+ PWMCnt = (PWMCnt + 1) & 0x3f; // 6-bit PWM counter (0-63)
+ DivCnt--; // Decrement period counter
+ }
+}
+
+// =================== Voice::Trigger ===================
+// Trigger a voice to start playing a sample
+void Paula::Voice::Trigger(sS8 *smp, sInt sl, sInt ll, sInt offs)
+{
+ Sample = smp; // Set sample pointer
+ SampleLen = sl; // Set sample length
+ LoopLen = ll; // Set loop length
+ Pos = sMin(offs, SampleLen - 1); // Set start position (clamped)
+}
+
+// =================== Paula::CalcFrag ===================
+// Generate audio fragments at Paula rate
+// This function renders all 4 voice channels into the output buffer
+void Paula::CalcFrag(sF32 *out, sInt samples)
+{
+ // Zero out output buffer (stereo: 2 channels)
+ sZeroMem(out, sizeof(sF32) * samples);
+ sZeroMem(out + RBSIZE, sizeof(sF32) * samples);
+
+ // Render each of the 4 Paula voices
+ for (sInt i = 0; i < 4; i++)
+ {
+ // Paula has stereo hardwired:
+ // Voices 0,3 go to left channel
+ // Voices 1,2 go to right channel
+ if (i == 1 || i == 2)
+ V[i].Render(out + RBSIZE, samples); // Right channel
+ else
+ V[i].Render(out, samples); // Left channel
+ }
+}
+
+// =================== Paula::Calc ===================
+// Fill ring buffer with new samples at Paula rate
+void Paula::Calc()
+{
+ // Calculate number of samples needed
+ sInt RealReadPos = ReadPos - FIR_WIDTH - 1;
+ sInt samples = (RealReadPos - WritePos) & (RBSIZE - 1);
+
+ // Generate samples in two chunks if wrapping around ring buffer
+ sInt todo = sMin(samples, RBSIZE - WritePos);
+ CalcFrag(RingBuf + WritePos, todo);
+
+ if (todo < samples)
+ {
+ WritePos = 0;
+ todo = samples - todo;
+ CalcFrag(RingBuf, todo);
+ }
+
+ WritePos += todo;
+}
+
+// =================== Paula::Render ===================
+// Resample from Paula rate (3.74 MHz) to output rate (48 KHz)
+// Uses windowed-sinc FIR filtering for high-quality resampling
+void Paula::Render(sF32 *outbuf, sInt samples)
+{
+ // Calculate resampling ratio
+ const sF32 step = sF32(PAULARATE) / sF32(OUTRATE); // ~77.92
+
+ // Calculate stereo panning coefficients
+ // Maintains constant power panning: vol_L^2 + vol_R^2 = constant
+ const sF32 pan = 0.5f + 0.5f * MasterSeparation;
+ const sF32 vm0 = MasterVolume * sFSqrt(pan);
+ const sF32 vm1 = MasterVolume * sFSqrt(1 - pan);
+
+ // Generate output samples
+ for (sInt s = 0; s < samples; s++)
+ {
+ // Check if we need to generate more Paula-rate samples
+ sInt ReadEnd = ReadPos + FIR_WIDTH + 1;
+ if (WritePos < ReadPos)
+ ReadEnd -= RBSIZE;
+ if (ReadEnd > WritePos)
+ Calc(); // Generate more Paula samples
+
+ // FIR filter: convolution with filter coefficients
+ sF32 outl0 = 0, outl1 = 0; // Left channel (two taps for interpolation)
+ sF32 outr0 = 0, outr1 = 0; // Right channel
+
+ // Calculate offset into ring buffer
+ sInt offs = (ReadPos - FIR_WIDTH - 1) & (RBSIZE - 1);
+
+ // Load first sample pair
+ sF32 vl = RingBuf[offs];
+ sF32 vr = RingBuf[offs + RBSIZE];
+
+ // Convolve with FIR filter coefficients
+ for (sInt i = 1; i < 2 * FIR_WIDTH - 1; i++)
+ {
+ sF32 w = FIRMem[i]; // FIR coefficient
+ outl0 += vl * w; // Accumulate left channel tap 0
+ outr0 += vr * w; // Accumulate right channel tap 0
+
+ // Advance to next sample
+ offs = (offs + 1) & (RBSIZE - 1);
+ vl = RingBuf[offs];
+ vr = RingBuf[offs + RBSIZE];
+
+ outl1 += vl * w; // Accumulate left channel tap 1
+ outr1 += vr * w; // Accumulate right channel tap 1
+ }
+
+ // Linear interpolation between two filter taps
+ sF32 outl = sLerp(outl0, outl1, ReadFrac);
+ sF32 outr = sLerp(outr0, outr1, ReadFrac);
+
+ // Apply panning and output (constant power stereo mixing)
+ *outbuf++ = vm0 * outl + vm1 * outr; // Output sample (mixed)
+ *outbuf++ = vm1 * outl + vm0 * outr; // Swapped for stereo separation
+
+ // Advance read position with fractional interpolation
+ ReadFrac += step;
+ sInt rfi = sInt(ReadFrac);
+ ReadPos = (ReadPos + rfi) & (RBSIZE - 1);
+ ReadFrac -= rfi; // Keep only fractional part
+ }
+}
+
+// =================== Paula::Constructor ===================
+// Initialize Paula emulator and build FIR filter
+Paula::Paula()
+{
+ // Build windowed-sinc FIR filter for low-pass resampling
+ sF32 *FIRTable = FIRMem + FIR_WIDTH; // Point to center of FIR array
+
+ // Calculate filter coefficients
+ sF32 yscale = sF32(OUTRATE) / sF32(PAULARATE); // Output/Paula rate ratio
+ sF32 xscale = sFPi * yscale; // Frequency scaling
+
+ // Generate windowed-sinc filter taps
+ // Windowed sinc: sinc(x) * hamming_window(x)
+ for (sInt i = -FIR_WIDTH; i <= FIR_WIDTH; i++)
+ {
+ sF32 sinc = sFSinc(sF32(i) * xscale);
+ sF32 hamming = sFHamming(sF32(i) / sF32(FIR_WIDTH - 1));
+ FIRTable[i] = yscale * sinc * hamming;
+ }
+
+ // Initialize ring buffer
+ sZeroMem(RingBuf, sizeof(RingBuf));
+ ReadPos = 0;
+ ReadFrac = 0;
+ WritePos = FIR_WIDTH;
+
+ // Initialize master volume and panning
+ MasterVolume = 0.66f; // Default to 66% volume
+ MasterSeparation = 0.5f; // Default to 50:50 stereo separation
+}
diff --git a/src/paula.h b/src/paula.h
new file mode 100644
index 0000000..575f511
--- /dev/null
+++ b/src/paula.h
@@ -0,0 +1,88 @@
+// =================== Paula Chip Emulator ===================
+// Emulates the Amiga Paula chip audio hardware
+// Faithfully recreates the sound of the Amiga by resampling
+// at the Paula master clock (3.5 MHz) and downsampling to output rate
+
+#ifndef PAULA_H
+#define PAULA_H
+
+#include "types.h"
+#include "config.h"
+
+// =================== Paula Class ===================
+// Represents the Amiga Paula audio chip emulator
+class Paula
+{
+public:
+ // === FIR Filter Configuration ===
+ static const sInt FIR_WIDTH = 512; // Finite Impulse Response filter width
+ sF32 FIRMem[2 * FIR_WIDTH + 1]; // FIR filter coefficients (1025 taps)
+
+ // =================== Voice Structure ===================
+ // Represents a single audio channel (Paula has 4 voices)
+ struct Voice
+ {
+ private:
+ sInt Pos; // Current sample position in waveform
+ sInt PWMCnt, DivCnt; // PWM counter and period divider
+ sIntFlt Cur; // Current sample value (float/int union)
+
+ public:
+ sS8 *Sample; // Pointer to sample data
+ sInt SampleLen; // Total sample length in words
+ sInt LoopLen; // Loop length in words
+ sInt Period; // Audio period (sample playback rate)
+ sInt Volume; // Volume (0-64)
+
+ // Voice constructor: initialize all values to default/zero
+ Voice()
+ : Period(65535), Volume(0), Sample(0), Pos(0), PWMCnt(0), DivCnt(0), LoopLen(1)
+ {
+ Cur.F32 = 0;
+ }
+
+ // Render voice samples into output buffer
+ // Uses PWM (Pulse Width Modulation) to convert sample data
+ void Render(sF32 *buffer, sInt samples);
+
+ // Trigger voice: start playing a sample
+ // smp: pointer to sample data
+ // sl: sample length in words
+ // ll: loop length in words
+ // offs: offset into sample (default 0)
+ void Trigger(sS8 *smp, sInt sl, sInt ll, sInt offs = 0);
+ };
+
+ Voice V[4]; // Array of 4 voices (Paula has 4 audio channels)
+
+ // =================== Ring Buffer ===================
+ // Circular buffer stores audio samples at Paula rate before resampling
+ static const sInt RBSIZE = 4096; // Ring buffer size in samples
+ sF32 RingBuf[2 * RBSIZE]; // Stereo ring buffer (left + right channels)
+ sInt WritePos; // Current write position in ring buffer
+ sInt ReadPos; // Current read position in ring buffer
+ sF32 ReadFrac; // Fractional position for interpolation
+
+ // Generate audio fragments at Paula rate (3.74 MHz)
+ // This is where the actual Paula emulation happens
+ void CalcFrag(sF32 *out, sInt samples);
+
+ // Calculate and fill ring buffer with new Paula-rate samples
+ void Calc();
+
+ // =================== Output Rendering ===================
+ // Master volume control (0.0 = silent, 1.0 = full volume)
+ sF32 MasterVolume;
+
+ // Stereo separation control (0.0 = mono, 1.0 = full stereo)
+ sF32 MasterSeparation;
+
+ // Resample from Paula rate to output rate and apply FIR filter
+ // Uses windowed-sinc FIR filtering for high-quality resampling
+ void Render(sF32 *outbuf, sInt samples);
+
+ // Paula constructor: initialize FIR filter and ring buffer
+ Paula();
+};
+
+#endif // PAULA_H
diff --git a/src/types.h b/src/types.h
new file mode 100644
index 0000000..d1c86cc
--- /dev/null
+++ b/src/types.h
@@ -0,0 +1,147 @@
+// =================== Type Definitions & Utilities ===================
+// Standard type definitions, memory utilities, and math functions
+
+#ifndef TYPES_H
+#define TYPES_H
+
+#include // Fixed size integer types (C standard library)
+#include // Mathematical operations (C standard library)
+#include // String handling (C standard library)
+#include // C++ fixed size integer types
+
+// =================== Type Definitions ===================
+// Signed integer types
+typedef int sInt; // Signed integer (platform dependent)
+typedef signed char sS8; // 8-bit signed integer
+typedef signed short sS16; // 16-bit signed integer
+typedef signed long sS32; // 32-bit signed integer
+typedef int64_t sS64; // 64-bit signed integer
+
+// Unsigned integer types
+typedef unsigned int sUInt; // Unsigned integer (platform dependent)
+typedef unsigned char sU8; // 8-bit unsigned integer
+typedef unsigned short sU16; // 16-bit unsigned integer
+typedef unsigned long sU32; // 32-bit unsigned integer
+typedef uint64_t sU64; // 64-bit unsigned integer
+
+// Floating point types
+typedef float sF32; // 32-bit floating point (single precision)
+typedef double sF64; // 64-bit floating point (double precision)
+
+// Boolean type
+typedef signed int sBool; // Boolean (0=false, non-zero=true)
+
+// Character type
+typedef char sChar; // Character
+
+// =================== Float/Integer Union ===================
+// Used for bit-level manipulation of floating point values
+union sIntFlt {
+ sU32 U32; // 32-bit unsigned integer view
+ sF32 F32; // 32-bit floating point view
+};
+
+// =================== Memory Utilities ===================
+// Zero out a memory block
+inline void sZeroMem(void *dest, sInt size)
+{
+ memset(dest, 0, size);
+}
+
+// =================== Math Utilities ===================
+// Min: return smallest of two values
+template inline T sMin(const T a, const T b)
+{
+ return (a < b) ? a : b;
+}
+
+// Max: return largest of two values
+template inline T sMax(const T a, const T b)
+{
+ return (a > b) ? a : b;
+}
+
+// Clamp: constrain value to min/max range
+template inline T sClamp(const T x, const T min, const T max)
+{
+ return sMax(min, sMin(max, x));
+}
+
+// Square: return x * x
+template T sSqr(T v)
+{
+ return v * v;
+}
+
+// Linear interpolation: a + f * (b - a)
+template T sLerp(T a, T b, sF32 f)
+{
+ return a + f * (b - a);
+}
+
+// Absolute value
+template T sAbs(T x)
+{
+ return (x < 0) ? -x : x;
+}
+
+// =================== Floating Point Math ===================
+// Square root (32-bit float)
+inline sF32 sFSqrt(sF32 x)
+{
+ return sqrtf(x);
+}
+
+// Sine (32-bit float)
+inline sF32 sFSin(sF32 x)
+{
+ return sinf(x);
+}
+
+// Cosine (32-bit float)
+inline sF32 sFCos(sF32 x)
+{
+ return cosf(x);
+}
+
+// Arc tangent (32-bit float)
+inline sF32 sFAtan(sF32 x)
+{
+ return atanf(x);
+}
+
+// Power: base to the power of exponent (32-bit float)
+inline sF32 sFPow(sF32 b, sF32 e)
+{
+ return powf(b, e);
+}
+
+// Pi constant: calculated as 4 * arctan(1)
+const sF32 sFPi = 4 * sFAtan(1);
+
+// =================== Signal Processing ===================
+// Sinc function: sin(x)/x or 1 if x=0
+// Used in windowed-sinc FIR filter design
+inline sF32 sFSinc(sF32 x)
+{
+ return x ? sFSin(x) / x : 1;
+}
+
+// Hamming window: cos(X * PI / 2)^2 or 0
+// Used to window the sinc function, reduces spectral leakage
+inline sF32 sFHamming(sF32 x)
+{
+ return (x > -1 && x < 1) ? sSqr(sFCos(x * sFPi / 2)) : 0;
+}
+
+// =================== Endian Utilities ===================
+// Swap endianness of 16-bit value (big-endian <-> little-endian)
+inline void sSwapEndian(sU16 &v)
+{
+ v = ((v & 0xff) << 8) | (v >> 8);
+}
+
+// Compiler pragmas for optimization (optional)
+// #pragma intrinsic(memset, sqrt, sin, cos, atan, powf)
+
+#endif // TYPES_H