My previous post discussed how to get basic mixing to work for MOD file playback. That gives a rough approximation of MOD playback, but you can't really play most actual Amiga MOD files until you support most of the effects. This includes things like vibrato, pitch slides, and volume adjustment.
Supporting effects is challenging because the documentation leaves a lot open to interpretation. The only way to figure out how to play back a MOD file is to test against other players or read their source code.
I wrote a bunch of test MOD files to answer questions about how effects are implemented. Here are some of the things I figured out, using VLC's built-in MOD player as a reference. I don't know how accurate VLC is, but it sounds fine on all the songs I've tried.
- If no sample is specified on a row, the previous sample is retriggered from its start, but the volume and fine-tune values are not reloaded from the sample.
- Glissando (0xE3), vibrato waveform (0xE4), and tremolo waveform (0xE6) are per-channel settings.
- Glissando (0xE3) quantization takes the current fine-tune setting into account.
- Arpeggios (0x0) cycle through notes once per tick, retriggering from the start on each tick.
- Vibrato (0x4 and 0x6) waveform period is 64 / (vibrato speed) / (song speed - 1). I don't know why the -1 is there.
- Vibrato (0x4 and 0x6) waveform peak-to-peak amplitude is (vibrato depth) / 4 half-steps.
- Tremolo (0x7) waveform peak-to-peak amplitude is (tremolo depth) * 4 volume units.
- Delay pattern (0xEE) is not cumulative if multiple channels have it on the same row, and the highest channel wins.
- Notes are triggered on tick 0 of a row only if a period is present on that row. In the case of delay sample (0xED), no note will be triggered unless a period is present.
- Retrigger (0xE9) will trigger a note after tick 0 even if no period is present on that row.
- Fine pitch slides (0xE1 and 0xE2) apply on the same tick that a note is triggered.
- Coarse pitch slides (0x1 and 0x2) and portamento to note (0x3 and 0x5) never apply on tick 0.
- Retrigger (0xE9) after a pitch slide uses the post-slide playback period.
- Note cut (0xEC) sets the channel volume to 0, so if no sample is specified in subsequent notes to reset the channel volume, those notes will be silent.
- Speed changes (0xF) take affect on the current row.
- After a vibrato (0x4 and 0x6) ends, the vibrato modulation stops being applied, so the pitch can jump.
- The vibrato waveform only advances during vibrato commands (0x4 and 0x6). The same applies to tremolo (0x7).
Most of these things were figured out by running test MOD files (constructed with makemod from a previous post) against VLC.
With all that in mind, it looks like we need to track a bunch of things for each channel to support all the effects correctly:
- Volume before tremolo
- Period before vibrato
- Most recent portamento-to-note rate
- Most recent vibrato speed
- Most recent vibrato depth
- Vibrato waveform
- Whether vibrato waveform is retriggered on new notes
- Vibrato waveform offset
- Most recent tremolo speed
- Most recent tremolo depth
- Tremolo waveform
- Whether tremolo waveform is retriggered on new notes
- Vibrato tremolo offset
- Whether glissando is enabled
- Current fine-tune setting
That seems like a lot of stuff, and it's possible that other players economize by using the same memory for tremolo and vibrato state. Hopefully not too many songs take advantage of that sort of thing.
I managed to put all of this together to make inefficient but reasonably correct-sounding effect handling. Here is the updated playmod.c with the new callback:
/* playmod.c */
/* Command-line Amiga MOD player */
/* All effects implemented except:
* - 8: set panning
* - E0: set filter on/off
* - E8: set panning
* - EF: invert loop
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <AudioToolbox/AudioQueue.h>
#include <AudioToolbox/AudioFormat.h>
#include "mod.h"
#define MAX(x,y) ((x) > (y) ? (x) : (y))
#define MIN(x,y) ((x) < (y) ? (x) : (y))
#define BUFFER_SIZE 16384
#define HALF_STEP 1.0594630943593
// 856 -> finetune -8
#define MAX_PERIOD 907
// 113 -> finetune 7
#define MIN_PERIOD 107
// Each channel automatically loops when it reaches the end of the
// current sample, using the next* values. To make a sample that
// doesn't loop, set the first two values to 0 and use a loop length
// of 2.
struct channel {
// playback state variables
int8_t *curSample; // currently playing sample
float offset; // current offset within sample
int length; // length of currently playing sample
int volume; // current playback volume
int period; // current playback period
// loop configuration variables
int8_t *nextSample; // sample/loop start to play next
int nextLength; // length of next sample
// pattern data for current row
int rowEffect;
int rowPeriod;
int rowSample;
// most recent pattern data
int lastEffect;
int lastPeriod;
int lastSample;
// effect state
int baseVolume; // volume before tremolo
int basePeriod; // period before vibrato
int slideRate;
int vibSpeed;
int vibDepth;
int vibWaveform;
int vibNoRetrigger;
float vibOffset;
int tremSpeed;
int tremDepth;
float tremOffset;
int tremWaveform;
int tremNoRetrigger;
int glissando;
int finetune;
};
struct audiostate {
struct modfile *song;
struct channel channels[4];
float sample; // current sample in this tick
int tick; // current tick in this row
int row; // current row in this pattern
int pattern; // current pattern in the pattern table
int speed; // default ticks per row
int nextRow; // row to play after current
int rowTicks; // number of ticks in current row
float samplesPerTick;
int loopStart;
int loopCount;
};
// This flag is set by callback() when the song ends
volatile int songover = 0;
void callback(void *userData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
{
struct audiostate *s = (struct audiostate *) userData;
float *out = (float *) inBuffer->mAudioData;
memset(out, 0, BUFFER_SIZE);
if (songover) return;
// loop over each sample
for (int i = 0; i < BUFFER_SIZE/8; i++, s->sample += 1.0) {
// do per-tick processing
if (s->sample >= s->samplesPerTick) {
s->sample -= s->samplesPerTick;
s->tick++;
if (s->tick >= s->rowTicks) {
s->tick = 0;
s->row = s->nextRow;
if (s->row >= 64) {
s->row = 0;
s->pattern++;
if (s->pattern >= s->song->header.numpatterns) {
songover = 1;
break;
}
}
s->nextRow = s->row + 1;
s->rowTicks = s->speed;
}
if (s->tick == 0) {
printf("[%02X] %02X: ", s->pattern, s->row);
int pattern = s->song->header.patternTable[s->pattern];
struct note *n = &s->song->patterns[pattern].note[s->row][0];
for (int c = 0; c < 4; c++) {
// read pattern data for each note
struct channel *chan = &s->channels[c];
chan->rowSample = (n[c].sampleHi << 4) | n[c].sampleLo;
chan->rowPeriod = (n[c].periodHi << 8) | n[c].periodLo;
chan->rowEffect = (n[c].effectHi << 8) | n[c].effectLo;
printf(" %03X %02X %03X ", chan->rowPeriod, chan->rowSample, chan->rowEffect);
if (chan->rowSample) chan->lastSample = chan->rowSample;
if (chan->rowPeriod) chan->lastPeriod = chan->rowPeriod;
chan->lastEffect = chan->rowEffect;
}
printf("\n");
}
for (int c = 0; c < 4; c++) {
struct channel *chan = &s->channels[c];
int effect = chan->rowEffect;
int period = chan->rowPeriod;
int sample = chan->rowSample;
int effectType = effect >> 8;
int effectArg = effect & 0xFF;
// determine whether to strike a new note
int strike = 0;
if (effectType == 0 && effectArg != 0) { // arpeggio
strike = 1;
} else if (effectType == 0xE && effectArg >> 4 == 9) { // retrigger
int arg = effectArg & 0xF;
strike = (s->tick == 0 && period != 0) ||
(s->tick != 0 && arg > 0 && s->tick % arg == 0);
} else if (effectType == 0xE && effectArg >> 4 == 0xD) { // delayed strike
strike = (period != 0 && s->tick == (effectArg & 0xF));
} else if (effectType != 3 && effectType != 5) {
strike = (s->tick == 0 && period != 0);
}
if (strike) {
int newSample = chan->lastSample-1;
chan->curSample = s->song->sampleData[newSample];
chan->nextSample = s->song->sampleData[newSample];
chan->length = s->song->header.samples[newSample].len * 2;
int repeat_start = s->song->header.samples[newSample].repstart * 2;
int repeat_length = s->song->header.samples[newSample].replen * 2;
chan->nextSample += repeat_start;
chan->nextLength = repeat_length;
if (effectType == 9) {
chan->offset = effectArg << 8;
} else {
chan->offset = 0;
}
}
if (strike || ((effectType == 3 || effectType == 5) && period !=0 && s->tick == 0)) {
if (sample) {
chan->baseVolume = s->song->header.samples[sample-1].volume;
chan->finetune = s->song->header.samples[sample-1].finetune;
}
if (chan->finetune != 0) {
float scale = pow(HALF_STEP, -(float)chan->finetune/8);
chan->lastPeriod = round((float) chan->lastPeriod * scale);
}
}
if (strike) {
chan->basePeriod = chan->lastPeriod;
}
// adjust base period
if (effectType == 1) { // slide up
if (s->tick != 0) {
chan->basePeriod = MAX(MIN_PERIOD, chan->basePeriod - effectArg);
}
} else if (effectType == 2) { // slide down
if (s->tick != 0) {
chan->basePeriod = MIN(MAX_PERIOD, chan->basePeriod + effectArg);
}
} else if (effectType == 3 || effectType == 5) { // porta to note
if (s->tick != 0) {
if (effectType == 3 && effectArg) chan->slideRate = effectArg;
if (chan->lastPeriod > chan->basePeriod) {
chan->basePeriod = MIN(chan->lastPeriod, chan->basePeriod + chan->slideRate);
} else {
chan->basePeriod = MAX(chan->lastPeriod, chan->basePeriod - chan->slideRate);
}
}
} else if (effectType == 0xE && effect >> 4 == 1) {
// fineslide up
if (s->tick == 0) {
chan->basePeriod = MAX(MIN_PERIOD, chan->basePeriod - effectArg);
}
} else if (effectType == 2) { // slide down
if (s->tick == 0) {
chan->basePeriod = MIN(MAX_PERIOD, chan->basePeriod + effectArg);
}
}
// adjust playback period
if (effectType == 4 || effectType == 6) { // vibrato
if (effectType == 4) {
if (effectArg & 0xF) chan->vibDepth = effectArg & 0xF;
if (effectArg >> 4) chan->vibSpeed = effectArg >> 4;
}
float offsetInc = (float) chan->vibSpeed * (s->speed - 1) / s->speed / 64.0;
if (period && s->tick == 0) {
if (!chan->vibNoRetrigger) chan->vibOffset = 0;
}
float v;
if (chan->vibWaveform == 0) { // sine wave
v = sin(chan->vibOffset * 2 * M_PI);
} else if (chan->vibWaveform == 1) { // sawtooth
if (chan->vibOffset < 0.5) v = - chan->vibOffset * 2.0;
else v = 2.0 - chan->vibOffset * 2.0;
} else if (chan->vibWaveform == 2) { // square
v = copysign(1.0, 0.5 - chan->vibOffset);
} else { // random
v = 1.0 - 2.0 * rand() / RAND_MAX;
}
v *= (float) chan->vibDepth / 8.0;
chan->period = round((float) chan->basePeriod * pow(HALF_STEP, v));
chan->vibOffset += offsetInc;
if (chan->vibOffset > 1.0) {
chan->vibOffset -= 1.0;
}
} else if (effectType == 0 && effectArg != 0) {
int subRow = s->tick % 3;
if (subRow == 0) {
chan->period = chan->basePeriod;
} else if (subRow == 1) {
float scale = pow(HALF_STEP, -(effectArg >> 4));
chan->period = round(scale * chan->basePeriod);
} else if (subRow == 1) {
float scale = pow(HALF_STEP, -(effectArg & 0xF));
chan->period = round(scale * chan->basePeriod);
}
} else if (chan->glissando && (effectType == 3 || effectType == 5)) {
// round to nearest half-step, including influence from finetune
float scale = pow(HALF_STEP, -(float)chan->finetune/8);
float c1period = 856.0 * scale;
float note = log(c1period / chan->basePeriod) / log(HALF_STEP);
float nearestNote = round(note);
int nearestPeriod = round(c1period * pow(HALF_STEP, -nearestNote));
chan->period = nearestPeriod;
} else {
chan->period = chan->basePeriod;
}
// adjust base volume
if (effectType == 0xC) {
chan->baseVolume = effectArg;
} else if ((effectType == 0xA || effectType == 0x5 || effectType == 0x6)
&& s->tick > 0) {
int slideUp = effectArg >> 4;
int slideDown = effectArg & 0xF;
if (slideUp) {
chan->baseVolume += slideUp;
} else {
chan->baseVolume -= slideDown;
}
} else if (effectType == 0xE) {
if (effectArg >> 4 == 0xA) { // fine vol slide up
if (s->tick == 0) chan->baseVolume += effectArg & 0xF;
} else if (effectArg >> 4 == 0xB) { // fine vol slide down
if (s->tick == 0) chan->baseVolume -= effectArg & 0xF;
} else if (effectArg >> 4 == 0xC) { // note cut
if (s->tick == (effectArg & 0xF)) {
chan->baseVolume = 0;
}
}
}
chan->baseVolume = MIN(64,MAX(0,chan->baseVolume));
// set channel volume
if (effectType == 7) {
if (effectArg & 0xF) chan->tremDepth = effectArg & 0xF;
if (effectArg >> 4) chan->tremSpeed = effectArg >> 4;
float offsetInc = (float) chan->tremSpeed * (s->speed - 1) / s->speed / 64.0;
if (period && s->tick == 0) {
if (!chan->tremNoRetrigger) chan->tremOffset = 0;
}
float v;
if (chan->tremWaveform == 0) { // sine wave
v = sin(chan->tremOffset * 2 * M_PI);
} else if (chan->tremWaveform == 1) { // sawtooth
if (chan->tremOffset < 0.5) v = - chan->tremOffset * 2.0;
else v = 2.0 - chan->tremOffset * 2.0;
} else if (chan->tremWaveform == 2) { // square
v = copysign(1.0, 0.5 - chan->tremOffset);
} else { // random
v = 1.0 - 2.0 * rand() / RAND_MAX;
}
v *= (chan->tremDepth * 2);
chan->volume = round(chan->baseVolume + v);
chan->tremOffset += offsetInc;
if (chan->tremOffset > 1.0) {
chan->tremOffset -= 1.0;
}
} else {
chan->volume = chan->baseVolume;
}
chan->volume = MIN(64,MAX(0,chan->volume));
if (s->tick == 0) {
if (effectType == 0xF) {
if (effectArg < 0x20) {
s->speed = effectArg;
s->rowTicks = effectArg;
} else {
s->samplesPerTick = 2.5 * 44100.0 / effectArg;
}
}
if (effectType == 0xB) {
s->nextRow = 0;
s->pattern = effectArg;
}
if (effectType == 0xD) {
s->nextRow = effectArg;
s->pattern++;
}
if (effectType == 0xE) {
int subType = effectArg >> 4;
int arg = effectArg & 0xF;
if (subType == 4) {
// set vibrato waveform
chan->vibWaveform = arg & 3;
chan->vibNoRetrigger = (arg >> 2) & 1;
} else if (subType == 7) {
// set tremolo waveform
chan->tremWaveform = arg & 3;
chan->tremNoRetrigger = (arg >> 2) & 1;
} else if (subType == 0xE) {
// pattern delay
s->rowTicks = s->speed + arg * s->speed;
} else if (subType == 3) {
chan->glissando = arg;
} else if (subType == 6) {
// loop pattern
if (arg == 0) { // define loop start
if (s->loopCount == 0) s->loopStart = s->row;
} else { // loop arg times back to start
if (s->loopCount == 0) s->loopCount = arg + 1;
if (s->loopCount > 1) s->nextRow = s->loopStart;
s->loopCount--;
}
}
}
}
}
}
// mix samples and handle loops
for (int c = 0; c < 4; c++) {
struct channel *chan = &s->channels[c];
if (chan->curSample == NULL) continue;
int offset = trunc(chan->offset);
// loop sample until offset is less than length
if (offset >= chan->length) {
chan->offset -= chan->length;
offset = trunc(chan->offset);
chan->curSample = chan->nextSample;
chan->length = chan->nextLength;
}
float v = (float) (chan->curSample[offset] * chan->volume) / 64.0;
if (c == 1 || c == 2) out[i*2 + 1] += v;
else out[i*2] += v;
float increment = 3.546895e6 / 44100.0 / chan->period;
chan->offset += increment;
}
// normalize to +/- 1.0 range
out[i*2] /= 2.0 * 128.0;
out[i*2 + 1] /= 2.0 * 128.0;
}
OSStatus err = AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
if (err != noErr) songover = 1;
}
const char *aq_error_string(OSStatus err)
{
switch(err) {
case kAudioQueueErr_InvalidBuffer: return "Invalid Buffer";
case kAudioQueueErr_BufferEmpty: return "Buffer Empty";
case kAudioQueueErr_DisposalPending: return "Disposal Pending";
case kAudioQueueErr_InvalidProperty: return "Invalid Property";
case kAudioQueueErr_InvalidPropertySize: return "Invalid Property Size";
case kAudioQueueErr_InvalidParameter: return "Invalid Parameter";
case kAudioQueueErr_CannotStart: return "Cannot Start";
case kAudioQueueErr_InvalidDevice: return "Invalid Device";
case kAudioQueueErr_BufferInQueue: return "Buffer In Queue";
case kAudioQueueErr_InvalidRunState: return "Invalid Run State";
case kAudioQueueErr_InvalidQueueType: return "Invalid Queue Type";
case kAudioQueueErr_Permissions: return "Permissions";
case kAudioQueueErr_InvalidPropertyValue: return "Invalid Property Value";
case kAudioQueueErr_PrimeTimedOut: return "Prime Timed Out";
case kAudioQueueErr_CodecNotFound: return "Codec Not Found";
case kAudioQueueErr_InvalidCodecAccess: return "Invalid Codec Access";
case kAudioQueueErr_QueueInvalidated: return "Queue Invalidated";
case kAudioQueueErr_RecordUnderrun: return "Record Underrun";
case kAudioQueueErr_EnqueueDuringReset: return "Enqueue During Reset";
case kAudioQueueErr_InvalidOfflineMode: return "Invalid Offline Mode";
case kAudioFormatUnsupportedDataFormatError: return "Unsupported Data Format Error";
default: return "Unkown error";
}
}
int playMod(struct modfile *mod)
{
struct audiostate audiostate;
OSStatus err;
AudioQueueRef aq;
AudioQueueBufferRef buf[2];
AudioStreamBasicDescription format = {
.mSampleRate = 44100.0,
.mFormatID = kAudioFormatLinearPCM,
.mFormatFlags = kLinearPCMFormatFlagIsFloat,
.mBytesPerPacket = 8,
.mFramesPerPacket = 1,
.mBytesPerFrame = 8,
.mChannelsPerFrame = 2,
.mBitsPerChannel = 32,
};
memset(&audiostate, 0, sizeof(audiostate));
audiostate.song = mod;
audiostate.tick = -1;
audiostate.speed = 6;
audiostate.row = 0;
audiostate.nextRow = 0;
audiostate.samplesPerTick = 44100.0 / 50.0;
audiostate.sample = audiostate.samplesPerTick;
songover = 0;
err = AudioQueueNewOutput(&format, callback, &audiostate, NULL, NULL, 0, &aq);
if (err != noErr) {
printf("AudioQueueNewOutput Error %d: %s\n", err, aq_error_string(err));
return -1;
}
for (int i = 0; i < sizeof(buf)/sizeof(buf[0]); i++) {
err = AudioQueueAllocateBuffer(aq, BUFFER_SIZE, &buf[i]);
if (err != noErr) {
printf("AudioQueueAllocateBuffer Error %d: %s\n", err, aq_error_string(err));
return -1;
}
buf[i]->mAudioDataByteSize = BUFFER_SIZE;
callback(&audiostate, aq, buf[i]);
}
err = AudioQueueStart(aq, NULL);
if (err != noErr) {
printf("AudioQueueStart Error %d: %s\n", err, aq_error_string(err));
return -1;
}
while(!songover) {
sleep(1);
}
err = AudioQueueStop(aq, TRUE);
if (err != noErr) {
printf("AudioQueueStop Error %d: %s\n", err, aq_error_string(err));
return -1;
}
err = AudioQueueDispose(aq, 0);
if (err != noErr) {
printf("AudioQueueDispose Error %d: %s\n", err, aq_error_string(err));
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
struct modfile *mod = NULL;
if (argc < 2) {
fprintf(stderr, "usage: playmod modfile.mod\n");
return -1;
}
mod = loadMod(argv[1]);
if (mod == NULL) {
fprintf(stderr, "Could not load MOD file\n");
return -1;
}
playMod(mod);
freeMod(mod);
return 0;
}
This uses mod.c to handle loading the MOD file:
/* mod.c */
/* Amiga MOD load/save routines */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "mod.h"
#define SWAP16(x) (((x) >> 8) | (((x) & 0xFF) << 8))
#define LEN(x) (sizeof(x)/sizeof(x[0]))
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
static uint8_t u8max(uint8_t *v, int len);
static uint8_t u8max(uint8_t *v, int len)
{
uint8_t max = 0;
for (; len > 0; len--) {
max = (*v > max) ? *v : max;
v++;
}
return max;
}
void freeMod(struct modfile *modfile)
{
if (modfile == NULL) return;
for (int i = 0; i < 31; i++)
{
if (modfile->sampleData[i] != NULL)
free(modfile->sampleData[i]);
}
free(modfile);
}
struct modfile *loadMod(char *filename)
{
FILE *f = NULL;
size_t nread;
struct modfile *modfile;
// clear to ensure unused pointers are NULL
modfile = (struct modfile *) calloc(sizeof(*modfile), 1);
if (modfile == NULL) goto error;
f = fopen(filename, "rb");
if (f == NULL) goto error;
nread = fread(&modfile->header, sizeof(modfile->header), 1, f);
if (nread < 1) goto error;
// count patterns so we know how many to load
int maxpattern = 1 + u8max(modfile->header.patternTable,
LEN(modfile->header.patternTable));
nread = fread(&modfile->patterns, sizeof(modfile->patterns[0]), maxpattern, f);
if (nread < maxpattern) goto error;
modfile->numPatterns = maxpattern;
// process & load samples,
// convert big->little endian for 16-bit values
for (int i = 0; i < 31; i++)
{
struct sample *s;
s = &modfile->header.samples[i];
s->len = SWAP16(s->len);
s->repstart = SWAP16(s->repstart);
s->replen = SWAP16(s->replen);
if (s->len == 0) continue;
int8_t *p = (int8_t *) malloc(s->len*2);
if (p == NULL) goto error;
modfile->sampleData[i] = p;
nread = fread(p, s->len*2, 1, f);
if (nread < 1) goto error;
}
fclose(f);
return modfile;
error:
if (modfile != NULL) freeMod(modfile);
if (f != NULL) fclose(f);
return NULL;
}
int saveMod(char *filename, struct modfile *modfile)
{
FILE *f = NULL;
size_t nwritten;
f = fopen(filename, "wb");
if (f == NULL) goto error;
// convert sample metadata to big endian
for (int i = 0; i < 31; i++) {
struct sample *s;
s = &modfile->header.samples[i];
s->len = SWAP16(s->len);
s->repstart = SWAP16(s->repstart);
s->replen = SWAP16(s->replen);
}
nwritten = fwrite(&modfile->header, sizeof(modfile->header), 1, f);
if (nwritten < 1) goto error;
// count patterns so we know how many to write
int maxpattern = 1 + u8max(modfile->header.patternTable,
LEN(modfile->header.patternTable));
nwritten = fwrite(&modfile->patterns, sizeof(modfile->patterns[0]), maxpattern, f);
if (nwritten < maxpattern) goto error;
// write samples
for (int i = 0; i < 31; i++) {
int len = SWAP16(modfile->header.samples[i].len);
int8_t *p = modfile->sampleData[i];
if (len == 0) continue;
nwritten = fwrite(p, len*2, 1, f);
if (nwritten < 1) goto error;
}
fclose(f);
return 0;
error:
if (f != NULL) fclose(f);
return -1;
}
And here's the header file mod.h that describes the MOD-loading interface:
/* mod.h */
/* Amiga MOD load/save routines */
#ifndef _MOD_H
#define _MOD_H 1
#include <stdint.h>
// the raw MOD file structures
#pragma pack(push, 1)
struct sample {
char name[22];
uint16_t len;
int8_t finetune : 4,
: 4;
uint8_t volume;
uint16_t repstart;
uint16_t replen;
};
struct note {
uint32_t periodHi: 4;
uint32_t sampleHi: 4;
uint32_t periodLo: 8;
uint32_t effectHi: 4;
uint32_t sampleLo: 4;
uint32_t effectLo: 8;
};
struct pattern {
struct note note[64][4];
};
struct modheader {
char name[20];
struct sample samples[31];
uint8_t numpatterns;
uint8_t unused;
uint8_t patternTable[128];
char mk[4];
};
struct modfile {
struct modheader header;
struct pattern patterns[64];
int8_t *sampleData[31];
int numPatterns;
};
#pragma pack(pop)
void freeMod(struct modfile *modfile);
struct modfile *loadMod(char *filename);
int saveMod(char *filename, struct modfile *modfile);
#endif // _MOD_H
This can all be built with a simple Makefile (note that indent here is a real TAB character):
# Makefile for playmod LDFLAGS = -framework CoreFoundation -framework AudioToolbox -F/Library/Frameworks -lm playmod: playmod.c mod.c Makefile cc -g $(LDFLAGS) $(filter-out Makefile,$^) -o $@
This all comes out to around 750 lines. That's much shorter than my attempts to do the same thing back in high school! The effect handling code is currently pretty ugly, just measured by the depth of if statements, so maybe I'll clean it up the next time I have a long vacation.
References:
- http://lclevy.free.fr/mo3/mod.txt - Relatively detailed description of the format.
- http://www.eblong.com/zarf/blorb/mod-spec.txt - Somewhat less detailed description of the format.
No comments:
Post a Comment