Sunday, July 10, 2016

Simple Amiga MOD File Player

Here's a set of posts on how to write an Amiga MOD file player. Since Blogger displays the posts backwards, this is the correct order:

If you read all 4 of those, you will know how to play Amiga MOD files!

Testing this code could be problematic since many MODs available on the internet are really not that good. Here are a few better ones that exercise the player pretty well:

Simple Amiga MOD File Player: Effects

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:

Simple Amiga MOD File Player: Basic Playback

The MOD format maps very closely to the Amiga hardware. For example, the note pitch is specified as a period that can go directly into the AUDnPER hardware registers. I think probably the simplest way to make a relatively accurate MOD player is to structure the player similarly to the hardware, and then much of the logic can be the same as it would be on an Amiga.

Amiga MOD files are structured as patterns of rows, where each row has data for four channels. The channels map 1:1 to the Amiga's 4 digital-to-analog converter (DAC) channels. The player uses a timer interrupt to wake up periodically (once per tick), see if it's time to move to the next row, and if so, update the DAC settings for the new notes. The most convenient timing source is the 50-Hz vertical blanking interrupt (it would be 60 Hz in North America, but MODs seem to be designed around European timings). This is fired once per video frame and has a fixed frequency. However, there's also a programmable timer on the CIA (complex interface adapter) chip that can be used to get an more flexible interrupt rate.

Most music is around 100 beats per minute or 1.67 Hz, and simple music doesn't use notes shorter than 16th notes, which come 4 per beat. This means we might want our rows to play at 6.7 Hz or so. MOD players running from a faster interrupt like the 50-Hz vertical blanking interrupt slow things down with a speed parameter that specifies how many ticks to play each row for. This is typically something like 6, which would give a tempo of

60 seconds per minute * 50 ticks per second / 6 ticks per row / 4 rows per beat 
  = 125 beats per minute

Since each row uses multiple ticks, there are many effects that perform some action on each tick, for example modifying the sample period or volume.

There's also a tempo parameter that adjusts the tick rate such that tempo / tick rate = 2.5. This is accomplished by varying the interrupt rate from 50 Hz.

Note frequencies are specified with a period value, varying from 113 to 856. It may be possible to use other values, but I'll stick with this range. This is the length of time to play each value from an instrument sample, measured in ticks of a 3.546895-MHz oscillator (this is half the CPU clock rate, and not coincidentally 0.8 * the PAL color subcarrier frequency).

To adapt this to a Mac, we just need a way to call a routine once per tick to set the new sample address, period, and volume. The easiest thing to do is to put this into our audio callback. If there are N audio samples per tick, we can call the routine after we produce each N samples.

Here is a version of playmod.c that can finally start to play MOD files. This doesn't handle any effects, including setting volume or speed, so most MOD files will sound pretty bad. But it is a step in the right direction.

/* playmod.c */
/* Command-line Amiga MOD player */
/* No effects implemented yet */

#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 BUFFER_SIZE 16384

// 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 {
  int8_t *curSample;  // currently playing sample
  int8_t *nextSample; // sample/loop start to play next
  float offset;       // current offset within sample
  int length;         // length of currently playing sample
  int nextLength;     // length of next sample
  int volume;         // current volume
  int period;         // current period
};

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;    // ticks per row
  float samplesPerTick;
};

// 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->speed) {
        s->tick = 0;
        s->row++;
        if (s->row >= 64) {
          s->row = 0;
          s->pattern++;
          if (s->pattern >= s->song->header.numpatterns) {
            songover = 1;
            break;
          }
        }
      }

      if (s->tick == 0) {
        // per-row channel updates
        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++) {
          int period = (n[c].periodHi << 8) | n[c].periodLo;
          if (period) {
            s->channels[c].period = period;
            s->channels[c].offset = 0; // restrike note
          }
          int sample = (n[c].sampleHi << 4) | n[c].sampleLo;
          if (sample) {
            s->channels[c].curSample = s->song->sampleData[sample-1];
            s->channels[c].nextSample = s->song->sampleData[sample-1];
            s->channels[c].length = s->song->header.samples[sample-1].len * 2;
            int repeat_start = s->song->header.samples[sample-1].repstart * 2;
            int repeat_length = s->song->header.samples[sample-1].replen * 2;
            s->channels[c].nextSample += repeat_start;
            s->channels[c].nextLength = repeat_length;
            s->channels[c].volume = s->song->header.samples[sample-1].volume;
            s->channels[c].offset = 0; // restrike note
          }
          int effect = (n[c].effectHi << 8) | n[c].effectLo;
          if (effect) {
            // FIXME: process effects here
          }
        }
      }
    }

    // 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] += v;
      else out[i*2 + 1] += 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.samplesPerTick = 44100 / 50;
  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 could obviously stand to be optimized, but it's using only 2% of my CPU at the moment, so I'll defer optimization until later. One idea would be to convert the MOD file in memory from its native format to something that doesn't require so much work at runtime.

Next will be implementing some of the effects.

References:

Simple Amiga MOD File Player: Reading and Writing MOD Files

The first step in writing a MOD player was just to set up the development environment and get sound playback working. The next step is to be able to load MOD files. Since the format is pretty simple, it's not that hard to write files, too, so I'll do both. This will make it easier to test the MOD player later by making MOD files for test cases.

