Andrew Que Sites list Photos
Projects Contact
Main

May 04, 2021

Rendering Space Debris

   As my MOD player becomes more and more functional I decided it was time to tackle a classic MOD.  I asked my good friend Zen what his favorite MOD was, and he said Space Debris.  This has to be among the all time greatest MODs ever composed.  The work of Finnish composer Markus Kaarlonen for the Horizon Party of 1990 this composition is both a musically and implementation complex.  Playing it back would be a challenge.
   I knew from looking at the MOD I would need to get the following effect functional:
  • Vibrato.
  • Vibrato + volume slide.
  • Portamento + volume slide.
  • Retrigger.
  • Fine volume slide up/down.
   Vibrato had been on my list for awhile.  My biggest issue implementing it was proving I did it correctly.  I had to find some MOD files that used vibrato with various parameters.  There are two.  The first determines the depth or how much the vibrato shifts the note.  The other is the speed at which the vibrato operates.  Once I was convinced I had this implemented correctly it was time for a test. 
   Found some interesting things about how MODs handle notes.  Setting the sample always resets the volume even if no note is played.  In Space Debris, some of the samples have 0 for the initial volume and volume slides are used to fade them in.  Setting the sample was a way to cause a volume reset and start a new slide.  The other thing I found was that the volume parameter is allowed to be set above 64 in the parameter although it shouldn't be allowed.  I had to limit that as there were two places the volume was set higher than 64.
   Fine volumes were very simple.  Retrigger wasn't too hard and I used the effect myself to make a kind of multi-hit snare drum sound.  Seems I may have learned this from Space Debris.  So, I am now able to render Space Debris and I'm fairly convinced this is accurate.
Space Debris

May 03, 2021

mod.softhunter

Sometime in 1994 I found the mod mod.softhunter which is a variation of J. S. Bach’s Toccata and Fugue in D minor. A truncated version of the toccata starts off with a strong synth instrument before giving way to the fugue played with a guitar which is then accompanied with a simple beat. The song is kind of fun but one that created problems for some MOD players. Otto Chrons’ Dual Module Player (DMP) rendered the MOD just fine, but other plays such as Kay Bruns’ Mod4Win did not.

Since this is a problem child I decided to have a look at rendering it myself. I had always believed the problem to be that the effect pattern break was implemented incorrectly. This effect has parameters which are supposed to specify the offset into the next pattern at which to begin the next note. Until this experiment I thought DMP implemented this position jump and Mod4Win did not. Turns out that assumption was incorrect. The MOD incorrectly uses pattern break. A position is specified but unless this is ignored the playback doesn’t render correctly. Interestingly, VLC which uses the ModPlug library, renders this module just fine. I’m not sure if ModPlug knows to ignore the pattern break parameters of certain files or just didn’t implement them. At the moment, I don’t have any MOD files that require a fully functional pattern break to work, so I cannot test it.

For my own player I added the pattern break’s parameters. It wasn’t until I slowed down DMP I could see that the pattern break parameters were being ignored. When I ignored the parameters the playback was mostly correct—mostly. On pattern 17 there are two speed changes in the first division. One sets the speed to 6 ticks/division, and second sets the speed to 0 ticks/division. My player would effectively skip every note at 0 ticks/division until the speed changed to something else. I simply added an ignore to speed settings of 0 ticks/division. My reference documentation says most players treat this speed as 1 tick/division. So this is probably a second item to cause incompatibility between players.

April 30, 2021

Waveform Audio File Format

In previous articles of the series on Amiga MOD files I wrote about implementing a system to read the file format and print the notes. The goal is to be able to render audio output. Rather than directly have audio go to a sound device I decided that writing the output to an audio file would be easier. One of the simplest audio file formats is the uncompressed Waveform Audio File Format, WAVE, or just WAV. It was created in the early 90s by IBM and Microsoft—right around the time I was learning how to write my own software. So along with BMP it became one of the first file formats I reverse engineered sometime between 1994 and 1996. With ready access to the Internet it is no longer necessary to manually workout the details by trial and error. In this article I want to look at what will take me much longer to write about than it did to implement.

Although my goal was to write WAV files, the first step was to be able to read them. The format supports storing multiple chunks of data, but most of the time there is just a single chunk. This site gave me enough detail on how the header to a WAV file is laid out. To correctly read a WAV file I would really need to account for all of the chunks. But the assumption was we were only going to use a single chunk—so that is all I was concerned about.

Now there is a weird mix of little/big endian. Why anyone would do this I couldn’t say—usually you pick one or the other. Intel has always been little endian and it turns out the specification only defines headers descriptions in big endian. It is easier just to tread those as 4 characters. Right away I could most of the all the data was 32-bit aligned. The are 16-bit fields but they come in pairs. Knowing I was writing for a little endian system I could take a shortcut—I could read the header directly into a C structure. C structures are get complected due to alignment. However, this structure was already aligned so I could use the structure trick. To be sure I used bitfields, making the 16-bit words actually bit fields in 32-bit words.

Let’s look at code that simply reads the WAV file header and prints the fields.

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "fileUtilities.h"

int main( int argumentCount, char * * arguments )
{
  bool isError = ( argumentCount <= 1 );

  if ( isError )
    fprintf( stderr, "Syntax: %s <MOD file>\n", arguments[ 0 ] );

  char const * fileName = arguments[ 1 ];

  FILE * inputFile = NULL;
  if ( ! isError )
  {
    inputFile = fopen( fileName, "rb" );

    isError = ( NULL == inputFile );
    if ( isError )
      fprintf( stderr, "Unable to open `%s`.\n", fileName );
  }

  typedef struct
  {
    uint32_t chunkId;
    uint32_t chunkSize;
    uint32_t chunkFormat;

    uint32_t subChunkId1;
    uint32_t subChunkSize1;

    uint32_t format : 16;
    uint32_t channels : 16;

    uint32_t sampleRate;
    uint32_t byteRate;
    uint32_t blockAlign : 16;
    uint32_t bitesPerSample : 16;

    uint32_t subChunkId2;
    uint32_t subChunkSize2;

  } WaveHeader;

  WaveHeader waveHeader;
  isError |= fileRead( inputFile, &waveHeader, sizeof( waveHeader ) );

  if ( ! isError )
  {
    printf( "chunkId.........: %.4s\n"(char*)&waveHeader.chunkId     );
    printf( "chunkSize.......: %d\n",   waveHeader.chunkSize           );
    printf( "chunkFormat.....: %.4s\n"(char*)&waveHeader.chunkFormat );
    printf( "\n" );
    printf( "subChunkId1.....: %.4s\n"(char*)&waveHeader.subChunkId1 );
    printf( "subChunkSize1...: %d\n",   waveHeader.subChunkSize1       );
    printf( "\n" );
    printf( "format..........: %04X\n", waveHeader.format              );
    printf( "channels........: %d\n",   waveHeader.channels            );
    printf( "\n" );
    printf( "sampleRate......: %d\n",   waveHeader.sampleRate          );
    printf( "byteRate........: %d\n",   waveHeader.byteRate            );
    printf( "blockAlign......: %d\n",   waveHeader.blockAlign          );
    printf( "bitesPerSample..: %d\n",   waveHeader.bitesPerSample      );
    printf( "\n" );
    printf( "subChunkId2.....: %.4s\n"(char*)&waveHeader.subChunkId2 );
    printf( "subChunkSize2...: %d\n",   waveHeader.subChunkSize2       );
  }

  if ( inputFile )
    fclose( inputFile );

  int returnResult = 0;
  if ( isError )
    returnResult = -1;

  return returnResult;
}

