Andrew Que Sites list Photos
Projects Contact
Main

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.

April 19, 2021

Adding Arpeggio and Portamento to MOD Player

//=============================================================================
// Uses:
// Date: 2021-04-19
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include <stdlib.h>
#include "modLoader.h"
#include "modStrings.h"
#include "amigaConstants.h"
#include "waveExport.h"

enum { FREQUENCY          = 21276 };
enum { TICKS_PER_DIVISION = 6 };
enum { BEAT_PER_MINUTE    = 125 };

static bool const printNotes = true;

// Number of samples in the longest possible division.
#define LONGEST_DIVISION_SAMPLES  ( ( 60L * FREQUENCY * MOD_PATTERN_DIVISIONS * MOD_SPEED_CHANGEOVER ) \
                                  / ( 24 * ( MOD_SPEED_CHANGEOVER + 1 ) ) )

enum { INDEX_FIXED_SHIFT = 8 };

typedef struct
{
  // Which instrument is sounding. 0xFF for none.
  uint8_t instrument;

  // Period (i.e. note).
  uint16_t period;
  MOD_Note note;

  uint16_t arpeggioA;
  uint16_t arpeggioB;
  uint8_t arpeggioIndex;

  // Volume of channel.
  uint8_t volume;

  // Volume slide (+/- slide per tick).
  int8_t volumeSlide;

  // Pitch slide (+/- periods per tick).
  int16_t pitchSlide;

  // For sliding to a new note, this is the target note.
  uint16_t slideTarget;

  // Current index into sample.
  uint32_t index;

  // Fixed-point increment each output sample added.
  uint32_t indexIncrement;

  // End of sample (used for looping).
  uint32_t sampleEnd;

} SoundChannel;

typedef struct
{
  SoundChannel soundChannels[ MOD_CHANNELS ];

  uint8_t * samples;
  unsigned sampleIndex;
  unsigned numberOfSamples;

  unsigned samplesPerTick;
  unsigned bpm;
  unsigned ticksPerDivision;

} SongPlaypack;

//-----------------------------------------------------------------------------
// Uses:
//   Process a single division tick.
// Input:
//   songPlayback - Song song playback.
//   modFile - Loaded MOD file.
// Output:
//   *songPlayback is modified.
//   songPlayback->samples - New samples are added here.
//   songPlayback->sampleIndex - Index into `samples`.
//-----------------------------------------------------------------------------
static void mixTick( SongPlaypack * songPlayback, ModFile const * modFile )
{
  for ( unsigned count = 0; count < songPlayback->samplesPerTick; count += 1 )
  {
    uint16_t mix = 0;
    for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    {
      SoundChannel * soundChannel = &songPlayback->soundChannels[ channelIndex ];

      int16_t subMix = 0;
      if ( 0xFF != soundChannel->instrument )
      {
        Sample const * sample = &modFile->samples[ soundChannel->instrument ];
        int8_t const * sampleData = modFile->sampleData[ soundChannel->instrument ];

        uint32_t sampleEnd = 2 * soundChannel->sampleEnd;
        uint32_t index = soundChannel->index >> INDEX_FIXED_SHIFT;

        // If this is a repeating sample and we've passed the repeat point...
        if ( ( index >= sampleEnd )
          && ( sample->repeatLength > 1 ) )
        {
          index = 2 * sample->repeatOffset;

          // Save new index.
          soundChannel->index = index << INDEX_FIXED_SHIFT;

          // Change end point.
          soundChannel->sampleEnd = sample->repeatLength + sample->repeatOffset;
          if ( soundChannel->sampleEnd > sample->sampleWords )
            // Invalid configuration--the repeat end is greater than the
            // actual end.  So use the actual end.
            soundChannel->sampleEnd = sample->sampleWords;
        }

        if ( index < sampleEnd )
        {
          subMix  = sampleData[ index ];
          subMix *= soundChannel->volume;
          subMix /= 64;

          soundChannel->index += soundChannel->indexIncrement;
        }

      }

      mix += subMix;
    }

    if ( songPlayback->sampleIndex < songPlayback->numberOfSamples )
      songPlayback->samples[ songPlayback->sampleIndex ] = ( mix / 4 ) + 0x80;

    songPlayback->sampleIndex += 1;
  }
}

//-----------------------------------------------------------------------------
// Uses:
//   Compute new speed settings.
// Input:
//   songPlayback - Instance of `SongPlaypack` to modify.
//   ticksPerDivision - New ticks/division.
//   bpm - New beats/minute.
// Output:
//   Results are stored in:
//     songPlayback->ticksPerDivision
//     songPlayback->bpm
//     songPlayback->samplesPerTick
//-----------------------------------------------------------------------------
static inline void calculationNewSpeed
(
  SongPlaypack * songPlayback,
  unsigned ticksPerDivision,
  unsigned bpm
)
{
  songPlayback->ticksPerDivision = ticksPerDivision;
  songPlayback->bpm              = bpm;
  songPlayback->samplesPerTick   = 60 * FREQUENCY / ( 24 * bpm );
}