The MOD file format has three parts:

  1. fixed-size header
  2. variable number of "patterns" containing the notes to play
  3. variable length instrument samples

There are some variations on the format that might change the header size, but I only want to handle one type: four-channel MODs with the identifier "M.K." at byte offset 1080.

I'm doing this the lazy way and using fread() to populate C structs declared with #pragma pack(1). This packs all struct elements on byte boundaries instead of using the normal C struct padding rules. I'm also assuming that char is 8 bits, but any compiler with a larger char won't support #pragma pack(1) anyway.

The Amiga is a big-endian architecture, so loading values in this way on an Intel Mac means I'll have to do some byte-swapping of 16-bit values. I leave the pattern data packed as on disk and just live with the fact that most values require two values to be combined instead of a simple shift and mask.

Here's mod.h, the header file defining the 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

Next, here's the code to load and save MOD files using these structures, mod.c:

/* 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;
}

These should be pretty self-explanatory. You can load a MOD file with loadMod(), free it when you're done with freeMod(), and write a MOD file to disk with saveMod().

These aren't that useful on their own. Here's a program that can dump the contents of a MOD file to simple text format, dumpmod.c:

/* dumpmod.c */
/* Dumps a binary MOD file to a text format. */

#include <stdio.h>
#include <string.h>
#include <math.h>
#include "mod.h"

static char *pitchStrings[] = {
  "C-1", "C#1", "D-1", "D#1", "E-1", "F-1", "F#1", "G-1", "G#1", "A-1", "A#1", "B-1",
  "C-2", "C#2", "D-2", "D#2", "E-2", "F-2", "F#2", "G-2", "G#2", "A-2", "A#2", "B-2",
  "C-3", "C#3", "D-3", "D#3", "E-3", "F-3", "F#3", "G-3", "G#3", "A-3", "A#3", "B-3",
};

static char *periodToString(int period)
{
  // If the period matches standard note values (PAL only), then
  // translate into note names. Otherwise, return the raw period in
  // decimal.
  float steps = log(856.0 / period)/0.057762265046662;
  int pitch = round(steps);
  float closestPeriod = 856.0 / pow(1.059463094359295, pitch);
  int N = sizeof(pitchStrings)/sizeof(pitchStrings[0]);
  if (fabs(closestPeriod - period) < 0.5 && pitch >= 0 && pitch < N) {
    return pitchStrings[pitch];
  } else {
    static char str[10];
    sprintf(str, "%03d", period);
    return str;
  }
}

int main(int argc, char *argv[])
{
  if (argc < 2) {
    printf("usage: dumpmod modfile.mod\n");
    return -1;
  }

  struct modfile *mod = loadMod(argv[1]);

  if (!mod) {
    printf("Error reading MOD file\n");
    return -1;
  }

  // dump patterns
  for (int i = 0; i < mod->numPatterns; i++) {
    struct pattern *p = &mod->patterns[i];
    printf("PATTERN %d\n", i);
    for (int row = 0; row < 64; row++) {
      printf("%02X  ", row);
      for (int col = 0; col < 4; col++) {
        struct note n = p->note[row][col];
        if (n.periodHi || n.periodLo) {
          int period = (n.periodHi << 8) + n.periodLo;
          printf(" %s", periodToString(period));
        } else {
          printf(" ...");
        }
        if (n.sampleHi || n.sampleLo)
          printf(" %01X%01X", n.sampleHi, n.sampleLo);
        else
          printf(" ..");
        if (n.effectHi || n.effectLo)
          printf(" %01X%02X", n.effectHi, n.effectLo);
        else
          printf(" ...");
        if (col < 3)
          printf(" |");
        else
          printf("\n");
      }
    }
    printf("END_PATTERN\n\n");
  }

  // dump pattern table
  printf("PATTERN_TABLE\n");
  for (int i = 0; i < mod->header.numpatterns; i++) {
    int p = mod->header.patternTable[i];
    printf("  %d\n", p);
  }
  printf("END_PATTERN_TABLE\n\n");

  // dump samples
  for (int i = 0; i < 31; i++) {
    if (mod->header.samples[i].len > 0) {
      printf("SAMPLE %02X\n", i+1);
      printf("  NAME \"");
      for (int j = 0; j < 22; j++) {
        char c = mod->header.samples[i].name[j];
        if (c == '"') printf("\\\"");
        else printf("%c", c);
      }
      printf("\"\n");
      printf("  ; Length: %d\n", mod->header.samples[i].len*2);
      printf("  VOLUME %d\n", mod->header.samples[i].volume);
      printf("  FINETUNE %d\n", mod->header.samples[i].finetune);
      printf("  REPEAT_START %d\n", mod->header.samples[i].repstart*2);
      printf("  REPEAT_LENGTH %d\n", mod->header.samples[i].replen*2);
      printf("  DATA\n   ");
      for (int j = 0; j < mod->header.samples[i].len * 2; j++) {
        printf(" %d", mod->sampleData[i][j]);
      }
      printf("\n  END_DATA\n");
      printf("END_SAMPLE\n\n");
    }
  }
}

It would also be nice to have a way to create MOD files to test the eventual player. This can be as simple or complicated as you want. Here's my current version, called makemod.c:

/* makemod.c */
/* Convert text description of MOD file into a binary MOD file */