This code was created in a very short amount of time so very little through was given to cleaning it up. What I really wanted now was to create a WAV file. Armed with the header information I wrote a program to produce a cord and write the results to a WAV file.

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "fileUtilities.h"

enum { SAMPLE_RATE = 4000 };
enum { DURATION = 3000 };
enum { SAMPLES = SAMPLE_RATE * DURATION / 1000 };

enum
{
//C   C#   D   D#   E   F   F#   G   G#   A   A#   B
  C0, C_0, D0, D_0, E0, F0, F_0, G0, G_0, A0, A_0, B0,
  C1, C_1, D1, D_1, E1, F1, F_1, G1, G_1, A1, A_1, B1,
  C2, C_2, D2, D_2, E2, F2, F_2, G2, G_2, A2, A_2, B2,
  C3, C_3, D3, D_3, E3, F3, F_3, G3, G_3, A3, A_3, B3,
  C4, C_4, D4, D_4, E4, F4, F_4, G4, G_4, A4, A_4, B4,
  C5, C_5, D5, D_5, E5, F5, F_5, G5, G_5, A5, A_5, B5,
  C6, C_6, D6, D_6, E6, F6, F_6, G6, G_6, A6, A_6, B6,
  C7, C_7, D7, D_7, E7, F7, F_7, G7, G_7, A7, A_7, B7,
  C8, C_8, D8, D_8, E8, F8, F_8, G8, G_8, A8, A_8, B8,

  NUMBER_OF_NOTES
};

typedef struct
{
  char     chunkId[ 4 ];
  uint32_t chunkSize;
  char     chunkFormat[ 4 ];

  char     subChunkId1[ 4 ];
  uint32_t subChunkSize1;

  uint32_t format : 16;
  uint32_t channels : 16;

  uint32_t sampleRate;
  uint32_t byteRate;
  uint32_t blockAlign : 16;
  uint32_t bitesPerSample : 16;

  char     subChunkId2[ 4 ];
  uint32_t subChunkSize2;

} WaveHeader;

static WaveHeader const DEFAULT_HEADER =
{
  { 'R''I''F''F' },
  SAMPLES + 36,
  { 'W''A''V''E' },

  { 'f''m''t'' ' },
  16,

  0x01,
  0x01,

  SAMPLE_RATE,
  SAMPLE_RATE,
  1,
  8,

  { 'd''a''t''a' },
  SAMPLES
};


int main( int argumentCount, char * * arguments )
{
  bool isError = ( argumentCount <= 1 );

  if ( isError )
    fprintf( stderr, "Syntax: %s <out file>\n", arguments[ 0 ] );

  char const * fileName = arguments[ 1 ];

  FILE * outputFile = NULL;
  if ( ! isError )
  {
    outputFile = fopen( fileName, "wb" );

    isError = ( NULL == outputFile );
    if ( isError )
      fprintf( stderr, "Unable to open `%s`.\n", fileName );
  }

  if ( ! isError )
  {
    WaveHeader waveHeader;
    memcpy( &waveHeader, &DEFAULT_HEADER, sizeof( waveHeader ) );
    fwrite( &waveHeader, sizeof( waveHeader )1, outputFile );

    // Generate frequencies for all notes.
    float notes[ NUMBER_OF_NOTES ];
    for ( unsigned index = 0; index < NUMBER_OF_NOTES; index += 1 )
      notes[ index ] = pow( 2( ( (float)index - 57 ) / 12.0 ) ) * 440.0;

    uint8_t samples[ SAMPLES ];
    for ( unsigned index = 0; index < SAMPLES; index += 1 )
    {
      // Linear fade out.
      float volume = 128.0 * ( 1.0 - (float)index / SAMPLES );

      float time = (float)index / SAMPLE_RATE;
      float const TWO_PI = 6.28318530717958647692528676655;
      float radians = TWO_PI * time;

      // C major seventh
      float sample =
        (
          sin( radians * notes[ C3 ] )
        + sin( radians * notes[ E3 ] )
        + sin( radians * notes[ G3 ] )
        + sin( radians * notes[ B3 ] )
        ) * volume / 4.0 + 128.0;

      if ( sample > 255 )
        sample = 255;

      samples[ index ] = sample;
      //printf( "%3.8f %3i %02X\n", sample, samples[ index ], samples[ index ] );
    }

    fwrite( samples, sizeof( samples )1, outputFile );
  }

  if ( outputFile )
    fclose( outputFile );

  int returnResult = 0;
  if ( isError )
    returnResult = -1;

  return returnResult;
}

Here is a simple file that uses a default header for an 8-bit, mono wave file of a fixed duration. Small modifications such as to duration and sample rate can be made and tested. It simply makes a C major seventh chord with a linear fade out and writes the data to the wave file. Part of this process requires building a table of note frequencies. A typical piano has 88 keys, but I didn’t like the odd range—7.3 octaves. So I went for an extended note rage of 108 keys—a full 9 octaves, from C0 to B8. The frequency of each notes follows this equation:

Where f is frequency, and n is the note from 0 to 107 with 0 being C0 and 107 being B8. This is a slight modification of the equation found for piano key freuencies being offset of account for the large range. Most of the file is fairly self-explanatory. The note generation happens here:

      float time = (float)index / SAMPLE_RATE;
      float const TWO_PI = 6.28318530717958647692528676655;
      float radians = TWO_PI * time;

      // C major seventh
      float sample =
        (
          sin( radians * notes[ C3 ] )
        + sin( radians * notes[ E3 ] )
        + sin( radians * notes[ G3 ] )
        + sin( radians * notes[ B3 ] )
        ) * volume / 4.0 + 128.0;

      if ( sample > 255 )
        sample = 255;

      samples[ index ] = sample;

First, we find the time in seconds. Then we calculate the angle of this time in radians. Multiplying this by the note frequency and take the sine and we get a the desired note. Add these notes up and we get a chord. The amplitude of each sine wave is 1, so adding four of them together will result in a maximum amplitude of 4. Thus we divide by 4. That produces a number between -1 and 1. For 8-bit PCM we need a value between 0 and 255 with 128 being the zero point. The maximum volume is 128 so we multiply by this changing our range from -128 to +128. Adding 128 will give a range between 0 and 256. 256 is too high so we clip it to 255. We now have the 8-bit PCM value that is stored in the sample buffer.