//-----------------------------------------------------------------------------
// Uses:
//   Mix down song.
//-----------------------------------------------------------------------------
bool modToWave( char const * const fileName, ModFile const * modFile )
{
  bool isError = false;

  unsigned numberOfSamples = LONGEST_DIVISION_SAMPLES;
  uint8_t * samples = malloc( LONGEST_DIVISION_SAMPLES );

  isError = ( NULL == samples );

  SongPlaypack songPlayback;
  songPlayback.samples = samples;
  songPlayback.numberOfSamples  = numberOfSamples;
  calculationNewSpeed( &songPlayback, TICKS_PER_DIVISION, BEAT_PER_MINUTE );

  for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
  {
    SoundChannel * soundChannel = &songPlayback.soundChannels[ channelIndex ];
    soundChannel->instrument = 0xFF;
    soundChannel->pitchSlide = 0;
    soundChannel->volumeSlide = 0;
    soundChannel->arpeggioA = 0;
    soundChannel->arpeggioB = 0;
  }

  for ( uint8_t patternIndex = 0; patternIndex < modFile->numberOfPatterns; patternIndex += 1 )
  //uint8_t patternIndex = 4;
  {
    songPlayback.sampleIndex = 0;

    uint8_t actualPattern = modFile->patternTable[ patternIndex ];
    Pattern const * pattern = &modFile->patterns[ actualPattern ];

    uint8_t divisionIndex = 0;
    uint8_t nextDivisionIndex = 1;
    while ( divisionIndex < MOD_PATTERN_DIVISIONS )
    {
      if ( printNotes )
        printf( "%2d/%02X: ", patternIndex, divisionIndex );

      // Loop through each channel.
      for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
      //unsigned channelIndex = 3;
      {
        // The active channel.
        Channel const * channel = &pattern->channels[ divisionIndex ][ channelIndex ];
        SoundChannel * soundChannel = &songPlayback.soundChannels[ channelIndex ];

        // If there is an instrument selected...
        if ( channel->period )
        {
          // Set the instrument.  If it is not specified, just use the last one.
          if ( 0 != channel->sample )
            soundChannel->instrument = channel->sample - 1;

          soundChannel->index = 0;

          Sample const * sample = &modFile->samples[ soundChannel->instrument ];
          soundChannel->sampleEnd = sample->sampleWords;
          soundChannel->note = channel->note;

          if ( MOD_EFFECT_TYPE__SLIDE_TO_NOTE == channel->effectType )
          {
            soundChannel->slideTarget = channel->period;

            // Sliding up or down?
            if ( channel->period < soundChannel->period )
              soundChannel->pitchSlide = (int16_t)channel->effectParameter;
            else
              soundChannel->pitchSlide = -(int16_t)channel->effectParameter;
          }
          else
            soundChannel->period = channel->period;

          // Default volume.
          soundChannel->volume = sample->volume;
        }

        soundChannel->arpeggioA = 0;
        soundChannel->arpeggioB = 0;
        soundChannel->arpeggioIndex = 0;
        if ( MOD_EFFECT_TYPE__ARPEGGIO == channel->effectType )
        {
          MOD_Note noteA   = soundChannel->note + ( ( channel->effectParameter >> 4 ) & 0x0F );
          uint16_t periodA = getPeriodByNote( noteA );
          soundChannel->arpeggioA = periodA - soundChannel->period;

          MOD_Note noteB   = soundChannel->note + ( ( channel->effectParameter >> 0 ) & 0x0F );
          uint16_t periodB = getPeriodByNote( noteB );
          soundChannel->arpeggioB = periodB - soundChannel->period;
        }

        if ( MOD_EFFECT_TYPE__SET_SPEED == channel->effectType )
        {
          if ( channel->effectParameter <= MOD_SPEED_CHANGEOVER )
            songPlayback.ticksPerDivision = channel->effectParameter;
          else
            songPlayback.bpm = channel->effectParameter;

          calculationNewSpeed( &songPlayback, songPlayback.ticksPerDivision, songPlayback.bpm );
        }

        // Volume slide.
        soundChannel->volumeSlide = 0;
        if ( MOD_EFFECT_TYPE__VOLUME_SLIDE == channel->effectType )
        {
          int8_t up   = ( channel->effectParameter & 0xF0 ) >> 4;
          int8_t down = ( channel->effectParameter & 0x0F ) >> 0;
          if ( ( 0 != up ) && ( 0 == down ) )
            soundChannel->volumeSlide = up;
          else
          if ( ( 0 == up ) && ( 0 != down ) )
            soundChannel->volumeSlide = -down;
        }

        // Pitch slide.
        if ( MOD_EFFECT_TYPE__SLIDE_UP == channel->effectType )
        {
          soundChannel->pitchSlide = -(int16_t)channel->effectParameter;
          soundChannel->slideTarget = getPeriodByNote( MOD_NOTE__B_3 );
        }
        else
        if ( MOD_EFFECT_TYPE__SLIDE_DOWN == channel->effectType )
        {
          soundChannel->pitchSlide = (int16_t)channel->effectParameter;
          soundChannel->slideTarget = getPeriodByNote( MOD_NOTE__C_1 );
        }
        else
        if ( MOD_EFFECT_TYPE__SLIDE_TO_NOTE != channel->effectType )
          soundChannel->pitchSlide = 0;

        // Volume set.
        if ( MOD_EFFECT_TYPE__SET_VOLUME == channel->effectType )
          soundChannel->volume = channel->effectParameter;

        // Pattern break and position jump.
        if ( ( MOD_EFFECT_TYPE__PATTERN_BREAK == channel->effectType )
          || ( MOD_EFFECT_TYPE__POSITION_JUMP == channel->effectType ) )
        {
          nextDivisionIndex = MOD_PATTERN_DIVISIONS;
        }

        // Print out
        if ( printNotes )
        {
          char const * effectName = "";
          char effectParameter[ 4 ] = "   ";
          if ( MOD_EFFECT_TYPE__NONE != channel->effectType )
          {
            effectName = MOD_EFFECT_NAMES[ channel->effectType ];
            snprintf( effectParameter, sizeof( effectParameter )"%03X", channel->effectParameter & 0xFFF );
          }

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

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

      }

      if ( printNotes )
        printf( "|\n" );

      // For each tick of this division, generate samples.
      for ( uint8_t tickCount = 0; tickCount < songPlayback.ticksPerDivision; tickCount += 1 )
      {
        // Print out
        if ( printNotes )
          printf( "\tt%d ", tickCount );
        for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
        {
          SoundChannel * soundChannel = &songPlayback.soundChannels[ channelIndex ];

          uint16_t period = soundChannel->period;

          if ( 1 == soundChannel->arpeggioIndex )
             period += soundChannel->arpeggioA;
          else
          if ( 2 == soundChannel->arpeggioIndex )
            period += soundChannel->arpeggioB;

          soundChannel->arpeggioIndex += 1;
          if ( soundChannel->arpeggioIndex >= 3 )
            soundChannel->arpeggioIndex = 0;

          // Number of samples/second.
          double sendRate =
            (double)AMIGA_NTSC_CRYSTAL / ( period * AMIGA_PAULA_PERIOD_DIVIDE );

          double sampleRate = sendRate / FREQUENCY;

          soundChannel->indexIncrement = sampleRate * ( 1 << INDEX_FIXED_SHIFT );

          // Print out
          if ( printNotes )
            printf
            (
              "| p%4d v%2d s%-2i %-5u",
              period,
              soundChannel->volume,
              soundChannel->pitchSlide,
              soundChannel->index >> INDEX_FIXED_SHIFT
            );
        }

        if ( printNotes )
          printf( " |\n" );

        mixTick( &songPlayback, modFile );

        for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
        {
          SoundChannel * soundChannel = &songPlayback.soundChannels[ channelIndex ];

          // If sliding and this isn't the last tick of the division.
          // (If the period of the note being played is z, then the final
          // period will be z - (x*16 + y)*(ticks - 1))
          if ( ( 0 != soundChannel->pitchSlide )
            && ( songPlayback.ticksPerDivision > 1 )
            && ( tickCount < ( songPlayback.ticksPerDivision - 1 ) ) )
          {
            soundChannel->period += soundChannel->pitchSlide;

            if ( ( soundChannel->pitchSlide < 0 )
              && ( soundChannel->period < soundChannel->slideTarget ) )
            {
              soundChannel->period = soundChannel->slideTarget;
            }

            if ( ( soundChannel->pitchSlide > 0 )
              && ( soundChannel->period > soundChannel->slideTarget ) )
            {
              soundChannel->period = soundChannel->slideTarget;
            }
          }

          if ( 0 != soundChannel->volumeSlide )
          {
            int8_t newVolume = soundChannel->volume;
            newVolume += soundChannel->volumeSlide;

            if ( newVolume < 0 )
              newVolume = 0;

            if ( newVolume > MOD_MAX_VOLUME )
              newVolume = MOD_MAX_VOLUME;

            soundChannel->volume = newVolume;
          }
        }
      }

      divisionIndex = nextDivisionIndex;
      nextDivisionIndex = divisionIndex + 1;
    }

    if ( 0 == patternIndex )
      // Create new file and write first pattern.
      isError |=
        waveExport( fileName, FREQUENCY, songPlayback.samples, songPlayback.sampleIndex );
    else
      // Append this pattern to file.
      isError |=
        waveExportAppend( fileName, FREQUENCY, songPlayback.samples, songPlayback.sampleIndex );
  }

  return isError;
}

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

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

  char const * modFileName = arguments[ 1 ];
  char const * wavFileName = arguments[ 2 ];

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

  if ( ! isError )
    isError = modToWave( wavFileName, &modFile );

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

  return returnResult;
}

