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
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user