Sunday, July 10, 2016

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.

No comments:

Post a Comment