Sound on the Gameboy Advance
Day 5


Ok time to look at the list. Sound mixer, check, MOD converter, check, music player...nope. It's certainly been more than a day since day 4 of the series, but now with the completion of my first professional project, I actually have some free time to spend writing again. Today we'll be covering the first half of writing a music player, just getting the framework set up, and the ability to play notes. No effects just yet, because those are what make it confusing.

Notes: I found an error in Day 4's output section, specifying the sample index in the SAMPLE_HEADER tables in ROM, when the sruct was defined to use an s8* to the sample data. It could be done either way, storing the pointer or looking up the index in dSmpDataTable at runtime, but since the SAMPLE_HEADER struct ends on a 4 byte boundary before the sample data member, it wouldn't save any memory to use a u16 for the data index, and it's easier to use a straight pointer anyway, so I did. That also rendered the dSmpDataTable completely useless, so I removed it as well.

Redefined MOD_NO_SAMPLE in the pattern data to be 31 instead of 0. The MOD format uses 0, but from a programming standpoint, it's easier if we let 0 be a valid sample, so we don't have to subtract 1 from the sample number when looking it up in the MOD's SAMPLE_HEADER table. So, the converter now checks if it's 0, and if so sets it to 31, or if it's not 0, subtracts 1 from it.

Changed extern dSmpData declarations to const s8[] instead of const s8*, because the compiler doesn't like initializing the const SAMPLE_HEADER's with pointers.

Fixed a bug in the converter, it was trying to create OutputFolder\SndData.c, .h and .s, even though it switches the working directory to the output folder to begin with. So, it was trying to create them in OutputFolder\OutputFolder\SndData.*, and since that folder doesn't exist it would die. And a bloody death too, because I also neglected to check if those output files were actually opened successfully before using them... Bad Deku, bad!

Modified the note frequency table generator to chop any frequencies above 65535Hz down to that, so the frequency table can be 16-bit instead of 32, saving a bit of ROM space. Only a couple of notes made it above 65535, and even those were only on the highest finetune settings, which will probably never be used anyways.

Added a better explanation of the timing code since it's pretty important, and separated out the timing and framework sections of this tutorial to organize it a bit better (really just added the framework separator).

The example project includes all of the files that have changed since day 4. If you want to actually build the whole project, you can overlay it onto Example 2, and it should have everything it needs (plus a few files that aren't used anymore, but won't cause any problems).
The MOD in the Data folder is just one that I found on the internet and chose for testing since it's small and uses quite a few effects. The other 3 files there are generated by the converter, when you run it on that very folder.

1. Timing issues
2. Player framework
3. Playing the song

Example project (235KB)



1. Timing issues

As explained in
Day 2, MOD timing is based on the speed and tempo. Tempo is how often to call the MOD update function, and speed is how many updates to wait between actually advancing a row and playing new notes. So, the first thing we want to do is create a MOD update function, and call it every so often based on tempo. That would all be fine and dandy, except our mixer is hardcoded to run at 60Hz! What ever will we do? There are three good solutions to this.

#1 is to modify the mixer to be able to mix any number of samples, and set up a timer interrupt to run at the MOD tempo, updating the song and then mixing enough samples to last until the next MOD tick. Very accurate timing, but troublesome because mixing takes a lot of CPU time, and you never know when the timer interrupt is going to fire off. It could be just before VBlank, blocking important processing until the next frame has started drawing, or it could block several lines of HBlank interrupt. The only solution to this is to write an interrupt handler that can handle nested interrupts (interrupts occurring during other interrupts), and even then the timer could fire off in the middle of your VBlank processing, so you'd have to add in some priority settings to specify that the timer should yield to any other interrupts. If you want to know how much work this is, check out this post on GBADev. Very cool and all, but there are much easier ways.

#2 is to "resample" the MOD to run at 60Hz, occasionally doing 2 updates in one frame if running faster than 60Hz, or skipping updates if running slower. This is easy to do and solves the interrupt blocking problem, but it's pretty noticable when a note is delayed a little too long, or happens a little too soon. The next step up from this is to update the MOD, mix half a frame's worth of samples, update the MOD again, and mix the other half. Twice as accurate, and still easy to do.