Just for fun I generated a couple of other chords, picked difference sample frequencies and duration. Naturally they are all functional as there isn’t anything special. All of this was to demonstrate I could write a valid WAV file. Now satisfied, it was time to make a library to create WAV files from a sample set.

My first library was more or less a wrapper of what is shown here. A single function that took a sample frequency and a set of samples and wrote them to a WAV file. That was enough for my first day’s work, but as the project progressed I needed better functions.

The second function I wrote simply appended samples to a WAV file. This allowed me to incrementally add data. The third and most useful function set allows a WAV file to be opened, samples added, and then closed at which time the header details are completed. The this version also included the number of channels so stereo WAV files could be created.

//=============================================================================
// Uses: Export sound samples to a WAVE file.
// Date: 2021-04-16
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "waveExport.h"
#include "fileUtilities.h"

enum { HEADER_SIZE = 36 };

typedef struct
{
  char     chunkId[ 4 ];
  uint32_t chunkSize;
  char     chunkFormat[ 4 ];

  char     subChunkId1[ 4 ];
  uint32_t subChunkSize1;

  uint32_t format : 16;
  uint32_t channels : 16;

  uint32_t sampleRate;
  uint32_t byteRate;
  uint32_t blockAlign : 16;
  uint32_t bitesPerSample : 16;

  char     subChunkId2[ 4 ];
  uint32_t subChunkSize2;

} WaveHeader;

static WaveHeader const DEFAULT_HEADER =
{
  { 'R''I''F''F' },
  0,
  { 'W''A''V''E' },

  { 'f''m''t'' ' },
  16,

  0x01,
  0x01,

  0,
  0,
  1,
  8,

  { 'd''a''t''a' },
  0
};

//-----------------------------------------------------------------------------
// Uses:
//   Export sample to a WAVE file.
// Input:
//   fileName - Name of file to create.
//   sampleRate - Samples/second.
//   samples - 8-bit sample data.
//   sampleSize - Number of samples.
// Output:
//   True if there was an error.
//-----------------------------------------------------------------------------
bool waveExport
(
  char const * fileName,
  uint16_t sampleRate,
  uint8_t const * samples,
  uint32_t sampleSize
)
{
  bool isError = false;

  FILE * outputFile = NULL;
  if ( ! isError )
  {
    outputFile = fopen( fileName, "wb" );
    isError = ( NULL == outputFile );
  }

  if ( ! isError )
  {
    WaveHeader waveHeader;
    memcpy( &waveHeader, &DEFAULT_HEADER, sizeof( waveHeader ) );

    waveHeader.chunkSize     = sampleSize + HEADER_SIZE;
    waveHeader.sampleRate    = sampleRate;
    waveHeader.byteRate      = sampleRate;
    waveHeader.subChunkSize2 = sampleSize;

    isError |= ( 1 != fwrite( &waveHeader, sizeof( waveHeader )1, outputFile ) );
    isError |= ( 1 != fwrite( samples, sampleSize, 1, outputFile ) );
  }

  if ( outputFile )
    fclose( outputFile );

  return isError;
}

//-----------------------------------------------------------------------------
// Uses:
//   Append data to wave file.  File is created if it does not exist.
// Input:
//   fileName - Name of file to create.
//   sampleRate - Samples/second.
//   samples - 8-bit sample data.
//   sampleSize - Number of samples.
// Output:
//   True if there was an error.
//-----------------------------------------------------------------------------
bool waveExportAppend
(
  char const * fileName,
  uint16_t sampleRate,
  uint8_t const * samples,
  uint32_t sampleSize
)
{
  bool isError = false;

  bool fileExists = false;
  FILE * inputFile = NULL;
  if ( ! isError )
  {
    inputFile = fopen( fileName, "rb" );
    fileExists = ( NULL != inputFile );
  }

  if ( ! fileExists )
    isError = waveExport( fileName, sampleRate, samples, sampleSize );
  else
  {
    // Read file header.
    WaveHeader waveHeader;
    isError |= fileRead( inputFile, &waveHeader, sizeof( waveHeader ) );

    fclose( inputFile );

    // Reopen file for appending.
    FILE * outputFile = fopen( fileName, "r+b" );
    isError = ( NULL == outputFile );
    fseek( outputFile, 0, SEEK_END );

    // Write new samples.
    if ( ! isError )
      isError |= ( 1 != fwrite( samples, sampleSize, 1, outputFile ) );

    // Write new header.
    if ( ! isError )
    {
      // Update header.
      waveHeader.chunkSize     += sampleSize;
      waveHeader.subChunkSize2 += sampleSize;

      // Write new header at beginning of file.
      rewind( outputFile );
      isError |= ( 1 != fwrite( &waveHeader, sizeof( waveHeader )1, outputFile ) );

      fclose( outputFile );
    }
  }

  return isError;
}

//-----------------------------------------------------------------------------
// Uses:
//   Start a wave file.
// Input:
//   fileName - Name of file to create.
//   sampleRate - Samples/second.
//   channels - Number of channels. (1=mono, 2=stereo)
// Output:
//   Wave file context.  NULL if there was an error.
//-----------------------------------------------------------------------------
WaveContext * waveStart( char const * fileName, uint16_t sampleRate, uint8_t channels )
{
  bool isError = false;

  FILE * outputFile = NULL;
  if ( ! isError )
  {
    outputFile = fopen( fileName, "w+" );
    isError = ( NULL == outputFile );
  }

  if ( ! isError )
  {
    WaveHeader waveHeader;
    memcpy( &waveHeader, &DEFAULT_HEADER, sizeof( waveHeader ) );

    waveHeader.sampleRate = sampleRate;
    waveHeader.byteRate   = sampleRate;
    waveHeader.channels   = channels;

    isError |= ( 1 != fwrite( &waveHeader, sizeof( waveHeader )1, outputFile ) );

    if ( isError )
    {
      fclose( outputFile );
      outputFile = NULL;
    }
  }

  return outputFile;
}

//-----------------------------------------------------------------------------
// Uses:
//   Write some samples to open wave file.
// Input:
//   wave - Open wave file.  Use `waveStart` to create this.
//   samples - 8-bit sample data.
//   sampleSize - Number of samples.
// Output:
//   True if there was an error.
//-----------------------------------------------------------------------------
bool waveAddSamples( WaveContext * wave, uint8_t const * samples, uint32_t sampleSize )
{
  FILE * outputFile = (FILE *)wave;
  return ( 1 != fwrite( samples, sampleSize, 1, outputFile ) );
}

