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