Merge pull request #1 from bou-samra/refactored

Refactor: modularize code into separate files with enhanced documenta…
This commit is contained in:
Jason
2026-04-24 19:40:13 +10:00
committed by GitHub
9 changed files with 1660 additions and 42 deletions
+37 -7
View File
@@ -1,10 +1,40 @@
## build TinyMOD # =================== TinyMOD Makefile ===================
## jbs - paragonsoft # Build configuration for TinyMOD MOD player
all: tinymod # === Compiler Settings ===
CC = g++
CFLAGS = -O2 -Wall -Wextra
tinymod: tinymod.cpp # === Source Files ===
## g++ -o tinymod tinymod.cpp SOURCES = src/main.cpp src/paula.cpp src/modplayer.cpp
g++ -o tinymod `pkg-config --libs alsa` tinymod.cpp -lm -L . -l:libportaudio.a 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: clean:
rm tinymod rm -f $(OBJECTS) $(TARGET)
@echo "Clean complete"
# Phony targets (not actual files)
.PHONY: all clean
+130 -35
View File
@@ -1,44 +1,139 @@
# TinyMOD # TinyMOD - Amiga MOD File Player
Written by Tammo "kb" Hinrichs in 2007<br>
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.</p>
<p>This player includes an Amiga Paula chip "emulation" that faithfully recreates 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.
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...</p>
<p>The code should be pretty portable, all OS/platform dependent stuff is ## Features
at the top. Code for testing is at the bottom.</p>
<p>You'll need some kind of sound output that calls back the player providing - **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
it a stereo interleaved single float buffer to write into (0dB=1.0).</p> - **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) ### Requirements
* 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
2007-12-07: (Tammo "kb" Hinrichs) - GCC/G++ compiler
* fixed 40x and 4x0 vibrato effects (jogeir - tiny tunes) - PortAudio library (libportaudio.a)
* fixed pattern loop (olof gustafsson - pinball illusions) - ALSA development libraries (for Linux)
* fixed fine volslide down (olof gustafsson - pinball illusions) - Make
* included some external header files
* cleanups
2007-12-06: (Tammo "kb" Hinrichs) ### Compilation
* first "release". Note to self: Don't post stuff on pouet.net when drunk.
## Compilation ```bash
type `make` or `g++ -o tinymod pkg-config --libs alsa tinymod.cpp -lm -L . -l:libportaudio.a` on the command line make
```
## Author(s) The Makefile will:
Tammo "kb" Hinrichs<br> - Compile the modular source files
Jason Bou-Samra - 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
+32
View File
@@ -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
+282
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <portaudio.h>
#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 [<mod file>|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;
}
+598
View File
@@ -0,0 +1,598 @@
// =================== MOD File Player Implementation ===================
// Implementation of MOD file parsing and playback
#include "modplayer.h"
#include <cstring>
#include <cstdlib>
// =================== 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<sInt>(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);
}
+154
View File
@@ -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
+192
View File
@@ -0,0 +1,192 @@
// =================== Paula Chip Emulator Implementation ===================
// Implementation of the Amiga Paula chip audio hardware emulator
#include "paula.h"
#include <cstring>
// =================== 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
}
+88
View File
@@ -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
+147
View File
@@ -0,0 +1,147 @@
// =================== Type Definitions & Utilities ===================
// Standard type definitions, memory utilities, and math functions
#ifndef TYPES_H
#define TYPES_H
#include <inttypes.h> // Fixed size integer types (C standard library)
#include <math.h> // Mathematical operations (C standard library)
#include <string.h> // String handling (C standard library)
#include <cstdint> // 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 <typename T> inline T sMin(const T a, const T b)
{
return (a < b) ? a : b;
}
// Max: return largest of two values
template <typename T> inline T sMax(const T a, const T b)
{
return (a > b) ? a : b;
}
// Clamp: constrain value to min/max range
template <typename T> inline T sClamp(const T x, const T min, const T max)
{
return sMax(min, sMin(max, x));
}
// Square: return x * x
template <typename T> T sSqr(T v)
{
return v * v;
}
// Linear interpolation: a + f * (b - a)
template <typename T> T sLerp(T a, T b, sF32 f)
{
return a + f * (b - a);
}
// Absolute value
template <typename T> 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