Added two more effects today: arpeggio and portamento (slides). I have arpeggio (effect 0) slide up (effect 1) and slide down (effect 2) tested but not slide to note (effect 3). Arpeggio turned out to be more of a pain than I had anticipated. The parameters are not pitch, but simitones. That meant I had to add a reference to the note being played as well as the note’s period. This allows me to select and lookup the other notes in the simitone.

Slides are fairly simple except for the ticks-1 ending. Not sure why but this means the note doesn’t slide on the last tick of the division. For testing I use a fairly notorious song: CYBER.MOD. There is more than one rendition of this song and it is an adaptation of Jeroen Tel’s theme to the 1987 game Cybernoid: The Fighting Machine. Who composed this MOD is unknown.

There are several problems with this MOD. First, the slides are not slide to note, but just slides with values set so they get close to their desired note. As a result, many (most) of the slides end up out-of-tune. Another problem is that the sample looping is incorrect. Some instruments have loop lengths that extend beyond the last sample.

This song is a version of a C64 track, and a common practice for the SID 6581/8580 sound generated music was to use arpeggio to mimic chords. This technique quickly plays several notes, usually 2 or 3, one after another (an arpeggio), to make a simulated chord. The sound is emblematic to chiptunes. MODs have the effect but it isn’t used very often, mainly because if one wants chord they can simply sample the chord and include it. Nonetheless this song makes heavy use of arpeggio to simulate the C64 sound.

The arpeggio and slides make it a good song for testing because it is easy to hear differences if the playback isn’t working correctly. I have successfully reproduced the song and that should prove slides and arpeggio are functional.

I meant to try another MOD that uses slide, but I cannot load 15 instrument MOD files. So that will have to wait for another day.

Note: Yes, the slides are out-of-tune—that is how the song is written and that is how it is being reproduced.

April 18, 2021

Playing my First MOD With my First MOD Player

Today instead of tending to yard work I decided to dive into the next phase if a new project: an Amiga module player. The first milestone has been reached and I’m impressed by how much I’ve already accomplished.

The journey started on Friday when I used some base reference material to read in a standard 4 channel MOD file. Initially I thought I’d detail all those steps are part of this article but I have decided instead to make a multi-part series on the process of creating my own MOD player. In this article I will focus on what happened during the day that allowed me take a skeleton of a library that was able to load a MOD file, and a very simple WAV file writer to render the first MOD I composed.

I spent a fair amount of time on Saturday trying to work out how the Amiga computer played back notes. The reference document was fairly vague and their numbers slightly inaccurate but we’ll save the details for another article. In the end, I learned that the sample period defines the rate at which new words are sent to the analog to digital converter. This determines the samples’ pitch. That part was easy. What I wasn’t sure about was how to convert these values to the sample frequency I was using. I finished Saturday with some understanding of how the rates worked.