/* Format is:
 *
 *   ; comment until end of line
 *
 *   PATTERN nn
 *   ; Row numbers from 00-3F hex, blank rows can be left out.
 *   ; Dots stand for missing values. If all remaining values in
 *   ; a channel are empty, dots aren't needed.
 *   ;
 *   ; row   Note Inst  Effect  |  N  I   E  |  N   I  E  |  N   I  E
 *     00    C-3   07   400     | C-3 .. 240 | E-3 02     | G-3 03 10C
 *     01    ...   ..   ...     | ... .. ... | E-3 02 240 |
 *   ; etc.
 *   ; Note can be [A-G][-#][1-3] or [0-9]{3}
 *   ; Inst is 01-20 hex
 *   ; Effect is always a 3-digit hex number
 *   ; ^ means repeat value from previous row for this channel
 *   END_PATTERN
 *
 *   PATTERN_TABLE
 *     ; These entries are space-delimited and multiple values can be on
 *     ; one line to save space
 *     00
 *     01
 *     05
 *     etc.
 *   END_PATTERN_TABLE
 *
 *   SAMPLE nn   ; hex number 01-1F
 *     NAME "sample name"
 *     FINETUNE n
 *     VOLUME n
 *     REPEAT_START n   ; default is 0
 *     REPEAT_LENGTH n  ; default is 2 - make first two points 0 to not repeat
 *     DATA
 *       0 1 5 2 0 ; etc.
 *     END_DATA
 *   END_SAMPLE
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdint.h>

#include "mod.h"

static FILE *f;
static int line = 1;
static char nextChar;

// PAL periods for C-1 through B-3
int periodTable[] = {
// C    C#   D    D#   E   -   F    F#   G    G#   A    A#   B   -
  856, 808, 762, 720, 678, 0, 640, 604, 570, 538, 508, 480, 453, 0,
  428, 404, 381, 360, 339, 0, 320, 302, 285, 269, 254, 240, 226, 0,
  214, 202, 190, 180, 170, 0, 160, 151, 143, 135, 127, 120, 113, 0 };

static struct modfile *parseMod(char *filename);

static void advanceChar(void)
{
  nextChar = fgetc(f);
  //printf("%c", nextChar);
}

static void skipComments()
{
  if (nextChar == ';') {
    while(nextChar != '\n') advanceChar();
  }
}

static void skipNewlines()
{
  for(;;) {
    skipComments();
    if (nextChar == '\n') {
      line++;
    } else if (!isspace(nextChar)) {
      break;
    }
    advanceChar();
  }
}

static void skipSpaces()
{
  for(;;) {
    if (isspace(nextChar) && nextChar != '\n') {
      advanceChar();
    } else {
      break;
    }
  }
}

static int getEllipses()
{
  skipSpaces();
  while(nextChar == '.') advanceChar();
  if (!isspace(nextChar)) return -1;
  return 0;
}

static int getHexNumber()
{
  int n = 0;
  skipSpaces();
  while(isxdigit(nextChar)) {
    n *= 16;
    if (nextChar > '9') {
      n += nextChar - 'A' + 10;
    } else {
      n += nextChar - '0';
    }
    advanceChar();
  }

  if (!isspace(nextChar)) return -1;
  return n;
}

static int getDecNumber(int *n)
{
  *n = 0;
  int negative = 0;
  skipSpaces();
  if (nextChar == '-') {
    negative = 1;
    advanceChar();
  }
  if (!isdigit(nextChar)) {
    return -1;
  }
  while(isdigit(nextChar)) {
    *n *= 10;
    *n += nextChar - '0';
    advanceChar();
  }

  if (!isspace(nextChar)) return -1;
  if (negative) *n = -*n;
  return 0;
}

static int getPitch()
{
  int note;
  int octave;
  skipSpaces();
  if (nextChar >= 'A' && nextChar <= 'G') {
    note = (nextChar - 'A') * 2;
  } else if (nextChar >= 'a' && nextChar <= 'g') {
    note = (nextChar - 'a') * 2;
  } else {
    return -1;
  }
  advanceChar();
  if (nextChar == '#') {
    note++;
  } else if (nextChar != '-') {
    return -1;
  }
  advanceChar();
  if (!isnumber(nextChar)) {
    return -1;
  }
  octave = nextChar - '0';
  if (octave < 1 || octave > 3) {
    return -1;
  }
  advanceChar();
  if (!isspace(nextChar)) {
    return -1;
  }
  // A-n, A#n, B-n, B#n are higher than C-n
  if (note < 4) octave++;
  note -= 4;
  note += (octave-1) * 14;

  if (note >= sizeof(periodTable)/sizeof(periodTable[0])) {
    return -1;
  }

  return periodTable[note];
}

static char *getString()
{
  static char str[1024];
  int idx = 0;
  skipSpaces();

  if (nextChar != '"') return NULL;
  advanceChar();

  while (nextChar != '"' && idx < 1023) {
    // escape \ and "
    if (nextChar == '\\') {
      advanceChar();
    }
    str[idx++] = nextChar;
    advanceChar();
  }
  while (nextChar != '"') {
    // escape \ and "
    if (nextChar == '\\') {
      advanceChar();
    }
    advanceChar();
  }
  advanceChar();
  str[idx++] = '\0';
  if (!isspace(nextChar)) {
    return NULL;
  }
  return str;
}

static char *getId()
{
  static char str[256];
  int idx = 0;
  skipSpaces();

  if (!isalpha(nextChar)) return NULL;

  while ((isalnum(nextChar) || nextChar == '_') && idx < 255) {
    str[idx++] = nextChar;
    advanceChar();
  }
  while ((isalnum(nextChar) || nextChar == '_')) {
    advanceChar();
  }
  str[idx++] = '\0';

  if (!isspace(nextChar)) return NULL;
  return str;
}

static int getNewline()
{
  skipSpaces();
  if (nextChar != '\r' && nextChar != '\n') {
    return -1;
  }
  for (;;) {
    if (nextChar == '\r') {
      advanceChar();
    } else if (nextChar == '\n') {
      advanceChar();
      line++;
    } else {
      break;
    }
  }
  return 0;
}

static int getPipe()
{
  skipSpaces();
  if (nextChar != '|') return -1;
  advanceChar();
  return 0;
}

static int getHat()
{
  skipSpaces();
  if (nextChar != '^') return -1;
  advanceChar();
  return 0;
}

#define PARSE_ERROR(x,...) do { fprintf(stderr, "Parse error at line %d: " x, line); return -1; } while(0)

static int parseSampleData(struct modfile *mod, int sample)
{
  int len = 0;
  char data[65536];

  for (;;) {
    skipNewlines();
    if (len >= 65536) {
      PARSE_ERROR("sample exceeded 65536 bytes");
    }
    int d;
    if (getDecNumber(&d) < 0) {
      break;
    }
    data[len++] = d;
  }
  char *s = getId();
  if (!s || strncmp(s, "END_DATA", 256)) {
    PARSE_ERROR("expected END_DATA");
  }
  //printf("Found sample %d, length %d\n", sample, len);

  mod->sampleData[sample-1] = (int8_t *) calloc(len, 1);
  if (!mod->sampleData[sample-1]) {
    return -1;
  }
  memcpy(mod->sampleData[sample-1], data, len);
  // length is in 16-bit words
  mod->header.samples[sample-1].len = len / 2;
  return 0;
}

static int parseSample(struct modfile *mod)
{
  char *s;
  int sample;

  sample = getHexNumber();
  if (sample < 0) {
    PARSE_ERROR("expected SAMPLE number");
  }
  if (sample < 1 || sample > 31) {
    PARSE_ERROR("sample < 1 or > 1F");
  }
  if (getNewline() < 0) {
    PARSE_ERROR("expected newline");
  }

  // set defaults: name is blank, finetune = 0
  mod->header.samples[sample-1].volume = 63;
  mod->header.samples[sample-1].repstart = 0;
  mod->header.samples[sample-1].replen = 2;

  // handle NAME, FINETUNE, VOLUME, REPEAT_START, REPEAT_LENGTH, DATA
  for (;;) {
    skipNewlines();
    s = getId();
    if (!s) {
      PARSE_ERROR("expected keyword");
    } else if (!strncmp(s, "NAME", 256)) {
      char *name = getString();
      if (!name) {
        PARSE_ERROR("expected string");
      }
      strncpy(mod->header.samples[sample-1].name, name, 22);
    } else if (!strncmp(s, "FINETUNE", 256)) {
      int finetune;
      if (getDecNumber(&finetune) < 0) {
        PARSE_ERROR("expected FINETUNE number");
      }
      mod->header.samples[sample-1].finetune = finetune;
    } else if (!strncmp(s, "VOLUME", 256)) {
      int volume;
      if (getDecNumber(&volume) < 0) {
        PARSE_ERROR("expected VOLUME number");
      }
      mod->header.samples[sample-1].volume = volume;
    } else if (!strncmp(s, "REPEAT_START", 256)) {
      int repeat_start;
      if (getDecNumber(&repeat_start) < 0) {
        PARSE_ERROR("expected REPEAT_START number");
      }
      mod->header.samples[sample-1].repstart = repeat_start / 2;
    } else if (!strncmp(s, "REPEAT_LENGTH", 256)) {
      int repeat_length;
      if (getDecNumber(&repeat_length) < 0) {
        PARSE_ERROR("expected REPEAT_LENGTH number");
      }
      mod->header.samples[sample-1].replen = repeat_length / 2;
    } else if (!strncmp(s, "DATA", 256)) {
      if (parseSampleData(mod, sample) < 0) return -1;
    } else {
      break;
    }
    if (getNewline() < 0) {
      PARSE_ERROR("expected newline");
    }
  }

  if (!s || strncmp(s, "END_SAMPLE", 256)) {
    PARSE_ERROR("expected END_SAMPLE");
  }
  return 0;
}

static int parsePatternTable(struct modfile *mod)
{
  int i;
  memset(mod->header.patternTable, 0, 128);
  for (i = 0; i < 128; i++) {
    int pattern;
    skipNewlines();
    if (getDecNumber(&pattern) < 0) {
      break;
    }
    mod->header.patternTable[i] = pattern;
  }
  skipNewlines();
  char *s = getId();
  if (!s || strncmp(s, "END_PATTERN_TABLE", 256)) {
    PARSE_ERROR("expected END_PATTERN_TABLE");
  }
  return i;
}

static int parsePattern(struct modfile *mod)
{
  int patternNum;
  if (getDecNumber(&patternNum) < 0) {
    PARSE_ERROR("expected PATTERN number");
  }
  if (patternNum < 0 || patternNum > 64) {
    PARSE_ERROR("pattern < 0 or > 64");
  }

  if (getNewline() < 0) {
    PARSE_ERROR("expected newline");
  }

  int lastrow = -1;

  for (;;) {
    int row;

    skipNewlines();

    char *s = getId();
    if (s && !strncmp(s, "END_PATTERN", 256)) {
      return patternNum;
    }
    if (getEllipses() >= 0) {
      row = lastrow + 1;
    } else {
      row = getHexNumber();
      if (row < 0) {
        PARSE_ERROR("expected row number");
      }
    }

    for (int col = 0; col < 4; col++) {

      struct note *n = &mod->patterns[patternNum].note[row][col];

      if (col < 3 && getPipe() == 0) continue;
      if (getNewline() == 0) goto nextRow;

      if (getEllipses() == 0) {
        n->periodHi = 0;
        n->periodLo = 0;
      } else if (getHat() == 0) {
        if (lastrow >= 0) {
          struct note *lastn = &mod->patterns[patternNum].note[lastrow][col];
          n->periodHi = lastn->periodHi;
          n->periodLo = lastn->periodLo;
        }
      } else {
        int p = getPitch();
        if (p < 0 && getDecNumber(&p) < 0) {
          PARSE_ERROR("expected pitch");
        }
        n->periodHi = p >> 8;
        n->periodLo = p & 0xFF;
      }

      if (col < 3 && getPipe() == 0) continue;
      if (getNewline() == 0) goto nextRow;

      if (getEllipses() == 0) {
        n->sampleLo = 0;
        n->sampleHi = 0;
      } else if (getHat() == 0) {
        if (lastrow >= 0) {
          struct note *lastn = &mod->patterns[patternNum].note[lastrow][col];
          n->sampleHi = lastn->sampleHi;
          n->sampleLo = lastn->sampleLo;
        }
      } else {
        int p = getHexNumber();
        if (p < 0) {
          PARSE_ERROR("expected instrument");
        }
        n->sampleHi = p >> 4;
        n->sampleLo = p & 0xF;
      }

      if (col < 3 && getPipe() == 0) continue;
      if (getNewline() == 0) goto nextRow;

      if (getEllipses() == 0) {
        n->effectHi = 0;
        n->effectLo = 0;
      } else if (getHat() == 0) {
        if (lastrow >= 0) {
          struct note *lastn = &mod->patterns[patternNum].note[lastrow][col];
          n->effectHi = lastn->effectHi;
          n->effectLo = lastn->effectLo;
        }
      } else {
        int p = getHexNumber();
        if (p < 0) {
          PARSE_ERROR("expected effect");
        }
        n->effectHi = p >> 8;
        n->effectLo = p & 0xFF;
      }

      if (getNewline() == 0) break;

      if (col < 3) {
        if (getPipe() < 0) {
          PARSE_ERROR("expected '|'");
        }
      } else {
        PARSE_ERROR("expected newline");
      }
    }
  nextRow:
    lastrow = row;
  }
}


static struct modfile *parseMod(char *filename)
{
  struct modfile *mod;
  int maxPattern = -1;
  int patternTableEntries = 0;

  f = fopen(filename, "rb");
  if (f == NULL) return NULL;

  mod = (struct modfile *) calloc(sizeof(*mod), 1);
  if (mod == NULL) return NULL;

  advanceChar();

  while(!feof(f)) {
    // look for PATTERN, PATTERN_TABLE, or SAMPLE:
    skipNewlines();
    if (nextChar == EOF) {
      break;
    }
    char *s = getId();
    if (s == NULL) {
      goto error;
    }
    if (!strncmp(s, "PATTERN_TABLE", 256)) {
      patternTableEntries = parsePatternTable(mod);
      if (patternTableEntries < 0) goto error;
    } else if (!strncmp(s, "PATTERN", 256)) {
      int patternNum = parsePattern(mod);
      if (patternNum < 0) goto error;
      if (patternNum > maxPattern) {
        maxPattern = patternNum;
      }
    } else if (!strncmp(s, "SAMPLE", 256)) {
      if (parseSample(mod) < 0) goto error;
    } else {
      fprintf(stderr, "Parse error at line %d: %s\n", line, s);
      goto error;
    }
  }
  fclose(f);

  strncpy(mod->header.name, filename, 20);
  if (maxPattern < 0) {
    fprintf(stderr, "Error, no patterns defined\n");
    goto error;
  }
  mod->header.numpatterns = patternTableEntries;
  mod->header.mk[0] = 'M';
  mod->header.mk[1] = '.';
  mod->header.mk[2] = 'K';
  mod->header.mk[3] = '.';

  return mod;
 error:
  free(mod);
  for (int i = 0; i < 31; i++) {
    if (mod->sampleData[i]) {
      free(mod->sampleData[i]);
    }
  }
  fclose(f);
  return NULL;
}


/// ----- main entry point ----

int main(int argc, char *argv[])
{
  if (argc < 3) {
    fprintf(stderr, "usage: makemod infile outfile\n");
    return -1;
  }

  struct modfile *mod = parseMod(argv[1]);

  if (mod == NULL) {
    return -1;
  }

  if (saveMod(argv[2], mod) < 0) {
    return -1;
  }

  return 0;
}

Here's an example of a file this can parse:

PATTERN 0
00    C-2 01 240 | C-3 03 482 | G-3 03 482 | .   .  F04
01    .   .  C00 | .   .  400 | .   .  400
02    C-3 01     | C-3 03 ^   | F-3 03 ^
03               | .   .  ^   | .   .  ^
04    D-3 01     | C-3 03 ^   | D#3 03 ^   | G-2 02
05               | .   .  ^   | ... .. ^
06    C-2 01 240 | C-3 03 ^   | F-3 03 ^
07    .   .  C00 | .   .  ^   | .   .  ^
08    D#3 01     | .   .  ^   | .   .  ^
09               | .   .  ^   | .   .  ^
0A    C-2 01 240 | C-3 03 ^   | D#3 03 ^
0B    .   .  C00 | .   .  ^   | .   .  ^
0C    D-3 01     | C-3 03 ^   | D-3 03 ^   | G-2 02
0D               | .   .  ^   | .   .  ^
0E    C-3 01     | C-3 03 ^   | D#3 03 ^
0F               | .   .  ^   | .   .  ^
10    C-2 01 240 | .   .  ^   | .   .  ^
11    ... .. C00 | .   .  ^   | .   .  ^
12    C-3 01     | C-3 03 ^   | D-3 03 ^
13               | .   .  ^   | .   .  ^
14    D-3 01     | C-3 03 ^   | C-3 03 ^   | G-2 02
15               | .   .  ^   | .   .  ^
16    C-2 01 240 | C-3 03 ^   | D-3 03 ^
17    ... .. C00 | .   .  ^   | .   .  ^
18    D#3 01     | .   .  ^   | .   .  ^
19               | .   .  ^   | .   .  ^
1A    F-3 01     | G-2 03 ^   | C-3 03 ^
1B               | .   .  ^   | .   .  ^
1C    D-3 01     | G-2 03 ^   | A#2 03 ^   | G-2 02
1D               | .   .  ^   | .   .  ^
1E    C-3 01     | G#2 03 ^   | C-3 03 ^
1F               | .   .  ^   | .   .  ^
20    C-2 01 240 | .   .  ^   | .   .  ^
21    .   .  C00 | .   .  ^   | .   .  ^
22    G#2 01     | A#2 03 ^   | D-3 03 ^
23               | .   .  ^   | .   .  ^
24    G#3 01     | C-3 03 ^   | D#3 03 ^   | G-2 02
25               | .   .  ^   | .   .  ^
26    C-2 01 240 | A#2 03 ^   | D-3 03 ^
27    .   .  C00 | .   .  ^   | .   .  ^
28    G#2 01     | .   .  ^   | .   .  ^
29               | .   .  ^   | .   .  ^
2A    C-2 01 240 | C-3 03 ^   | D#3 03 ^
2B    .   .  C00 | .   .  ^   | .   .  ^
2C    G#3 01     | D-3 03 ^   | F-3 03 ^   | G-2 02
2D               | .   .  ^   | .   .  ^
2E    G#2 01     | C-3 03 ^   | D#3 03 ^
2F               | .   .  ^   | .   .  ^
30    C-2 01 240 | .   .  ^   | .   .  ^
31    .   .  C00 | .   .  ^   | .   .  ^
32    A#2 01     | D-3 03 ^   | F-3 03 ^
33               | .   .  ^   | .   .  ^
34    A#3 01     | D#3 03 ^   | G-3 03 ^   | G-2 02
35               | .   .  ^   | .   .  ^
36    C-2 01 240 | D-3 03 ^   | F-3 03 ^
37    .   .  C00 | .   .  ^   | .   .  ^
38    A#2 01     | .   .  ^   | .   .  ^
39               | .   .  ^   | .   .  ^
3A    C-2 01 240 | D#3 03 ^   | G-3 03 ^
3B    .   .  C00 | .   .  ^   | .   .  ^
3C    A#3 01     | F-3 03 ^   | A#3 03 ^   | G-2 02
3D               | .   .  ^   | .   .  ^
3E    .   .  C00 | .   .  C00 | .   .  C00
3F               | .   .  ^   | .   .  .
END_PATTERN

PATTERN_TABLE
  0
END_PATTERN_TABLE

SAMPLE 1
  NAME "low triangle"
  FINETUNE 0
  VOLUME 63
  REPEAT_START 0
  REPEAT_LENGTH 64
  DATA
   -127 -119 -111 -103 -95 -87 -79 -71 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64 71 79 87 95 103 111 119 127 119 111 103 95 87 79 71 64 56 48 40 32 24 16 8 0 -8 -16 -24 -32 -40 -48 -56 -64 -71 -79 -87 -95 -103 -111 -119
  END_DATA
END_SAMPLE

SAMPLE 2
  NAME "drum"
  FINETUNE 0
  VOLUME 63
  REPEAT_START 0
  REPEAT_LENGTH 2
  DATA
 0 0 103 -12 -58 15 -81 93 -67 -84 -120 -44 15 84 79 -120 -32 -1 -8 -116 31 21 -50 -15 -92 -50 68 -117 -58 128 -13 -88 99 -118 -97 -63 67 103 -70 45 82 115 96 -26 112 -15 -85 -47 47 112 115 26 115 -23 -116 -52 -118 94 -16 -103 -91 -65 -70 95 68 -39 -28 25 10 -41 91 -59 112 -27 48 -56 -119 113 10 45 28 65 124 59 92 126 67 -89 -43 -119 51 118 59 61 -82 -36 20 -9 86 81 -44 -9 45 25 -92 -122 -95 -70 -12 10 126 56 123 -113 36 118 -58 116 104 -77 61 -82 -100 -46 -38 -25 -54 -46 57 114 -104 -55 26 85 73 16 -117 44 90 -110 -35 -61 -16 -48 120 -93 -71 101 32 -25 -36 38 60 60 118 -115 -18 -124 53 73 18 -121 -125 126 -75 83 -34 -9 -98 73 -11 -51 -35 -3 -17 52 46 -125 -106 -9 72 82 35 -33 100 -63 -110 59 69 103 69 115 10 118 123 7 89 102 111 -10 67 113 80 111 -13 86 125 -32 -83 83 23 119 -2 -62 46 107 31 -44 -1 -23 -105 24 98 15 -50 102 76 71 26 16 -50 13 126 -3 48 -4 64 -73 -63 -102 54 92 75 63 -100 6 89 89 -30 -57 -95 50 15 -124 -20 -15 74 85 24 -90 25 59 -17 -73 -59 127 -4 85 27 82 117 39 34 38 -111 113 103 51 -47 -39 -107 6 29 59 -126 -83 -8 -34 116 111 118 -74 -86 19 25 52 -49 -26 -41 -94 -104 13 -45 127 58 19 56 123 -18 106 102 86 -114 -35 -51 34 -92 -31 94 -63 -119 128 28 -64 -15 61 16 -15 27 -80 80 82 -2 66 -23 59 -87 3 44 -88 -111 -41 -13 67 -93 103 -70 -30 -19 122 -81 98 14 -45 103 12 -8 80 -34 121 15 70 47 -74 86 94 -116 30 -57 64 -2 76 7 26 -70 -34 -51 91 122 33 101 124 38 77 -23 -3 -110 78 -37 90 126 -3 -36 -113 107 53 -15 120 58 41 5 97 -111 -6 -47 -77 -88 103 -108 118 52 -123 -125 91 -7 87 -70 79 -63 128 113 15 106 36 72 18 -78 105 5 128 -30 -74 60 -47 -97 100 50 113 -20 82 79 -35 107 -53 -84 -10 -65 -126 30 42 42 47 90 -117 47 19 50 102 -58 -109 -47 75 1 -15 -84 -59 48 82 53 21 127 -9 -1 -57 41 -23 -50 28 75 121 71 26 -12 -87 -73 52 117 36 101 -71 48 -46 -120 83 -16 -104 112 -27 23 -65 40 -33 -4 -24 -41 -58 69 -65 119 109 -57 -70 -62 -64 -65 -17 84 31 -77 80 125 -89 91 -66 -20 -44 -16 -43 -101 45 -114 -75 1 -56 -20 26 -114 100 92 -105 -81 -123 24 54 67 -72 120 -3 -111 -81 -40 98 -116 -36 37 85 -115 75 -113 -51 -57 106 28 2 61 -98 99 9 -125 -85 -60 -117 47 -119 1 -4 -55 24 -53 108 -125 17 -32 -32 -121 49 15 69 75 -64 -76 -33 57 -95 -17 -122 -108 55 78 106 106 34 -93 -111 72 63 33 59 -75 76 122 -19 -64 -4 44 91 115 -94 51 -106 -100 -93 5 -4 -13 77 8 44 -3 -42 -61 122 -59 75 0 -45 31 -125 -110 108 105 86 106 47 72 50 -76 -11 75 -126 15 -16 -112 -14 88 31 63 112 -44 64 96 -16 6 40 -7 -80 -69 -21 -123 65 -111 -16 12 29 -15 -106 -54 -108 60 -38 40 -4 -29 -43 -100 -32 -30 -4 76 50 90 4 92 -8 -59 34 17 11 -26 -19 -92 -52 -125 111 29 101 -19 -24 15 26 3 23 -93 68 40 -37 -47 -57 -88 -41 86 -67 -109 0 -95 -24 -103 57 -39 18 40 40 52 -50 -105 35 -59 102 -50 56 -42 -33 25 104 -59 116 62 -125 90 51 17 -40 -42 -107 -10 41 19 -24 112 -35 115 -119 -14 52 53 4 30 64 78 -41 17 92 24 3 91 48 -99 1 -122 -14 105 -115 115 -81 -32 -74 -102 -16 -56 -101 1 -66 -37 16 13 -19 -28 -29 -55 -81 -94 -122 66 2 58 68 -58 -77 -110 -18 53 123 14 -115 22 -56 12 -13 -97 -30 69 108 50 121 -94 38 -28 17 -76 -26 -40 -36 -31 -97 1 82 -39 1 -96 20 5 -36 94 -39 -10 -66 70 -57 7 9 96 113 -114 123 35 -108 -72 -35 -18 67 107 34 117 66 8 90 82 -93 67 31 -25 88 -117 -102 115 -26 9 104 -40 43 59 -70 15 -10 29 12 6 -54 98 112 79 121 7 -98 39 -78 92 114 -100 -51 77 -14 -1 -119 20 19 63 -107 74 -77 9 109 125 122 -49 66 -74 93 41 -100 22 119 89 66 53 -64 -116 -9 -97 -85 -19 43 122 -55 9 111 -84 75 -100 99 7 122 66 58 -126 -74 -60 -106 61 83 -39 -92 43 85 45 -91 -30 46 5 123 -58 95 -31 -92 -105 62 -32 -29 -49 -51 -60 -8 125 -28 114 -84 109 66 92 27 -99 -10 40 -112 -121 -53 111 5 97 -30 99 10 101 -100 -72 52 -34 -126 -29
  END_DATA
END_SAMPLE

SAMPLE 3
  NAME "25% pulse low"
  FINETUNE 0
  VOLUME 63
  REPEAT_START 0
  REPEAT_LENGTH 32
  DATA
    -127 -127 -127 -127 -127 -127 -127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127
  END_DATA
END_SAMPLE

And here's a Makefile you can use to build it all:

# Makefile for playmod, makemod, dumpmod
LDFLAGS = -framework CoreFoundation -framework AudioToolbox -F/Library/Frameworks -lm

all: playmod makemod dumpmod

playmod: playmod.c mod.c Makefile
	cc -g $(LDFLAGS) $(filter-out Makefile,$^) -o $@

makemod: makemod.c mod.c Makefile
	cc -g $(filter-out Makefile,$^) -o $@

dumpmod: dumpmod.c mod.c Makefile
	cc -g $(filter-out Makefile,$^) -o $@

I think that the design for makemod could have been a lot simpler just using fscanf(), but I thought it would be fun to structure it like a recursive-descent parser (even though there's no recursion in this format).

The next step will be actually playing back some simple MOD files.

Simple Amiga MOD File Player: Preliminaries

I've always been fascinated by making music with computers. When I was in middle school, in the dark ages before I had internet access, I bought a book that was supposed to tell me how to program my Sound Blaster card, but its sample code didn't show you how to do anything other than play existing files from disk. The problem was that I didn't have any music files to play in the first place, so that didn't help at all.

The book wasn't all a waste, though. It came with some Amiga MOD music to listen to. This was intriguing, because up to this point I had only heard my computer do OPL3 FM synthesis, which sounds kind of embarrassing, while the Amiga MOD files sounded like real instruments.

Unfortunately, without the internet, I had to wait a few more years before I found another book that explained how to actually read MOD files and play them. And I never got around to finishing my player.

So now I'm going to finally write that MOD player I always wanted to write. First thing's first: set up the development environment on my Mac. For now, I'll do development on the command line with Emacs and Makefiles. I'll use the native Core Audio API for the audio interface so I can stay out of Objective-C.

First, I had to use

$ xcode-select --install
to get the command line compiler, header files in /usr/include, and git.

I already have emacs installed, from emacsformacosx.com.

Here's "Hello, World" just to make sure everything works. This is playmod.c:

/* playmod.c */
/* Test of basic compilation functionality. */