#3 is taking #2 another step further, mixing exactly as many samples as you need between MOD ticks, updating MOD, and then mixing that many samples again, until you've done the full frame's worth. Because we're mixing to a buffer anyway, there's no reason that we have to go through that whole timer ordeal to update at the same rate as the song. All you really need to know is how many samples there are between each MOD tick, and that's a piece of cake:

samplesPerTick = mixFreq / MODFreq
Where MODFreq is the tempo converted to Hz, as descibed in Day 3 (Hz = tempo*2/5), and mixFreq is the master fequency, 18157Hz. We still need to modify the mixer to mix any number of samples at a time though, but that's not too much trouble. Another thing is that we'll almost always finish mixing our 304 samples for the frame when we still have quite a few left until the next MOD tick. Just store how many were left and mix those next frame before calling the MOD update. THEN wait a full tick's worth again. The full sound update will look something like this:

void SndUpdate()
{
   s32 samplesLeft = 304;

   while(samplesLeft > 0)
   {
         // Check if the song needs updated
      if(sndVars.samplesUntilMODTick == 0)
      {
            // Update the song and set the number of samples to mix before the next update
         MODUpdate();
         sndVars.samplesUntilMODTick = sndVars.samplesPerMODTick;
      }

         // Figure out if this is the last batch of samples for this frame
      if(sndVars.samplesUntilMODTick < samplesLeft)
      {
            // Song will need updated before we're out of samples, so mix up to the song tick
         SndMix(sndVars.samplesUntilMODTick);
            // Subtract the number we just mixed
         samplesLeft -= sndVars.samplesUntilMODTick;

            // No more left, so song will get updated next time through the loop
         sndVars.samplesUntilMODTick = 0;
      }
      else
      {
            // Not enough samples left to make it to another song tick
            // Just mix what's left and exit
         SndMix(samplesLeft);
            // This is how many samples will get mixed first thing next frame
            // before updating the song
         sndVars.samplesUntilMODTick -= samplesLeft;
            // Mixed the last of the 304 samples, this will break from the while loop
         samplesLeft = 0;
      }
   }
}
Now we have a very accurate way of updating the MOD and mixing all based on VBlank. Let's go over it one step at a time.

First, set the total samples needing mixed, which is one frame's worth just like always, and loop until they've all been mixed.
Inside the loop, the first thing to do is check if it's time to update the MOD. Usually we'll be mixing a few samples before updating it though.
If it does get updated, reset the samples needed before the next update. For a MOD at the default tempo of 125, you get Hz = 125*2/5 = 50Hz, and samplesPerMODTick = 18157Hz / 50Hz, so about 363. This means that in what appears to the user to be a continuous stream of audio playing on their GBA, the MOD will be updated every 363 samples. They have no idea that a buffer is being swapped out at all, so they don't care if the MOD updates don't match up nicely with the buffer length, they just want 363 samples to get played between each one, and that's exactly what we're aiming to do.

Then you check where the next stop is, either a MOD update, or the end of the buffer. The first case here is that the MOD will need updated again before the buffer is full. In this case, we mix however many samples were left until the tick, and then subtract those from the total left needing to be mixed and from the total left until the tick (actually set it to 0, since subtracting it from itself would be silly).
Then go through the loop again, updating the MOD first thing, then check if there will be yet another MOD tick before the buffer is full, and proceed accordingly.