Today it was time to make sound. It made sense to me to render the first MOD I ever wrote: Que’s First. While technically Crazzy was the first MOD I wrote, it was not composed so much as randomly thrown together. Que’s First’s melody was composed on a keyboard before it was put into MOD form. So it is really the first MOD I composed.

I started by ignoring periods all together. Somewhere I had read the the samples that make MOD instruments were sampled at 8000 Hz. So I made a program to extract MOD samples and turn them into WAV files setup with a 8000 Hz playback frequency. This is quick but required one conversion. Amiga samples are two’s complement 8-bit signed integers, and was a WAV sample is an 8-bit unsigned integer. So all the samples need to have 0x80 added to them.

//=============================================================================
// Uses: Export a MOD file's samples to WAV files.
// Date: 2021-04-17
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include "modLoader.h"
#include "waveExport.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 )
  {
    for ( unsigned sampleIndex = 0; sampleIndex < MOD_NUMBER_OF_SAMPLES; sampleIndex += 1 )
    {
      if ( modFile.samples[ sampleIndex ].sampleWords > 0 )
      {
        char fileName[ 14 ];
        snprintf( fileName, sizeof( fileName )"%d.WAV", sampleIndex );

        unsigned sampleSize = modFile.samples[ sampleIndex ].sampleWords * 2;

        for ( unsigned index = 0; index < sampleSize; index += 1 )
          modFile.sampleData[ sampleIndex ][ index ] += 0x80;

        waveExport
        (
          fileName,
          MOD_SAMPLE_RATE,
          (uint8_t*)modFile.sampleData[ sampleIndex ],
          sampleSize
        );
      }
    }
  }

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

  return returnResult;
}

The produced all the samples from my MOD as I expected, and their pitch sounded reasonable. My first pass at playback would simply render the first channel of the pattern. This would work well because for that song, the first channel is the drum beat—a simple kick drum and snare combination. Pitch doesn’t matter too much. This would allow me to get the speed of playback correct.

//=============================================================================
// Uses: Mix the first pattern, channel 0, no pitch, volume, effects, etc.
// Date: 2021-04-18
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include <stdlib.h>
#include "modLoader.h"
#include "modStrings.h"
#include "amigaConstants.h"
#include "waveExport.h"

enum { FREQUENCY          = 8000 };
enum { TICKS_PER_DIVISION = 6 };
enum { BEAT_PER_MINUTE    = 125 };

#define DIVISIONS_PER_MINUTE      (24.0 * BEAT_PER_MINUTE / TICKS_PER_DIVISION)
#define SAMPLES_PER_DIVISION      (60.0 * FREQUENCY / DIVISIONS_PER_MINUTE)
#define SAMPLES_PER_PATTERN       (SAMPLES_PER_DIVISION * MOD_PATTERN_DIVISIONS)

static ModFile modFile;
static uint8_t * samples;

static unsigned sampleIndex = 0;
static unsigned instrumentIndex = 0;

typedef struct
{
  //Sample const * sample;
  int instrument;
  unsigned period;
  unsigned volume;
  unsigned index;

} SoundChannel;

static SoundChannel soundChannels[ MOD_CHANNELS ];

void mixDivision( void )
{
  for ( unsigned count = 0; count < SAMPLES_PER_DIVISION; count += 1 )
  {
    SoundChannel * channel = &soundChannels[ 0 ];

    samples[ sampleIndex ] = 0x80;
    if ( 0xFF != channel->instrument )
    {
      Sample const * sample = &modFile.samples[ channel->instrument ];
      if ( channel->index < ( 2 * sample->sampleWords ) )
      {
        samples[ sampleIndex ] = modFile.sampleData[ channel->instrument ][ channel->index ];

        channel->index += 1;
      }
    }

    instrumentIndex += 1;
    sampleIndex += 1;
  }
}

void play( void )
{
  uint8_t actualPattern = modFile.patternTable[ 0 ];
  Pattern * pattern = &modFile.patterns[ actualPattern ];

  for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    soundChannels[ channelIndex ].instrument = 0xFF;

  for ( uint8_t divisionIndex = 0; divisionIndex < MOD_PATTERN_DIVISIONS; divisionIndex += 1 )
  {
    printf( "Division: %2d - ", divisionIndex );

    Channel * channel = &pattern->channels[ divisionIndex ][ 0 ];
    if ( pattern->channels[ divisionIndex ][ 0 ].sample )
    {
      soundChannels[ 0 ].instrument = channel->sample - 2;
      soundChannels[ 0 ].period = channel->period;
      soundChannels[ 0 ].index = 0;
      printf( " %2d %5d %s", channel->sample, channel->period, modFile.samples[ channel->sample - 2 ].name );
    }
    else
      printf( " -- " );

    printf( "\n" );

    mixDivision();
  }

}

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

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

  char const * modFileName = arguments[ 1 ];
  char const * wavFileName = arguments[ 2 ];

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

  if ( ! isError )
  {
    samples = (uint8_t*)malloc( SAMPLES_PER_PATTERN );
    isError = ( NULL == samples );
  }

  if ( ! isError )
  {
    play();
    isError = waveExport( wavFileName, FREQUENCY, samples, SAMPLES_PER_PATTERN );
  }

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

  return returnResult;
}

This produced a WAV file that had my bass/snare beat. A good start. The next step was to render all 4 channels. The song begins with a measure of just the beat. There is the kick/snare on the first track, and the second channel which has a rattle shake (like a Maraca). The volume of the shake alternates between full and half which loosely mimics the beads in the rattle sounding at different volumes depending on the side they hit.

//=============================================================================
// Uses: Mixdown of first pattern all 4 channels, no effects, no pitch.
// Date: 2021-04-18
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include <stdlib.h>
#include "modLoader.h"
#include "modStrings.h"
#include "amigaConstants.h"
#include "waveExport.h"