//-----------------------------------------------------------------------------
// Uses:
//   Close open wave file.
// Input:
//   wave - Open wave file.  Use `waveStart` to create this.
// Output:
//   True if there was an error.
// Notes:
//   This must be called before wave file is valid.
//-----------------------------------------------------------------------------
bool waveClose( WaveContext * wave )
{
  FILE * outputFile = (FILE *)wave;

  long int sampleSize = ftell( outputFile ) - sizeof( WaveHeader );
  rewind( outputFile );

  // Read file header.
  WaveHeader waveHeader;
  bool isError = fileRead( outputFile, &waveHeader, sizeof( waveHeader ) );

  rewind( outputFile );

  waveHeader.chunkSize     = sampleSize + HEADER_SIZE;
  waveHeader.subChunkSize2 = sampleSize;

  isError |= ( 1 != fwrite( &waveHeader, sizeof( waveHeader )1, outputFile ) );

  return isError;
}

If I expand the MOD library to handle other formats I might also add the ability to work with 16-bit data. For now, 8-bit data is sufficient.

   Starting yesterday evening and continuing though through the day today I felt very lethargic which is likely the result of my second COVID-19 vaccination dose.  I didn't feel bad, but I just wanted to sleep and do almost nothing.  The second dose effect everyone to a different degree.  Steve had no effect.  Another roommate just felt crappy.  I just felt lazy.
Second Dose

Second Dose

   Today was my second COVID-19 vaccination shot and in a week from now I will be considered fully inoculated.  I received an e-mail on Saturday, April 3rd saying that I was eligible to sign up.  I had been waiting for my turn since December when the first vaccine was approved.  So on receiving the e-mail I immediately began the enrollment process and scheduled an appointment for the following Tuesday morning at the FEMA center in Milwaukee.  My good friend Steve also received the eligibility notice and signed up, and we got the same time.  Latter our friend Kim, who had an appointment for latter in the month, signed up and get a time window 15 minutes after ours.  We decided to go as a group.
   Tuesday, April 6th we took the trip out to Milwaukee.  From the time we got out of the car it was mostly following arrows and before long we had our first shot of Comirnaty, the Pfizer-BioNTech COVID-19 vaccine.  The longest part of the process was waiting the 15 minutes after the shot to make sure there were no allergic reactions.  Starting that evening and continuing through the following day my arm felt like someone gave it a good solid punch, but that was it.  On the second day I could hardly notice my arm.
   Our second dose was scheduled for today at 2:45 pm, and again we traveled as a group.  This time we were given more information before the shot, pretty much all of which I already knew from my own research.  Kim took this picture of me getting my second dose.  After the 15 minute wait for allergies we went in search of ice cream to celebrate.  In a weeks time, we would be among those who showed a 94% chance of not getting infected.
   The COVID-19 pandemic has put a large damper on life over the past year, and my hopes are that getting the vaccine is the first step toward returning activities I've had to postpone during that time.  A salute to the Security Force Assistance Brigades from Fort Bragg who came up to Milwaukee to administer shots, everyone who is putting forth effort into getting us vaccinated, all the researchers who put the vaccine together and all the front-line workers who have been battling this virus.

April 26, 2021

Printing MOD File

In an earlier article I described in detail the parts necessary for loading a MOD file. Now that we have this information we can print out how a song is played.

For this we actually have to consider the effects and how the notes work. Let’s start with the notes. MOD files don’t use notes per sa—they use a period. This defines the time between samples which directly correlates to pitch. The larger the period, the more time between samples, the lower the pitch.

The original MOD format allowed periods between 113 and 856 which correspond to C in the 1st octave (C-1) to B in the 3rd octave (B- 3). Something important to keep in mind is that the actual key and octave isn’t absolute—it is relative. The actual tuning of the note depends on the sample itself. If the sample is 440 Hz sine wave this would know this to be an absolute A4 in scientific pitch. However, it might have been sampled such that at a period of 428 results in a plays back at 440 Hz, which would make this a C2 on the MOD scale. It doesn’t matter because if all the other samples are tuned similarly then the notes produced are still accurate.

MOD extensions push this range from C-0 to B-4. The actual notes are then:

  C C# D D# E F F# G G# A A# B
Octave 0 1,712 1,616 1,525 1,440 1,357 1,281 1,209 1,141 1,077 1,017 961 907
Octave 1 856 808 762 720 678 640 604 570 538 508 480 453
Octave 2 428 404 381 360 339 320 302 285 269 254 240 226
Octave 3 214 202 190 180 170 160 151 143 135 127 120 113
Octave 4 107 101 95 90 85 80 76 71 67 64 60 57

The observe will see that each column roughly doubles each octave. The period is the inverse of frequency so this is what we would expect. The reason the columns do not double exactly is because they are integers and the fractional parts are missing. Thus the doubling takes into account the fractional part, but doesn’t display it. If we assume the row for C is fixed, then each column is roughly the column to the right * 21/12 which is the standard semitone in the twelve-tone equal temperament scale used in western music. I’ve yet to compute exactly how this table was derived as the values—particularly for octave 0 E to G#—are off by more than we can account for with rounding.

The reference documentation states that these values are not always exact and recommends choosing the closest period value to determine the note. In reality, it doesn’t matter what periods one starts with as long as all the periods follow twelve-tone equal temperament. But to print the notes, picking the closest value to the periods in the chart should allow the printing of the tab.

So we can now determine the note, but there are just a couple of other items we need to address before we can print MOD tab. First is the range of what to print. We could just print all the patterns, which will produce a tab, but it doesn’t mean the tab is in order. For that we need to use the pattern table. The pattern table defines which patterns to play and the order in which to play them. From a playback standpoint that means we loop for each pattern to be played.

For each pattern there are 64 divisions where a note can be played. But a note doesn’t have to be played. A new note begins if a period is specified. The note will continue to sound until it either ends or a new note begins. We cannot ignore the divisions without a note because effects could be applied. That we can represent in the tab as well.

There is one last item before we can print the tab. One of the effects is a pattern break. That is, we stop the current pattern and start from the given offset in the next pattern. So although patterns can contain 64 notes, you don’t need to use them all. So we will have to look for pattern breaks. There is also a position jump. This is more complected because position jumps are often used to repeat a song indifferently. It is easiest simply to ignore position jumps. Technically a position jump could jump around without looping, but in practice most position jumps are just for looping part of all of the song. I used this effect myself when making video game music. A position jump would loop the level music, but ignoring the position jump would continue the track onto the boss music. That was the concept anyway—I never completed any games that had integrated music.

//=============================================================================
// Uses: Print the notes to a song.
// Date: 2021-04-17
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include "modLoader.h"
#include "modStrings.h"