In the second case, when the buffer WILL be full before the MOD tick happens, you have to give a little more thought as to what is actually happening, and what will happen next frame.
First of all, we know that there will be nothing stopping us from mixing until we reach the end of the buffer, so just mix as many samples as were left. Then, instead of declaring it time for the MOD tick, we subtract however many samples we just mixed from the number left to wait.
Here's an example situation. Say we just did a MOD tick and reset the wait samples to 363, but only had 100 left in the buffer. Then we see that samplesLeft < samplesUntilMODTick, so we mix the 100 samples, and subtract them from samplesUntilMODTick, which then becomes 263.
Next frame, samplesLeft is set to 304 again. Then it goes into the loop and sees that samplesUntilMODTick is still 263, which is not 0, so it doesn't update the MOD. Instead, it goes and checks it against samplesLeft. 263 < 304, so MOD tick wins, and will happen before the buffer is full.
Here we mix those 263 samples. Remember last frame we updated the MOD and then mixed 100 samples? After those finish playing, and then these 263 play, that will be the total 363 needed between ticks, so our mission was a success.

After that subtract the 263 from samplesLeft (which is still 304) to get 41, and set samplesUntilMODTick to 0 to trigger the MOD update next trip through the loop. After that, the 41 samples left is of course less than the 363 needed for another MOD tick, so it mixes them, subtracts from samplesUntilMODTick, and continues on with the process for ever and ever.



2. Player framework

Now to define our GBA-side MOD structure that will store the current state of things.

typedef struct _MOD
{
   const SAMPLE_HEADER **sample;  // Pointer to table of samples in ROM
   const u8 **pattern;            // Pointer to table of pointers to patterns
   const u8 *order;               // Array of pattern numbers to play

   const u8 *rowPtr;              // Current position in current pattern, for quick access

   u8 state;                      // MOD_STATE enum, defined below (stopped/playing/paused)

   u8 speed;
   u8 tick;                       // When this gets to speed, process a new row and reset it to 0

   u8 curRow;                     // When this gets to 64, move to the next order and reset to 0

   u8 orderCount;
   u8 curOrder;                   // When this gets to orderCount, stop the song

   u8 pad[2];

   MOD_CHANNEL channel[4];        // MOD_CHANNEL struct, defined below

} MOD;

typedef enum _MOD_STATE
{
   MOD_STATE_STOP,
   MOD_STATE_PLAY,
   MOD_STATE_PAUSE,

} MOD_STATE;

typedef struct _MOD_CHANNEL
{
   u32 frequency;   // Current frequency of note being played, in Hz

   u8 sample;       // Last sample used on this channel
   u8 vol;          // Current volume

   u8 pad[2];       // Align to 4 bytes

} MOD_CHANNEL;