enum { FREQUENCY          = 8000 };
enum { TICKS_PER_DIVISION = 6 };
enum { BEAT_PER_MINUTE    = 125 };

#define DIVISIONS_PER_MINUTE      (24.0 * BEAT_PER_MINUTE / TICKS_PER_DIVISION)
#define SAMPLES_PER_DIVISION      (60.0 * FREQUENCY / DIVISIONS_PER_MINUTE)
#define SAMPLES_PER_PATTERN       (SAMPLES_PER_DIVISION * MOD_PATTERN_DIVISIONS)

static ModFile modFile;
static uint8_t * samples;

static unsigned sampleIndex = 0;

typedef struct
{
  uint8_t instrument;
  unsigned period;
  unsigned volume;
  unsigned index;

} SoundChannel;

static SoundChannel soundChannels[ MOD_CHANNELS ];

void mixDivision( void )
{
  for ( unsigned count = 0; count < SAMPLES_PER_DIVISION; count += 1 )
  {
    uint16_t mix = 0;
    for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    {
      SoundChannel * soundChannel = &soundChannels[ channelIndex ];

      int16_t subMix = 0;
      if ( 0xFF != soundChannel->instrument )
      {
        Sample const * sample = &modFile.samples[ soundChannel->instrument ];
        if ( soundChannel->index < ( 2 * sample->sampleWords ) )
        {
          subMix  = modFile.sampleData[ soundChannel->instrument ][ soundChannel->index ];
          subMix *= soundChannel->volume;
          subMix /= 64;

          soundChannel->index += 1;
        }
      }

      mix += subMix;
    }

    samples[ sampleIndex ] = ( mix / 4 ) + 0x80;

    sampleIndex += 1;
  }
}

void play( void )
{
  uint8_t actualPattern = modFile.patternTable[ 0 ];
  Pattern * pattern = &modFile.patterns[ actualPattern ];

  for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    soundChannels[ channelIndex ].instrument = 0xFF;

  for ( uint8_t divisionIndex = 0; divisionIndex < MOD_PATTERN_DIVISIONS; divisionIndex += 1 )
  {
    printf( "Division: %2d - ", divisionIndex );

    for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    {
      Channel * channel = &pattern->channels[ divisionIndex ][ channelIndex ];
      if ( channel->sample )
      {
        soundChannels[ channelIndex ].instrument = channel->sample - 1;
        soundChannels[ channelIndex ].period = channel->period;
        soundChannels[ channelIndex ].index = 0;

        if ( MOD_EFFECT_TYPE__SET_VOLUME == channel->effectType )
          soundChannels[ channelIndex ].volume = channel->effectParameter;
        else
          soundChannels[ channelIndex ].volume = 64;

        printf( " %2d %5d %2d ", channel->sample, channel->period, channel->effectType );
      }
      else
        printf( " -- ----- -- " );
    }

    printf( "\n" );

    mixDivision();
  }

}

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

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

  char const * modFileName = arguments[ 1 ];
  char const * wavFileName = arguments[ 2 ];

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

  if ( ! isError )
  {
    samples = (uint8_t*)malloc( SAMPLES_PER_PATTERN );
    isError = ( NULL == samples );
  }

  if ( ! isError )
  {
    play();
    isError = waveExport( wavFileName, FREQUENCY, samples, SAMPLES_PER_PATTERN );
  }

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

  return returnResult;
}

This code produced the entire beat which is looped throughout the song. Now it was time for pitch. My work yesterday gave me an equation to convert the note’s period value to the number of samples per second sent to the Digital-to-Analog Converter (DAC). I had a fixed number of samples per second sent to the DAC, so what I needed to calculate was which sample would be getting sent to the DAC at a moment in time. I plan to write in much more detail about how MOD timing works, but for now just understand that songs are broken up in to patterns which consist of 64 divisions in which a note can be played. The speed at which divisions are played is based on the tempo which defaults to 125 beats/minute. There are 4 divisions in a beat. A division is further divided into ticks but other than knowing that the speed calculations assume 6 ticks/division, ticks are not yet used elsewhere.

So I added a function to mix a single division worth of samples. This function has a fixed-point index used to figure out where in the channel’s instrument sample the next output sample comes from. The is some fractional number based on the note’s period. We only use the whole number for the index, but keep the fractional part so it can properly accumulate as playback continues.

I needed to add the calculation to compute the note’s playback increment rate. This is how many counts (including fractional) the instrument sample index changes for each output sample of the mix. Just simple scaling math here. To make it easy on myself I used floating-point for doing the calculation. There are no speed concerns and I was just trying to move quickly so I didn’t feel bad about this.

//=============================================================================
// Uses: Mixdown first pattern with correct pitch.
// Date: 2021-04-18
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include <stdlib.h>
#include "modLoader.h"
#include "modStrings.h"
#include "amigaConstants.h"
#include "waveExport.h"

enum { FREQUENCY          = 44100 };
enum { TICKS_PER_DIVISION = 6 };
enum { BEAT_PER_MINUTE    = 125 };

#define DIVISIONS_PER_MINUTE      (24.0 * BEAT_PER_MINUTE / TICKS_PER_DIVISION)
#define SAMPLES_PER_DIVISION      (60.0 * FREQUENCY / DIVISIONS_PER_MINUTE)
#define SAMPLES_PER_PATTERN       (SAMPLES_PER_DIVISION * MOD_PATTERN_DIVISIONS)

static ModFile modFile;
static uint8_t * samples;

static unsigned sampleIndex = 0;

enum { INDEX_FIXED_SHIFT = 16 };

typedef struct
{
  uint8_t  instrument;
  uint16_t period;
  uint8_t  volume;
  uint32_t index;
  uint32_t indexIncrement;

} SoundChannel;