int main( int argumentCount, char * * arguments )
{
  bool isError = ( argumentCount <= 1 );

  if ( isError )
    fprintf( stderr, "Syntax: %s <MOD file>\n", arguments[ 0 ] );

  char const * fileName = arguments[ 1 ];

  ModFile modFile;
  if ( ! isError )
  {
    isError |= loadMod( fileName, &modFile );
    if ( isError )
      fprintf( stderr, "Unable to load file.\n" );
  }

  if ( ! isError )
  {
    //uint8_t divisionStart = 0;
    uint8_t nextDivisionIndex = 0;
    for ( uint8_t patternIndex = 0; patternIndex < modFile.numberOfPatterns; patternIndex += 1 )
    {
      uint8_t actualPattern = modFile.patternTable[ patternIndex ];
      Pattern * pattern = &modFile.patterns[ actualPattern ];

      uint8_t divisionIndex = nextDivisionIndex;
      nextDivisionIndex = 0;
      bool divisionOver = false;
      while ( ! divisionOver )
      {
        printf( "%3d/%2d: ", patternIndex, divisionIndex );
        for ( uint8_t channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
        {
          Channel * channel = &pattern->channels[ divisionIndex ][ channelIndex ];

          char const * effectName = "";
          char effectParameter[ 3 ] = "  ";
          if ( MOD_EFFECT_TYPE__NONE != channel->effectType )
          {
            effectName = MOD_EFFECT_NAMES[ channel->effectType ];
            snprintf( effectParameter, sizeof( effectParameter )"%02X", channel->effectParameter & 0xFF );
          }

          char const * note = "";
          char sample[ 4 ] = "   ";
          if ( 0 != channel->sample )
          {
            note = getNoteName( channel->period );
            snprintf( sample, sizeof( sample )"%3d", channel->sample );
          }

          printf
          (
            "| %s %3s %-10s %s ",
            sample,
            note,
            effectName,
            effectParameter
          );

          if ( MOD_EFFECT_TYPE__PATTERN_BREAK == channel->effectType )
          {
            nextDivisionIndex  = ( channel->effectParameter >> 4 ) * 10;
            nextDivisionIndex += channel->effectParameter & 0xF;
            divisionOver = true;
          }

          if ( MOD_EFFECT_TYPE__POSITION_JUMP == channel->effectType )
            divisionOver = true;
        }
        printf( "|\n" );

        divisionIndex += 1;
        divisionOver |= ( divisionIndex >= MOD_PATTERN_DIVISIONS );
      }
    }

  }

  int returnResult = 0;
  if ( isError )
    returnResult = -1;

  return returnResult;
}

April 25, 2021

Decoding an Amiga MOD file

The first step in creating a Amiga MOD player is to be able to load a MOD file for playback. The format is not that complex and a good deal of work has been done in this reference document to explain exactly how the files are formatted.

Let’s start with an overview of how MOD files store a song. We’ll break this up into three parts: header, samples data and pattern data.

The header contains basic information about a song. This includes:

  • Title.
  • Sample information.
  • Number of patterns.
  • Restart position.

The title is just that—what the composer calls the song. The sample information is its own structure and contains:

  • Sample name
  • Sample words (size of sample).
  • Fine tune.
  • Volume.
  • Repeat offset.
  • Repeat length.

The original MOD files had 15 possible samples, with latter versions extending this to 31. Which depends on the version of MOD. This can be obtained by reading 4 bytes from file offset 0x438. The most typical value here is “M.K.” meaning 31 instruments, but there are a few of other possibilities. If it isn’t any of the known possibilities, the file contains 15 instruments.

Next we’ll address the samples. This is a set of signed linear pulse-code modulated (LPCM) 8-bit values the represent a sound. Due to how the Amiga’s sound chip Paula handled sound playback, samples always come in pairs. So the size of a sample is in words or two-byte pairs. The sample size is a 16-bit number, so that means a sample can be up to 65,535 words in length, or 131,070 bytes. That is enough for a few seconds of audio.

Samples are played through a channel. Original MOD files had 4 channels making it a 4-voice pol lyphony. Each channel has its own selected sample, period (pitch) and volume. In addition, effects can be applied on a per-channel basis.

The second part of a MOD is defining the note of a song. A complete song comes as a collection of patterns. A pattern consists of 64 divisions for which a note may be played or modified. Each division defines this information for each of the 4 channels. Since patterns may be repeated in a song, there is a pattern table that defines the order in which patterns are played. The pattern table is up to 128 patterns in length, although this is typically truncated by specifying pattern zero for all remaining patterns once the song is over. The original MOD format allowed 63 different patterns to be define, although latter extensions allowed more. Presumably the extension could play up to 128 patterns as it isn’t possible to play more than that due to the limits of the pattern table. In theory, 256 different patterns could be defined of which only 128 are playable.

Since not all patterns are defined, the method used for calculating how many patterns to load is simply to find the largest pattern number in the pattern table. It means it is possible a pattern is define but isn’t used. Some MOD players would tell you the difference between a MOD file’s size and the memory actually used. Sometimes samples were unused but more often it is patterns that were unused. Each pattern is 1024 bytes, so that is 1 KiB of RAM for every pattern that doesn’t have to be loaded.

The last item needed to load a song is the channel data—that is, an individual note. The channel data is a 32-bit value that is broken up into 3 parts: selected sample (8-bits), sample period (pitch, 12-bits), and effect (12-bits). The effects typically have three fields: effect number (4-bits), parameter A (4-bits) and parameter B (4-bits). There are extended effects with a single 4-bit parameter. This is because they are all effect number 14, with the extended effect number being parameter A. That leaves 4-bits for a parameter. So in total, there are 30 effects (15 primary effects + 15 extended effects, one unused extended effect). These are:

  • Arpeggio
  • Slide up
  • Slide down
  • Slide to note
  • Vibrato
  • Slide to note + volume slide
  • Vibrato + volume slide
  • Tremolo
  • Panning (unused in original Amiga format).
  • Sample offset
  • Volume slide
  • Position jump
  • Set volume
  • Pattern break
    • Set filter
    • Fine slide up
    • Fine slide down
    • Glissado on/off
    • Vibrato waveform
    • Finetune
    • Loop pattern
    • Tremolo waveform
    • Retrigger sample
    • Fine volume slide up
    • Fine volume slide down
    • Cut sample
    • Delay sample
    • Delay pattern
    • Invert loop
  • Set speed

There is not a specific “no effect”. Instead, this is assumed by the nature of how arpeggio works. Arpeggio is effect 0, and with the parameters A/B set to 0 this effect does nothing—no effect.

So to break it down, the song consists of: pattern table → pattern → division → channel → channel data (sample/pitch/effect). With all of this information we can load a MOD file.

Structures for loading

//=============================================================================
// Uses: Structures for a MOD file.
// Date: 2021-04-17
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#ifndef MODSTRUCTURES_H
#define MODSTRUCTURES_H

#include <stdint.h>

typedef enum
{
  MOD_FORMAT__SOUNDTRACKER,     // 15 instruments.
  MOD_FORMAT__SOUNDTRACKER_2_4, // 31 instruments.
  MOD_FORMAT__PROTRACKER,       // >64 patterns.
  MOD_FORMAT__STARTREKKER_4,    // 4 channel.
  // $$$FUTURE MOD_FORMAT__STARTREKKER_6,    // 6 channel.
  // $$$FUTURE MOD_FORMAT__STARTREKKER_8,    // 8 channel.

  MOD_FORMATS
} MOD_Format;

// Title of song.
enum { MOD_TITLE_LENGTH = 20 };

// Number of samples.
// $$$FUTURE - Can vary, 15 or 31.
enum { MOD_NUMBER_OF_SAMPLES = 31 };

// Name length of a sample (instrument).
enum { MOD_SAMPLE_NAME_LENGTH = 22 };

// Size of pattern table.
enum { MOD_PATTERN_TABLE_SIZE = 128 };

// Number of notes in each pattern.
enum { MOD_PATTERN_DIVISIONS = 64 };

// Identification of file format.
enum { MOD_FORMAT_BYTES = 4 };

// Location for file identity.
enum { MOD_FORMAT_OFFSET = 0x438 };

// Number of channels.
// $$$FUTURE - Variable.
enum { MOD_CHANNELS = 4 };

// Bytes in a single note.
enum { MOD_NOTE_BYTES = sizeof( uint32_t ) };

// Number of bytes in a raw pattern.
enum { MOD_RAW_PATTERN_SIZE = MOD_CHANNELS * MOD_PATTERN_DIVISIONS * MOD_NOTE_BYTES };

// Root rate for instrument.
enum { MOD_SAMPLE_RATE = 8000 };

// Highest volume on a channel.
enum { MOD_MAX_VOLUME = 64 };

// Special panning value meaning surround-sound.  DMP extension.
enum { MOD_SURROUND_PAN = 164 };

// Number at which the change from ticks/division to beats/minute occurs.
enum { MOD_SPEED_CHANGEOVER = 32 };

// Valid notes.
typedef enum
{
 MOD_NOTE__C_0,
 MOD_NOTE__CS0,
 MOD_NOTE__D_0,
 MOD_NOTE__DS0,
 MOD_NOTE__E_0,
 MOD_NOTE__F_0,
 MOD_NOTE__FS0,
 MOD_NOTE__G_0,
 MOD_NOTE__GS0,
 MOD_NOTE__A_0,
 MOD_NOTE__AS0,
 MOD_NOTE__B_0,
 MOD_NOTE__C_1,
 MOD_NOTE__CS1,
 MOD_NOTE__D_1,
 MOD_NOTE__DS1,
 MOD_NOTE__E_1,
 MOD_NOTE__F_1,
 MOD_NOTE__FS1,
 MOD_NOTE__G_1,
 MOD_NOTE__GS1,
 MOD_NOTE__A_1,
 MOD_NOTE__AS1,
 MOD_NOTE__B_1,
 MOD_NOTE__C_2,
 MOD_NOTE__CS2,
 MOD_NOTE__D_2,
 MOD_NOTE__DS2,
 MOD_NOTE__E_2,
 MOD_NOTE__F_2,
 MOD_NOTE__FS2,
 MOD_NOTE__G_2,
 MOD_NOTE__GS2,
 MOD_NOTE__A_2,
 MOD_NOTE__AS2,
 MOD_NOTE__B_2,
 MOD_NOTE__C_3,
 MOD_NOTE__CS3,
 MOD_NOTE__D_3,
 MOD_NOTE__DS3,
 MOD_NOTE__E_3,
 MOD_NOTE__F_3,
 MOD_NOTE__FS3,
 MOD_NOTE__G_3,
 MOD_NOTE__GS3,
 MOD_NOTE__A_3,
 MOD_NOTE__AS3,
 MOD_NOTE__B_3,
 MOD_NOTE__C_4,
 MOD_NOTE__CS4,
 MOD_NOTE__D_4,
 MOD_NOTE__DS4,
 MOD_NOTE__E_4,
 MOD_NOTE__F_4,
 MOD_NOTE__FS4,
 MOD_NOTE__G_4,
 MOD_NOTE__GS4,
 MOD_NOTE__A_4,
 MOD_NOTE__AS4,
 MOD_NOTE__B_4,

 MOD_NOTES
} MOD_Note;

// A sample is an instrument made of Pulse-Code Modulated (PCM) audio.
typedef struct
{
  char name[ MOD_SAMPLE_NAME_LENGTH ];
  uint16_t sampleWords;
  uint8_t fineTune;
  uint8_t volume;
  uint16_t repeatOffset;
  uint16_t repeatLength;

} Sample;

typedef enum
{
  MOD_EFFECT_TYPE__ARPEGGIO,                // 0
  MOD_EFFECT_TYPE__SLIDE_UP,                // 1
  MOD_EFFECT_TYPE__SLIDE_DOWN,              // 2
  MOD_EFFECT_TYPE__SLIDE_TO_NOTE,           // 3
  MOD_EFFECT_TYPE__VIBRATO,                 // 4
  MOD_EFFECT_TYPE__SLIDE_AND_VOLUME,        // 5
  MOD_EFFECT_TYPE__VIBRATO_AND_VOLUME,      // 6
  MOD_EFFECT_TYPE__TREMOLO,                 // 7
  MOD_EFFECT_TYPE__PAN,                     // 8
  MOD_EFFECT_TYPE__SAMPLE_OFFSET,           // 9
  MOD_EFFECT_TYPE__VOLUME_SLIDE,            // 10
  MOD_EFFECT_TYPE__POSITION_JUMP,           // 11
  MOD_EFFECT_TYPE__SET_VOLUME,              // 12
  MOD_EFFECT_TYPE__PATTERN_BREAK,           // 13
  MOD_EFFECT_TYPE__EXTENDED,                // 14
  MOD_EFFECT_TYPE__SET_SPEED,               // 15

  MOD_EFFECT_TYPE__SET_FILTER,              // 14-0
  MOD_EFFECT_TYPE__FINE_SLIDE_UP,           // 14-1
  MOD_EFFECT_TYPE__FINE_SLIDE_DOWN,         // 14-2
  MOD_EFFECT_TYPE__GLISSANDO,               // 14-3
  MOD_EFFECT_TYPE__VIBRATO_WAVEFORM,        // 14-4
  MOD_EFFECT_TYPE__FINE_TUNE,               // 14-5
  MOD_EFFECT_TYPE__LOOP_PATTERN,            // 14-6
  MOD_EFFECT_TYPE__TREMOLO_WAVEFORM,        // 14-7
  MOD_EFFECT_TYPE__UNUSED,                  // 14-8
  MOD_EFFECT_TYPE__RETRIGGER,               // 14-9
  MOD_EFFECT_TYPE__FINE_VOLUME_SLIDE_UP,    // 14-10
  MOD_EFFECT_TYPE__FINE_VOLUME_SLIDE_DOWN,  // 14-11
  MOD_EFFECT_TYPE__CUT_SAMPLE,              // 14-12
  MOD_EFFECT_TYPE__DELAY_SAMPLE,            // 14-13
  MOD_EFFECT_TYPE__DELAY_PATTERN,           // 14-14
  MOD_EFFECT_TYPE__INVERT_LOOP,             // 14-15

  MOD_EFFECT_TYPE__NONE,                    //

  MOD_EFFECT_TYPES

} EffectType;

typedef struct
{
  uint8_t sample;
  uint16_t period;
  MOD_Note note;
  EffectType effectType;
  uint16_t effectParameter;
} Channel;

typedef struct
{
  Channel channels[ MOD_PATTERN_DIVISIONS ][ MOD_CHANNELS ];
} Pattern;

typedef struct
{
  char title[ MOD_TITLE_LENGTH ];

  Sample samples[ MOD_NUMBER_OF_SAMPLES ];

  uint8_t numberOfPatterns;
  uint8_t usedPatterns;
  uint8_t restartPosition;
  uint8_t patternTable[ MOD_PATTERN_TABLE_SIZE ];
  char    format[ MOD_FORMAT_BYTES ];
  Pattern * patterns;

  uint8_t numberOfSamples;
  int8_t * sampleData[ MOD_NUMBER_OF_SAMPLES ];

} ModFile;


#endif // MODSTRUCTURES_H

Loading

//=============================================================================
// Uses: Load a MOD file.
// Date: 2021-04-17
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "fileUtilities.h"
#include "modLoader.h"

char const * FORMATS[ MOD_FORMATS ] =
{
  NULL,     // MOD_FORMAT__SOUNDTRACKER,
  "M.K.",   // MOD_FORMAT__SOUNDTRACKER_2_4,
  "M!K!",   // MOD_FORMAT__PROTRACKER,
  "FLT4",   // MOD_FORMAT__STARTREKKER_4,
  // $$$FUTURE "FLT6",   // MOD_FORMAT__PROTRACKER_6,
  // $$$FUTURE "FLT8",   // MOD_FORMAT__PROTRACKER_8,
};

typedef struct
{
  uint16_t period;
  MOD_Note note;
} NoteLookup;

static NoteLookup const NOTE_TABLE[ MOD_NOTES ] =
{
  { 1712,  MOD_NOTE__C_0 },
  { 1616,  MOD_NOTE__CS0 },
  { 1525,  MOD_NOTE__D_0 },
  { 1440,  MOD_NOTE__DS0 },
  { 1357,  MOD_NOTE__E_0 },
  { 1281,  MOD_NOTE__F_0 },
  { 1209,  MOD_NOTE__FS0 },
  { 1141,  MOD_NOTE__G_0 },
  { 1077,  MOD_NOTE__GS0 },
  { 1017,  MOD_NOTE__A_0 },
  { 961,   MOD_NOTE__AS0 },
  { 907,   MOD_NOTE__B_0 },
  { 856,   MOD_NOTE__C_1 },
  { 808,   MOD_NOTE__CS1 },
  { 762,   MOD_NOTE__D_1 },
  { 720,   MOD_NOTE__DS1 },
  { 678,   MOD_NOTE__E_1 },
  { 640,   MOD_NOTE__F_1 },
  { 604,   MOD_NOTE__FS1 },
  { 570,   MOD_NOTE__G_1 },
  { 538,   MOD_NOTE__GS1 },
  { 508,   MOD_NOTE__A_1 },
  { 480,   MOD_NOTE__AS1 },
  { 453,   MOD_NOTE__B_1 },
  { 428,   MOD_NOTE__C_2 },
  { 404,   MOD_NOTE__CS2 },
  { 381,   MOD_NOTE__D_2 },
  { 360,   MOD_NOTE__DS2 },
  { 339,   MOD_NOTE__E_2 },
  { 320,   MOD_NOTE__F_2 },
  { 302,   MOD_NOTE__FS2 },
  { 285,   MOD_NOTE__G_2 },
  { 269,   MOD_NOTE__GS2 },
  { 254,   MOD_NOTE__A_2 },
  { 240,   MOD_NOTE__AS2 },
  { 226,   MOD_NOTE__B_2 },
  { 214,   MOD_NOTE__C_3 },
  { 202,   MOD_NOTE__CS3 },
  { 190,   MOD_NOTE__D_3 },
  { 180,   MOD_NOTE__DS3 },
  { 170,   MOD_NOTE__E_3 },
  { 160,   MOD_NOTE__F_3 },
  { 151,   MOD_NOTE__FS3 },
  { 143,   MOD_NOTE__G_3 },
  { 135,   MOD_NOTE__GS3 },
  { 127,   MOD_NOTE__A_3 },
  { 120,   MOD_NOTE__AS3 },
  { 113,   MOD_NOTE__B_3 },
  { 107,   MOD_NOTE__C_4 },
  { 101,   MOD_NOTE__CS4 },
  { 95,    MOD_NOTE__D_4 },
  { 90,    MOD_NOTE__DS4 },
  { 85,    MOD_NOTE__E_4 },
  { 80,    MOD_NOTE__F_4 },
  { 76,    MOD_NOTE__FS4 },
  { 71,    MOD_NOTE__G_4 },
  { 67,    MOD_NOTE__GS4 },
  { 64,    MOD_NOTE__A_4 },
  { 60,    MOD_NOTE__AS4 },
  { 57,    MOD_NOTE__B_4 }
};

//-----------------------------------------------------------------------------
// Uses:
//   Get a period by note.
// Input:
//   note - Note to get.
// Output:
//   Period of this note.
//-----------------------------------------------------------------------------
uint16_t getPeriodByNote( MOD_Note note )
{
  return NOTE_TABLE[ note ].period;
}

//-----------------------------------------------------------------------------
// Uses:
//   Get a note by period.
// Input:
//   period - Period of note.
// Output:
//   Closest note.
// Notes:
//   Use a binary search.  Not all software uses the same note values, so just
//   get as close as we can.
//-----------------------------------------------------------------------------
MOD_Note getNoteByPeriod( uint16_t period )
{
  int left = 0;
  int right = MOD_NOTES - 1;
  int index = left + ( right - left ) / 2;

  while ( ( period != NOTE_TABLE[ index ].period )
       && ( left <= right ) )
  {
    if ( NOTE_TABLE[ index ].period != period )
    {
      if ( NOTE_TABLE[ index ].period > period )
        // Must be after this.
        left = index + 1;
      else
      if ( NOTE_TABLE[ index ].period < period )
        // Must be before this.
        right = index - 1;

      index = left + ( right - left ) / 2;
    }
  }

  return NOTE_TABLE[ index ].note;
}

//-----------------------------------------------------------------------------
// Uses:
//   Load a MOD file.
// Input:
//   fileName - File to load.
//   modFile - Pointer to where loaded data is stored.
// Output:
//   True if there was an error.
//   modFile - Contains resulting loaded data.
//-----------------------------------------------------------------------------
bool loadMod( char const * fileName, ModFile * modFile )
{
  bool isError = false;

  FILE * inputFile = NULL;
  if ( ! isError )
  {
    inputFile = fopen( fileName, "rb" );

    isError = ( NULL == inputFile );
    if ( isError )
      fprintf( stderr, "Unable to open `%s`.\n", fileName );
  }


  // Read format header.
  isError |= ( 0 != fseek( inputFile, MOD_FORMAT_OFFSET, SEEK_SET ) );
  isError |= fileRead( inputFile, modFile->format, MOD_FORMAT_BYTES );
  rewind( inputFile );

  modFile->numberOfSamples = 15;
  //if ( 0 == memcmp( modFile->format, FORMATS[ MOD_FORMAT__SOUNDTRACKER     ], MOD_FORMAT_BYTES ) )
  if ( ( 0 == memcmp( modFile->format, FORMATS[ MOD_FORMAT__SOUNDTRACKER_2_4 ], MOD_FORMAT_BYTES ) )
    || ( 0 == memcmp( modFile->format, FORMATS[ MOD_FORMAT__PROTRACKER       ], MOD_FORMAT_BYTES ) )
    || ( 0 == memcmp( modFile->format, FORMATS[ MOD_FORMAT__STARTREKKER_4    ], MOD_FORMAT_BYTES ) ) )
  {
    modFile->numberOfSamples = 31;
  }

  isError |= fileRead( inputFile, modFile->title, MOD_TITLE_LENGTH );

  for ( uint8_t index = 0; index < modFile->numberOfSamples; index += 1 )
  {
    Sample * sample = &modFile->samples[ index ];
    isError |= fileRead( inputFile, sample->name, MOD_SAMPLE_NAME_LENGTH );
    isError |= fileRead16( inputFile, &sample->sampleWords  );
    isError |= fileRead8 ( inputFile, &sample->fineTune     );
    isError |= fileRead8 ( inputFile, &sample->volume       );
    isError |= fileRead16( inputFile, &sample->repeatOffset );
    isError |= fileRead16( inputFile, &sample->repeatLength );
  }

  isError |= fileRead8 ( inputFile, &modFile->numberOfPatterns );
  isError |= fileRead8 ( inputFile, &modFile->restartPosition );
  isError |= fileRead  ( inputFile, &modFile->patternTable, MOD_PATTERN_TABLE_SIZE );
  if ( modFile->numberOfSamples > 15 )
    isError |= fileRead  ( inputFile,  modFile->format, MOD_FORMAT_BYTES );

  for ( uint8_t patternIndex = 0; patternIndex < MOD_PATTERN_TABLE_SIZE; patternIndex += 1 )
    if ( modFile->patternTable[ patternIndex ] > modFile->usedPatterns )
      modFile->usedPatterns = modFile->patternTable[ patternIndex ];

  modFile->usedPatterns += 1;

  modFile->patterns =
    (Pattern*)malloc( sizeof( Pattern ) * modFile->usedPatterns * MOD_PATTERN_DIVISIONS );

  isError |= ( NULL == modFile->patterns );
  if ( ! isError )
  {
    uint8_t rawPattern[ MOD_RAW_PATTERN_SIZE ];

    for ( uint8_t patternIndex = 0; patternIndex < modFile->usedPatterns; patternIndex += 1 )
    {
      isError |= fileRead( inputFile, rawPattern, MOD_RAW_PATTERN_SIZE );

      uint16_t rawIndex = 0;
      Pattern * pattern = &modFile->patterns[ patternIndex ];
      for ( uint8_t divisionIndex = 0; divisionIndex < MOD_PATTERN_DIVISIONS; divisionIndex += 1 )
      {
        for ( uint8_t channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
        {
          uint32_t rawWord =
              ( (uint32_t)rawPattern[ rawIndex + 0 ] << 24 )
            | ( (uint32_t)rawPattern[ rawIndex + 1 ] << 16 )
            | ( (uint32_t)rawPattern[ rawIndex + 2 ] <<  8 )
            | ( (uint32_t)rawPattern[ rawIndex + 3 ] <<  0 );

          rawIndex += 4;

          Channel * channel = &pattern->channels[ divisionIndex ][ channelIndex ];

          // 7654-3210 7654-3210 7654-3210 7654-3210
          // 3322 2222 2221 1111 1111 1
          // 1098 7654 3210 9876 5432 1098 7654 3210
          // wwww xxxxxxxxxxxxxx yyyy zzzzzzzzzzzzzz
          //      period              effect

          channel->sample = ( ( rawWord >> ( 28 - 4 ) ) & 0xF0 ) + ( ( rawWord >> 12 ) & 0xF );
          channel->period = ( rawWord >> 16 ) & 0xFFF;
          channel->note = getNoteByPeriod( channel->period );

          // The full effect word (12 bit).
          uint16_t effectWord = rawWord & 0xFFF;

          // Effect high, middle and low nibbles.
          uint8_t effectH = ( effectWord >> 8 ) & 0xF;
          uint8_t effectM = ( effectWord >> 4 ) & 0xF;
          uint8_t effectL = ( effectWord >> 0 ) & 0xF;

          // No effect if effect is 0 and has 0 as parameters.
          // This is actually arpeggio with 0 as parameters, which have no
          // effect--but for notation purposes it is nice to have it
          // independently labeled.
          if ( 0 == effectWord )
          {
            channel->effectType = MOD_EFFECT_TYPE__NONE;
          }
          else
          // Extended parameter?
          if ( MOD_EFFECT_TYPE__EXTENDED == effectH )
          {
            // Middle nibble defines which extended effect.
            channel->effectType = effectM + MOD_EFFECT_TYPE__SET_FILTER;

            // Low nibble is the parameter.
            channel->effectParameter = effectL;
          }
          else
          {
            channel->effectType = effectH + MOD_EFFECT_TYPE__ARPEGGIO;
            channel->effectParameter = effectWord & 0xFF;
          }

        }
      }

    }
  }

  // Load samples.
  for ( uint8_t index = 0; index < modFile->numberOfSamples; index += 1 )
  {
    uint16_t sampleSize = modFile->samples[ index ].sampleWords * 2;
    if ( sampleSize > 0 )
    {
      int8_t * sample = (int8_t*)malloc( sampleSize );
      isError |= ( NULL == sample );
      if ( ! isError )
        isError |= fileRead( inputFile, sample, sampleSize );

      if ( ! isError )
        modFile->sampleData[ index ] = sample;
    }
  }

  if ( inputFile )
    fclose( inputFile );

  return isError;
}