We'll need to add a lot more to the channel structure when we go adding effects, but for now all we really have to have is the last sample used, since even the frequency and volume won't be totally necessary without effects (I'll still use them though, to make adding effects easier).

Another thing we need is a table of frequencies for each note. We'll generate this similarly to the period-matching table from
Day 3's pattern section, except instead of storing the period for each note, we'll store the frequency. One more detail when generating the table is a thing called finetune. It's that byte stored in the sample info that I mentioned back in Day 3 samples. There are 16 different finetune levels, ranging from -8 to +7. These make a slight adjustment to the frequency of the sample. It doesn't make much difference, but it's good to support them for compatibility.
They're stored as a signed 4-bit value, so 0x0-0x7 are positive, 0xF is -1, 0xE is -2, 0x8 is -8, etc. The easiest way to convert this to a signed byte value is to just check if it's >= 8, and if so, subtract 16 from it. Like, 0xE, which is 14, becomes 14-16 = -2, which is what we wanted.
However, we don't even need to do this, we'll just make our table so the finetune values line up with it when read as a plain 4-bit value.

So, to generate our table, we need to take the C-1 row from the period table for each finetune level in fmoddoc.txt (to get the most accuracy like before). I've already done it here, so you can just copy this:

const u16 periodTable[16][12] = 
{
//Finetune 0
   { 856,808,762,720,678,640,604,570,538,508,480,453 },   // C-1 to B-1
//Finetune 1
   { 850,802,757,715,674,637,601,567,535,505,477,450 },   // C-1 to B-1
//Finetune 2
   { 844,796,752,709,670,632,597,563,532,502,474,447 },   // ...
//Finetune 3
   { 838,791,746,704,665,628,592,559,528,498,470,444 },
//Finetune 4
   { 832,785,741,699,660,623,588,555,524,495,467,441 },
//Finetune 5
   { 826,779,736,694,655,619,584,551,520,491,463,437 },
//Finetune 6
   { 820,774,730,689,651,614,580,547,516,487,460,434 },
//Finetune 7
   { 814,768,725,684,646,610,575,543,513,484,457,431 },
//Finetune -8 (actually 0x8)
   { 907,856,808,762,720,678,640,604,570,538,508,480 },
//Finetune -7 (actually 0x9)
   { 900,850,802,757,715,675,636,601,567,535,505,477 },
//Finetune -6 (actually 0xA)
   { 894,844,796,752,709,670,632,597,563,532,502,474 },
//Finetune -5 (actually 0xB)
   { 887,838,791,746,704,665,628,592,559,528,498,470 },
//Finetune -4 (actually 0xC)
   { 881,832,785,741,699,660,623,588,555,524,494,467 },
//Finetune -3
   { 875,826,779,736,694,655,619,584,551,520,491,463 },
//Finetune -2
   { 868,820,774,730,689,651,614,580,547,516,487,460 },
//Finetune -1
   { 862,814,768,725,684,646,610,575,543,513,484,457 },
};

u8 finetune, note;
   // This is the table of frequencies in Hz for each note at each finetune level in each octave. 
   // 16 finetune levels * 12 notes per octave * 5 octaves
u16 freqTable[16*12*5];

   // For reference
#define AMIGA_VAL   3579545

for(finetune = 0; finetune < 16; finetune++)
{
   for(octave = 0; octave < 5; octave++)
   {
      for(note = 0; note < 12; note++)
      {
            // Calculate the period of this note in this octave. First multiply by 2 
            // to get into octave 0, then divide by 2 for each octave up from there
         u16 tempPeriod = (periodTable[finetune][note]*2) >> octave;
            // And plug it into our period->Hz formula
         u32 tempFreq = (AMIGA_VAL / tempPeriod);

            // Chop it down if it doesn't fit in 16 bits. Only a couple of notes 
            // at the highest finetune levels make it that high, so they'll 
            // probably never be used anyway (and not noticable even if they are)
         if(tempFreq > 65535)
            tempFreq = 65535;

         freqTable[finetune*12*5 + octave*12 + note] = tempFreq;
      }
   }
}
This should be done in a seperate program, output to a text file, and copy/pasted into the GBA project.



2. Playing the song

Finally, the moment is at hand, all the background prep-work is finished, and all that remains is to actually play the notes.

To play notes, all you need to do is read the pattern data, and if there's a note, you set up the mixer channel to play the specified sample at the specified frequency, and let it go to work. The sample can either be specified on the row along with the note, or left blank and assumed to be the last sample played. Of course, there's that special case of what if they never specify a sample. There's no standard result of that, so either default to sample 0 or skip the note altogether, just as long as you don't go trying to read from a non-existant one. I just initialize to MOD_NO_SAMPLE (31) and check for that before playing a note.

The general flow of the update function is like this:

void MODUpdate()
{
   if(++mod.tick >= mod.speed)
   {
      mod.tick = 0;

      if(++mod.curRow >= 64)
      {
         mod.curRow = 0;

         if(++mod.curOrder >= mod.orderCount)
         {
            mod.state = MOD_STATE_STOP;
            return;
         }
         else
         {
            mod.rowPtr = mod.pattern[mod.order[mod.curOrder]]];
         }
      }

      MODProcessRow();
   }
   else
   {
      // We'll update the non row-tick effects here later
   }
}
FIrst check if you need to update a row or not, then see if you hit the end of the pattern, then the end of the song, and then take care of business. The row processing is where most of the action takes place, so I pulled it out into its own function. If there's one thing I've learned in all my years of programming, it's that just because you only call a function in one place does not mean that you should just code it right into that place. Especially in an outer function like this, it will make no difference in speed, and it will keep your code much more organized and manageable.
Anyway MODProcessRow looks like this:

   // Constants used