static SoundChannel soundChannels[ MOD_CHANNELS ];

void mixDivision( void )
{
  for ( unsigned count = 0; count < SAMPLES_PER_DIVISION; count += 1 )
  {
    uint16_t mix = 0;
    for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    {
      SoundChannel * soundChannel = &soundChannels[ channelIndex ];

      int16_t subMix = 0;
      if ( 0xFF != soundChannel->instrument )
      {
        Sample const * sample = &modFile.samples[ soundChannel->instrument ];
        int8_t const * sampleData = modFile.sampleData[ soundChannel->instrument ];
        uint16_t index = soundChannel->index >> INDEX_FIXED_SHIFT;
        if ( index < ( 2 * sample->sampleWords ) )
        {
          subMix  = sampleData[ index ];
          subMix *= soundChannel->volume;
          subMix /= 64;

          soundChannel->index += soundChannel->indexIncrement;
        }
      }

      mix += subMix;
    }

    samples[ sampleIndex ] = ( mix / 4 ) + 0x80;

    sampleIndex += 1;
  }
}

void play( void )
{
  uint8_t actualPattern = modFile.patternTable[ 0 ];
  Pattern * pattern = &modFile.patterns[ actualPattern ];

  for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    soundChannels[ channelIndex ].instrument = 0xFF;

  for ( uint8_t divisionIndex = 0; divisionIndex < MOD_PATTERN_DIVISIONS; divisionIndex += 1 )
  {
    printf( "Division: %2d - ", divisionIndex );

    for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    {
      Channel * channel = &pattern->channels[ divisionIndex ][ channelIndex ];
      if ( channel->sample )
      {
        soundChannels[ channelIndex ].instrument = channel->sample - 1;

        soundChannels[ channelIndex ].index = 0;

        // Number of samples/second.
        double sendRate =
          (double)AMIGA_NTSC_CRYSTAL / ( channel->period * AMIGA_PAULA_PERIOD_DIVIDE );

        double sampleRate = sendRate / FREQUENCY;

        soundChannels[ channelIndex ].indexIncrement = sampleRate * ( 1 << INDEX_FIXED_SHIFT );
        //soundChannels[ channelIndex ].indexIncrement = 1 << INDEX_FIXED_SHIFT;



        if ( MOD_EFFECT_TYPE__SET_VOLUME == channel->effectType )
          soundChannels[ channelIndex ].volume = channel->effectParameter;
        else
          soundChannels[ channelIndex ].volume = 64;

        printf( " %2d %5d %2d %7.3f ", channel->sample, channel->period, channel->effectType, sendRate );
      }
      else
        printf( " -- ----- -- ------- " );
    }

    printf( "\n" );

    mixDivision();
  }

}

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

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

  char const * modFileName = arguments[ 1 ];
  char const * wavFileName = arguments[ 2 ];

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

  if ( ! isError )
  {
    samples = (uint8_t*)malloc( SAMPLES_PER_PATTERN );
    isError = ( NULL == samples );
  }

  if ( ! isError )
  {
    play();
    isError = waveExport( wavFileName, FREQUENCY, samples, SAMPLES_PER_PATTERN );
  }

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

  return returnResult;
}

The results: I had a full playback of my first module that was mostly accurate. For my next iteration I addressed two issues: the pattern break effect and volume slides. In order to do this I needed to address ticks. As briefly stated, each division is further broken into a number of ticks. Effects are applied on the tick level. For volume slide, the amount the volume changes is applied each tick.

Although I didn’t need it for this song, I also added instrument loops. While rendering my own song, I was also rendering a classic: Bjorn Lynne’s 12th Warrior. I wasn’t worried about getting everything correct—just pieces—and did get instrument loops working.

//=============================================================================
// Uses: Mixdown first pattern with correct pitch.
// Date: 2021-04-18
// Author: Andrew Que <https://www.DrQue.net/>
//=============================================================================
#include <stdio.h>
#include <stdlib.h>
#include "modLoader.h"
#include "modStrings.h"
#include "amigaConstants.h"
#include "waveExport.h"

enum { FREQUENCY          = 8000 };
enum { TICKS_PER_DIVISION = 6 };
enum { BEAT_PER_MINUTE    = 125 };

#define DIVISIONS_PER_MINUTE      (24 * BEAT_PER_MINUTE / TICKS_PER_DIVISION)
#define SAMPLES_PER_DIVISION      (60 * FREQUENCY / DIVISIONS_PER_MINUTE)
#define SAMPLES_PER_TICK          (SAMPLES_PER_DIVISION / TICKS_PER_DIVISION)
#define SAMPLES_PER_PATTERN       (SAMPLES_PER_DIVISION * MOD_PATTERN_DIVISIONS)

static uint8_t * samples;

static unsigned sampleIndex = 0;

enum { INDEX_FIXED_SHIFT = 16 };

typedef struct
{
  uint8_t  instrument;
  uint16_t period;
  uint8_t  volume;
  int8_t volumeSlide;
  uint32_t index;
  uint32_t indexIncrement;
  uint32_t sampleLength;

} SoundChannel;

static SoundChannel soundChannels[ MOD_CHANNELS ];

void mixTick( ModFile const * modFile )
{
  for ( unsigned count = 0; count < SAMPLES_PER_TICK; count += 1 )
  {
    uint16_t mix = 0;
    for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    {
      SoundChannel * soundChannel = &soundChannels[ channelIndex ];

      int16_t subMix = 0;
      if ( 0xFF != soundChannel->instrument )
      {
        Sample const * sample = &modFile->samples[ soundChannel->instrument ];
        int8_t const * sampleData = modFile->sampleData[ soundChannel->instrument ];

        uint16_t sampleLength = 2 * sample->sampleWords;
        uint16_t index = soundChannel->index >> INDEX_FIXED_SHIFT;

        if ( ( index > sampleLength )
          && ( sample->repeatLength > 0 ) )
        {
          uint16_t offset = 2 * sample->repeatOffset;
          uint16_t length = 2 * sample->repeatLength;
          index = ( index - sampleLength ) + offset;// - offset - length;
          soundChannel->index = index / 2;
        }

        if ( index < sampleLength )
        {
          subMix  = sampleData[ index ];
          subMix *= soundChannel->volume;
          subMix /= 64;

          soundChannel->index += soundChannel->indexIncrement;
        }

      }

      mix += subMix;
    }

    samples[ sampleIndex ] = ( mix / 4 ) + 0x80;

    sampleIndex += 1;
  }
}