#include <stdio.h>

int main(int argc, char *argv[])
{
  printf("Hello, playmod\n");
  return 0;
}

Here is the Makefile to build this:

# Makefile for playmod
playmod: playmod.c Makefile
	cc $< -o $@

This builds with make, and the resulting executable works:

$ make
cc playmod.c -o playmod
$ ./playmod
Hello, playmod

The next step is playing sound. The relevant API is called AudioQueue, part of the AudioToolbox framework. This works like most other sound programming APIs. You provide a callback function to fill a buffer with new samples whenever the sound hardware needs more.

The only confusing part was bootstrapping the playback. Other APIs I've used will start calling your callback when you begin playback. AudioQueue seemed to require that I call AudioQueueEnqueueBuffer() on each new buffer to get things started. I did this by calling the callback function myself on each new buffer I allocated.

Here's a new version of playmod.c that still doesn't play MOD files, but just plays sine waves out of each speaker:

/* playmod.c */
/* Plays sine waves for 5 seconds. */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>

#include <AudioToolbox/AudioQueue.h>
#include <AudioToolbox/AudioFormat.h>

#define BUFFER_SIZE 16384

// Flag set if the callback fails so the main application can quit
OSStatus callback_error = noErr;

int callback_calls = 0;

void callback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
{
  static float t = 0;
  OSStatus err;
  float *buf = (float *) inBuffer->mAudioData;
  callback_calls++;
  for (int i = 0; i < BUFFER_SIZE / 4; i+=2) {
    t += 1.0/44100.0;
    buf[i] = sin((float) 440 * 2 * M_PI * t);
    buf[i+1] = sin((float) 660 * 2 * M_PI * t);
  }

  err = AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
  if (err != noErr) callback_error = err;
}

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 main(int argc, char *argv[])
{
  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,
   };
  err = AudioQueueNewOutput(&format, callback, NULL, 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(NULL, aq, buf[i]);
  }

  err = AudioQueueStart(aq, NULL);
  if (err != noErr) {
    printf("AudioQueueStart Error %d: %s\n", err, aq_error_string(err));
    return -1;
  }

  sleep(5);

  printf("Callback was called %d times\n", callback_calls);

  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;
}

The Makefile has to be modified to tell clang which frameworks are being used:

# Makefile for playmod
LDFLAGS = -framework CoreFoundation -framework AudioToolbox -F/Library/Frameworks -lm

playmod: playmod.c Makefile
	cc $(LDFLAGS) $< -o $@

This code passes NULL as the 4th argument to AudioQueueNewOutput() so that AudioQueue allocates a thread to run our callback for us. That allows us to use the normal Unix sleep() call to wait for playback instead of any extra complexity involving CFRunLoopRun().

Next time, I'll implement MOD file reading and writing.