#define MOD_NO_SAMPLE 31   // MOD uses 0 as no-sample, but our converter subtracts 1 from the 
                           // numbers so we don't have to compensate for the unused sample 0
#define MOD_NO_NOTE   63   // From the example converter (valid notes are 0-59, so anything 
                           // above that would work)

void MODProcessRow()
{
   s32 curChannel;

   for(curChannel = 0; curChannel < mod.channelCount; curChannel++)
   {
      u8 note, sample, effect, param;

         // Read in the pattern data, advancing rowPtr to the next channel in the process
      note   = *mod.rowPtr++;
      sample = *mod.rowPtr++;
      effect = *mod.rowPtr++;
      param  = *mod.rowPtr++;

         // Use sample memory if no sample, or set sample memory if there is one
      if(sample == MOD_NO_SAMPLE)
      {
         sample = mod.channel[curChannel].sample;
      }
      else
      {
            // Set sample memory
         mod.channel[curChannel].sample = sample;
            // Another tricky thing to know about MOD: Volume is only set when 
            // specifying new samples, NOT when playing notes, and it is set even 
            // when there is a sample, but no note specified (although the sample 
            // playing doesn't change in that case)
         mod.channel[curChannel].vol = mod.sample[sample].vol;
         sndChannel[curChannel].vol = mod.channel[curChannel].vol;
      }

         // See if there's any note to play
      if(note != MOD_NO_NOTE)
      {
         MODPlayNote(curChannel, note, sample, effect, param);
      }
   }
}

void MODPlayNote(u32 channelIdx, u32 note, u32 sampleIdx, u32 effect, u32 param)
{
   SOUND_CHANNEL *sndChn;
   MOD_CHANNEL *modChn;
   SAMPLE_HEADER *sample;
   u8 finetune;

      // Here's that special case that they didn't specify a sample before playing a note
   if(sampleIdx == MOD_NO_SAMPLE)
   {
      return;
   }

      // These make things less cumbersome
   sndChn = &sndChannel[channelIdx];
   modChn = &mod.channel[channelIdx];
   sample = &mod.sample[sampleIdx];

      // 60 notes total, and one full set of notes for each finetune level
   modChn->frequency = freqTable[sample->finetune*60 + note];

      // Set up the mixer channel
   sndChn->data       = sample->sndData;
   sndChn->pos        = 0;
   sndChn->inc        = modChn->frequency * sndVars.rcpMixFreq >> 16;      // Explained below
   // Next member is volume, but skip setting it because it doesn't change unless a 
   // new sample was specified, in which case it was already set back in MODProcessRow
   sndChn->end        = sample->sndData + (sample->loopLength != 0 ? 
                                           sample->loopStart + sample->loopLength : 
                                           sample->length);
   sndChn->loopLength = sample->loopLength;
}
I just created a new variable there, sndVars.rcpMixFreq. Since we need to know how many source samples to advance for each output sample, we must divide the channel frequency by the master mixing frequency.
In our case, the "reciprocal mixing frequency" here will be 1/18157, or 0.0000550751... This will be fixed point of course, but needs to be very high accuracy since it's so small. Since our mixer uses 12-bits of fractional accuracy, I decided to go with 16 more bits than that, giving a 28-bit fixed-point number. This will be (1<<28)/18157, or 14784. So, when we multiply our frequency table value and shift down 16 bits, we're left with a 12-bit noteFreq/mixFreq, exactly what the mixer needs using only a multiply and shift down, rather than a shift up and divide. 14784 is small enough that we can multiply by our highest frequency (65535Hz) and still fit into 32 bits, so no need to bother with a long multiply.

And... well I guess that does it for today. That little bit of code right there is enough to play a full song, as long as it doesn't use any effects. The real work is all in the preparation of the data (and that bit about timing), which is already said and done. The final hump to climb over is effects, which can either be very simple, or clutter your player until you have no idea what's happening anymore. Until then, happy coding!

Example project for Day 5 (235KB)

Home, Day 4, Day 6