void play( ModFile const * modFile )
{
  for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
    soundChannels[ channelIndex ].instrument = 0xFF;

  for ( uint8_t patternIndex = 0; patternIndex < modFile->numberOfPatterns; patternIndex += 1 )
  {
    uint8_t actualPattern = modFile->patternTable[ patternIndex ];
    Pattern const * pattern = &modFile->patterns[ actualPattern ];

    uint8_t divisionIndex = 0;
    uint8_t nextDivisionIndex = 1;
    while ( divisionIndex < MOD_PATTERN_DIVISIONS )
    {
      printf( "%3d/%2d: ", patternIndex, divisionIndex );

      // Loop through each channel.
      for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
      {
        // The active channel.
        Channel const * channel = &pattern->channels[ divisionIndex ][ channelIndex ];

        // If there is an instrument selected...
        if ( channel->sample )
        {
          soundChannels[ channelIndex ].instrument = channel->sample - 1;
          soundChannels[ channelIndex ].index = 0;

          // Number of samples/second.
          double sendRate =
            (double)AMIGA_NTSC_CRYSTAL / ( channel->period * AMIGA_PAULA_PERIOD_DIVIDE );

          double sampleRate = sendRate / FREQUENCY;

          soundChannels[ channelIndex ].indexIncrement = sampleRate * ( 1 << INDEX_FIXED_SHIFT );

          // Default volume.
          soundChannels[ channelIndex ].volume = 64;
        }

        soundChannels[ channelIndex ].volumeSlide = 0;
        if ( MOD_EFFECT_TYPE__VOLUME_SLIDE == channel->effectType )
        {
          int8_t up   = ( channel->effectParameter & 0xF0 ) >> 4;
          int8_t down = ( channel->effectParameter & 0x0F ) >> 0;
          if ( ( 0 != up ) && ( 0 == down ) )
            soundChannels[ channelIndex ].volumeSlide = up;
          else
          if ( ( 0 == up ) && ( 0 != down ) )
            soundChannels[ channelIndex ].volumeSlide = -down;
        }

        if ( MOD_EFFECT_TYPE__SET_VOLUME == channel->effectType )
          soundChannels[ channelIndex ].volume = channel->effectParameter;


        if ( ( MOD_EFFECT_TYPE__PATTERN_BREAK == channel->effectType )
          || ( MOD_EFFECT_TYPE__POSITION_JUMP == channel->effectType ) )
        {
          nextDivisionIndex = MOD_PATTERN_DIVISIONS;
        }

        //------------
        // Print out
        //------------

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

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

        printf
        (
          "| %s %3s %2d %-10s %s %3i ",
          sample,
          note,
          channel->effectType,
          effectName,
          effectParameter,
          soundChannels[ channelIndex ].volumeSlide
        );

        //------------

      }

      for ( uint8_t tickCount = 0; tickCount < TICKS_PER_DIVISION; tickCount += 1 )
      {
        mixTick( modFile );

        for ( unsigned channelIndex = 0; channelIndex < MOD_CHANNELS; channelIndex += 1 )
        {
          SoundChannel * soundChannel = &soundChannels[ channelIndex ];
          if ( 0 != soundChannels[ channelIndex ].volumeSlide )
          {
            int8_t newVolume = soundChannel->volume;
            newVolume += soundChannels[ channelIndex ].volumeSlide;

            if ( newVolume < 0 )
              newVolume = 0;

            if ( newVolume > MOD_MAX_VOLUME )
              newVolume = MOD_MAX_VOLUME;

            //printf( "\t%d %i %i %i\n", channelIndex, soundChannels[ channelIndex ].volumeSlide, soundChannel->volume, newVolume );

            soundChannel->volume = newVolume;
          }
        }
      }

      divisionIndex = nextDivisionIndex;
      nextDivisionIndex = divisionIndex + 1;

      printf( "\n" );

    }
  }
}

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

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

  char const * modFileName = arguments[ 1 ];
  char const * wavFileName = arguments[ 2 ];

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

  printf
  (
    "DIVISIONS_PER_MINUTE..: %d\n"
    "SAMPLES_PER_DIVISION..: %d\n"
    "SAMPLES_PER_TICK......: %d\n"
    "SAMPLES_PER_PATTERN...: %d\n",
    DIVISIONS_PER_MINUTE,
    SAMPLES_PER_DIVISION,
    SAMPLES_PER_TICK,
    SAMPLES_PER_PATTERN
  );

  if ( ! isError )
  {
    samples = (uint8_t*)malloc( SAMPLES_PER_PATTERN * modFile.numberOfPatterns );
    isError = ( NULL == samples );
  }

  if ( ! isError )
  {
    play( &modFile );
    isError = waveExport( wavFileName, FREQUENCY, samples, sampleIndex );
  }

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

  return returnResult;
}

That was it. With the volume slide functional I was able to fully render my very first Amiga module. For the first time I am releasing this MOD and my first rendering of it. Be aware, I was 13 or 14 years old when I composed this song and was no (and am still not a) musical prodigy.

The song itself was composed around 1991 or 1992 on a Yamaha Protasound PSS-140 keyboard acquired from a garage sale, and I tracked this MOD sometime in between mid and late 1993. 28 years latter, I am able to render it into a playable waveform using only my own software.

All the code posted here was written in a full day of work, based on code written over bits of the prior 4 days. I typically don’t release uncleaned code, but this is kind of a unique project in its ability to show developing code.

April 11, 2021

Fully Static Iterative AVL Tree with No Stack

After implementing an iterative AVL tree using a stack, I immediately asked myself it were possible to to eliminate the stack.  For those who found this page first, this article is part of my series on command handler lookups.  An AVL tree is probably not the best solution for a command handler, but is being explored to demonstrate some basics on its implementation.

The quick answer to if the stack can be eliminated from the iterative AVL tree insert function is yes, but at the cost of additional memory needed for each node. The stack is needed so the balance process can traverse back up the tree from the node just inserted to the trunk. However, if each node knows its parent then the tree can be traversed in either direction. This is basically makes the tree—alright a kind of single linked-list—a double linked list.

The modifications needed for implementation all have to do with the parent node. For every node there is a pointer to the parent. The first node of the tree has no parent and is the only node to have a parent of NULL. Unlike a conventional double linked list, the parent node isn’t the only information we need to concern ourselves with. The parent node has two children—left and right—and during rebalance rotations the child needs to know which of those children it is.  This should become more evident as we explore the implementation.

The node insert has three parts.  The first part searches for the location the new needs to be placed.  This happens the same way the search for a specific key happens.  Start at the trunk and pick the left or right branch based based on a comparison of the new node's key value.  When the next node to search is NULL, meaning the end of the branch has been reached, the insert location has been found.  The search for the insert location only has one change: we remember the parent node.

while ( ( NULL != *nodePointer )
     && ( ! isError ) )
{
  // Alias pointer (make things easier to read).
  AVL_Node * node = *nodePointer;

  parent = node;

  // Compare key of this node to the one being inserted.
  int compare = strcmp( key, node->key );

  // Check for duplicate key which is not allowed.
  if ( 0 == compare )
    isError = true;
  else
  // Select the right or left node depending on compare results.
  if ( compare < 0 )
    nodePointer = &node->right;
  else
    nodePointer = &node->left;
}

Now the new node insertion after finding the insert location:

// Setup new node.
newNode->key    = key;
newNode->value  = value;
newNode->left   = NULL;
newNode->right  = NULL;
newNode->height = 1;
newNode->parent = parent;

// Add node to tree.
*nodePointer = newNode;

We just have to set the parent in the new node. Otherwise, the first part of the insert remains the same. From this point forward the changes are more complex and much easier to get wrong.

AVL_Node * node = parent;
while ( ( ! isError )
     && ( NULL != node ) )

Notice we start the balance process with the parent node. There isn’t anything wrong with starting at the new node except this node is always starts balanced as it has no children. The loop will run until there are no parent nodes.  Now to the outer part of the loop itself.

// Remember the parent node.
AVL_Node * parent = node->parent;

// Figure out which parent link points to the active node.
AVL_Node ** parentLink;
if ( NULL == parent )
  parentLink = tree;
else
if ( node == parent->left )
  parentLink = &parent->left;
else
if ( node == parent->right )
  parentLink = &parent->right;
else
  // Tree is broken.
  isError = true;

...

// Get previous node.
node = node->parent;

First we need to remember the parent node for latter. Then we need to figure out which of the parent’s children point to this node. This is important because if a rotation is needed we are going to reassign children. Now keep in mind we want a link to the pointer to the current node—a double pointer. There are three possibilities, left, right and trunk. Left and right is simple: if they point to our node, they are the pointer we are looking for. However, if our parent is NULL it means this is the trunk of the tree. In that case, we need modify the pointer to the first node—the trunk of the tree itself. The last condition is something that should never happen—the assigned parent does not have this child node. In that case, something went wrong and the tree is dysfunctional.

Now onto the rotations. We will just explore one rotation but the same rules apply to the other.

static inline AVL_Node * rightRotate( AVL_Node * y )
{
  AVL_Node * x    = y->left;
  AVL_Node * hold = x->right;

  // Perform rotation.
  x->right = y;
  y->left  = hold;

  // Reassign parents.
  y->parent = x;
  if ( NULL != hold )
    hold->parent = y;

  // Update heights.
  y->height = max( height( y->left ), height( y->right ) ) + 1;
  x->height = max( height( x->left ), height( x->right ) ) + 1;

  // Return new root.
  return x;
}

The change here is is simply updating the parent nodes. It is simply the reverse of the lines just above it. The right child of x is assigned to y, thus the parent of y parent is x. We do have to check the hold value before setting the parent because it may NULL meaning it isn’t yet assigned.

This will take care of the right/left rotation basics, but not the entire rotation process. Each rotation returns a node which is further modified.

// Left imbalance?
if ( balance > 1 )
{
  int compare = strcmp( key, node->left->key );

  // Left/left
  if ( compare > 0 )
    *parentLink = rightRotate( node );
  else
  // Left/right
  {
    node->left = leftRotate( node->left );
    node->left->parent = node;
    *parentLink = rightRotate( node );
  }

  // Update parent node.
  (*parentLink)->parent = parent;
}
else
// Right imbalance?
if ( balance < -1 )
{
  int compare = strcmp( key, node->right->key );

  // Right/right
  if ( compare < 0 )
    *parentLink = leftRotate( node );
  else
  // Right/left
  {
    node->right = rightRotate( node->right );
    node->right->parent = node;
    *parentLink = leftRotate( node );
  }

  // Update parent node.
  (*parentLink)->parent = parent;
}

Here we see parentLink being used during a rotation. Remember that parent link points to either parent->left or parent->right. After the rotation sets the parent link the last step that needs to be done is updating the parent link of the child that was just modified.

That’s it, but each of those steps are important. Missing one can easily corrupt the tree.  Below is a link to a fully functional demonstration using an AVL tree as a lookup table.