Home CPSC 305

GBA Sound

 

Overview

Sound is caused by waves which travel through air, causing air particles to vibrate. The frequency of these waves determine the pitch, and the amplitude determine the volume. Most sounds are not simple sine waves, but more complex waves with multiple pitches sounding at once.

Sound can be represented as the height of a wave as it changes over time. In the digital world, we don't perfectly store sounds, but instead approximate them. This is done by storing a number of samples which are slices of the height of a wave in time.

Each sample is a single number. CDs store these as 16-bit numbers, while the GBA uses 8-bit numbers. This determines the difference between the quietest and loudest sound that can be produced.

The number of samples per second, called the sampling rate, determines the quality of the audio, and also the highest pitch which can be produced. CDs use 44,100 samples per second. We will use less on the GBA to avoid using so much memory.


 

GBA Sound Hardware

The Game Boy Advance has four sound channels that are backwards-compatible with the original Game Boy. These are fairly limited, and we won't use them in the examples here.

The GBA also adds two new sound channels, called Direct Sound A and B, that can each play 8-bit stereo sampled digital audio.

To enable the sound system, we have to set the 7th bit of the master sound enable register, which is a control register at a specific address:


/* allows turning on and off sound for the GBA altogether */
volatile unsigned short* master_sound = (volatile unsigned short*) 0x4000084;
#define SOUND_MASTER_ENABLE 0x80

This register also has bits for setting volume.

We then have a separate control register for the two channels we will use:


/* has various bits for controlling the direct sound channels */
volatile unsigned short* sound_control = (volatile unsigned short*) 0x4000082;

There are bits in this register for controlling whether the sound is enabled on the left and right side of channels A and B, and indicating that the channel has been reset. We have the following defines to use them:


/* bit patterns for the sound control register */
#define SOUND_A_RIGHT_CHANNEL 0x100
#define SOUND_A_LEFT_CHANNEL 0x200
#define SOUND_A_FIFO_RESET 0x800
#define SOUND_B_RIGHT_CHANNEL 0x1000
#define SOUND_B_LEFT_CHANNEL 0x2000
#define SOUND_B_FIFO_RESET 0x8000

Each of these two channels has a FIFO buffer for storing samples to be played. Each of these buffers is only 4 bytes large. The sound hardware will play the sample that is in this buffer. In order to play audio, we must continually change the value stored in these buffers. They are located here:


/* the location of where sound samples are placed for each channel */
volatile unsigned char* fifo_buffer_a  = (volatile unsigned char*) 0x40000A0;
volatile unsigned char* fifo_buffer_b  = (volatile unsigned char*) 0x40000A4;


 

Getting Sound Data

In order to get sound data, we must have it in the format that the GBA can process, which is uncompressed 8-bit signed samples.

One program that can convert to this format is Audacity which is free and cross-platform. You can import audio into Audacity from many different formats.

One should then compress the tracks to Mono by clicking "Tracks -> Stereo Track to Mono". While the GBA can do stereo sound, we will not handle that here. The sample rate of the sound can also be changed at the bottom of the screen. The higher the rate, the better the quality, but the larger the file.

To export the data, click "File -> Export Audio". Then we must choose "Other uncompressed files" from the option menu. Then "RAW (header-less)" from the Header menu, and "Signed 8-bit PCM" from the Encoding menu. Typically such files are named .raw

This file will now contain the audio information in a direct format which can be used by the GBA. Because the file has no header, it doesn't convey any information about what sort of file it is, or the sample rate, or anything else.

In order to load the file into a GBA program, we must get this file into the .gba file somehow. Again the simplest way is to convert it into an array of data and pass it along to our compiler. To help with this, I have a program called raw2gba. This is similar to the png2gba program, but dumps the raw audio into a .h file.

We can run the program on a .raw file to produce a .h file, and then include that into a program.


 

Automatic Sound Transfer

Now we have a large array of samples, and the hardware to play them. The next thing to do is continually put them into the audio buffer one by one. This could be done manually in a for loop, but then we would have no time for the rest of the program!

Instead we have to set it up such that the audio samples are delivered to the buffer automatically. This is done via direct memory access (DMA).

We've already seen that DMA can be used to transfer data from one part in memory to another. We did this with DMA channel 3. The GBA actually has 3 other DMA channels which can be used. Channels 1 and 2 can be used for providing sound data to sound channels A and B respectively.

Previously we used the DMA control register to specify that DMA should happen and choose 16 or 32 bit transfers. Other bits of these registers allow us to set up the automatic sound transfer:


/* this causes the DMA destination to be the same each time rather than increment */
#define DMA_DEST_FIXED 0x400000

/* this causes the DMA to repeat the transfer automatically on some interval */
#define DMA_REPEAT 0x2000000

/* this causes the DMA repeat interval to be synced with timer 0 */
#define DMA_SNC_TO_TIMER 0x30000000

Setting these bits make the destination fixed (so we can continually copy into the sound buffers), and repeated. To copy a sound array into the sound buffer A continually, we could do this:


*dma1_source = (unsigned int) sound;
*dma1_destination = (unsigned int) fifo_buffer_a;
*dma1_control = DMA_DEST_FIXED | DMA_REPEAT | DMA_32 | DMA_SYNC_TO_TIMER | DMA_ENABLE;

The next question is how often is a sample transferred. In order to control that, we need to use a timer.


 

Timers

The GBA has 4 timers. These can be used for different purposes. We can set one to go off at some set time and have it trigger something then (through an interrupt). Timer 0 is also used to control the DMA transfer to the sound channels.

The way that the timers work is by syncing to the GBA processor clock which operates at a fixed speed (16.78 MHz). The timers are just counters which increment until they eventually overflow. Each timer has a data value (the current count) and a control (which turns it on or off, and sets its frequency):


/* define the timer control registers */
volatile unsigned short* timer0_data = (volatile unsigned short*) 0x4000100;
volatile unsigned short* timer0_control = (volatile unsigned short*) 0x4000102;

/* make defines for the bit positions of the control register */
#define TIMER_FREQ_1 0x0
#define TIMER_FREQ_64 0x2
#define TIMER_FREQ_256 0x3
#define TIMER_FREQ_1024 0x4
#define TIMER_ENABLE 0x80

The timers can increment every clock cycle, every 64 clock cycles, and so on.

In order to set up a timer, we set the timer's data field to be the number of ticks for that timer until it goes off, and then set the control to be enabled along with the frequency with which the time ticks.

To set it up to play our sound, we calculate the number of CPU cycles that happen for each sample according to the sample rate of the audio:


unsigned short ticks_per_sample = CLOCK / sample_rate;

The clock (CPU ticks/second) is divided by the sample rate (samples/second) to give us the number of CPU ticks per sample.

We then use this value to set the timer's data field:


*timer0_data = 65536 - ticks_per_sample;

The timers count up to 65536 (the largest 16-bit number). So we start it the number below that which we need. Every clock tick, this timer will be incremented. When it hits 65536, a new sample will be needed and DMA will transfer one to the sound buffer.

All that's needed now is to turn on the timer to tick once every clock cycle:


*timer0_control = TIMER_ENABLE | TIMER_FREQ_1;

After setting that up, our sound will now play!

The issue now is to make the sound stop playing at the right time. If we don't handle this, the sound hardware will begin playing garbage data after the sound samples run out (which will sound horrible!)


 

Getting the Sound to Stop or Repeat

This could be handled by checking if the sound is done inside the main loop of our program, and stopping the sound then. The issue with this is that the sounds then depend on how fast the rest of the program is going. In order to always stop the sounds right when they are done, we use an interrupt instead.

The interrupt which we will use is the VBLANK interrupt which is triggered every time that the screen is drawn. This is reliable because on the GBA a VBLANK occurs once every 280,806 clock cycles.

The idea is that we figure out how many VBLANK periods our sound will last for. Then we catch an interrupt when VBLANK occurs, and decrement this value. When it reaches 0 we know that the sound is done. We'll make these globals so they can be accessed in the interrupt handler:


unsigned int channel_a_vblanks_remaining = 0;
unsigned int channel_b_vblanks_remaining = 0;

When it's done, we can either turn it off, or play it again (to create looped music for instance).

To figure out how many VBLANK periods we have in our sound, we can multiply the total number of samples by the number of CPU ticks per sample, then divide by the number of CPU ticks per VBLANK period. This will give us the number of VBLANK periods for the whole sound:


channel_a_vblanks_remaining = total_samples * ticks_per_sample * (1.0 / CYCLES_PER_BLANK);

We can now display the entire play_sound function which will start a sound effect playing on either one of the channels with any sample rate and length:


/* play a sound with a number of samples, and sample rate on one channel 'A' or 'B' */
void play_sound(const signed char* sound, int total_samples, int sample_rate, char channel) {
    /* start by disabling the timer and dma controller (to reset a previous sound) */
    *timer0_control = 0;
    if (channel == 'A') {
        *dma1_control = 0;
    } else if (channel == 'B') {
        *dma2_control = 0;
    }

    /* output to both sides and reset the FIFO */
    if (channel == 'A') {
        *sound_control |= SOUND_A_RIGHT_CHANNEL | SOUND_A_LEFT_CHANNEL | SOUND_A_FIFO_RESET;
    } else if (channel == 'B') {
        *sound_control |= SOUND_B_RIGHT_CHANNEL | SOUND_B_LEFT_CHANNEL | SOUND_B_FIFO_RESET;
    }

    /* enable all sound */
    *master_sound = SOUND_MASTER_ENABLE;

    /* set the dma channel to transfer from the sound array to the sound buffer */
    if (channel == 'A') {
        *dma1_source = (unsigned int) sound;
        *dma1_destination = (unsigned int) fifo_buffer_a;
        *dma1_control = DMA_DEST_FIXED | DMA_REPEAT | DMA_32 | DMA_SYNC_TO_TIMER | DMA_ENABLE;
    } else if (channel == 'B') {
        *dma2_source = (unsigned int) sound;
        *dma2_destination = (unsigned int) fifo_buffer_b;
        *dma2_control = DMA_DEST_FIXED | DMA_REPEAT | DMA_32 | DMA_SYNC_TO_TIMER | DMA_ENABLE;
    }

    /* set the timer so that it increments once each time a sample is due
     * we divide the clock (ticks/second) by the sample rate (samples/second)
     * to get the number of ticks/samples */
    unsigned short ticks_per_sample = CLOCK / sample_rate;

    /* the timers all count up to 65536 and overflow at that point, so we count up to that
     * now the timer will trigger each time we need a sample, and cause DMA to give it one! */
    *timer0_data = 65536 - ticks_per_sample;

    /* determine length of playback in vblanks
     * this is the total number of samples, times the number of clock ticks per sample,
     * divided by the number of machine cycles per vblank (a constant) */
    if (channel == 'A') {
        channel_a_vblanks_remaining = total_samples * ticks_per_sample * (1.0 / CYCLES_PER_BLANK);
    } else if (channel == 'B') {
        channel_b_vblanks_remaining = total_samples * ticks_per_sample * (1.0 / CYCLES_PER_BLANK);
    }

    /* enable the timer */
    *timer0_control = TIMER_ENABLE | TIMER_FREQ_1;
}


 

Interrupts

Now we just need to set up the GBA to respond to the VBLANK interrupt. There are a few control registers we need:


/* the global interrupt enable register */
volatile unsigned short* interrupt_enable = (unsigned short*) 0x4000208;

/* this register stores the individual interrupts we want */
volatile unsigned short* interrupt_selection = (unsigned short*) 0x4000200;

/* this registers stores which interrupts if any occured */
volatile unsigned short* interrupt_state = (unsigned short*) 0x4000202;

/* the address of the function to call when an interrupt occurs */
volatile unsigned int* interrupt_callback = (unsigned int*) 0x3007FFC;

/* this register needs a bit set to tell the hardware to send the vblank interrupt */
volatile unsigned short* display_interrupts = (unsigned short*) 0x4000004;

/* the interrupts are identified by number, we only care about this one */
#define INTERRUPT_VBLANK 0x1

Now we can set this up in main:


*interrupt_enable = 0;
*interrupt_callback = (unsigned int) &on_vblank;
*interrupt_selection |= INTERRUPT_VBLANK;
*display_interrupts |= 0x08;
*interrupt_enable = 1;

Interrupts should always be disabled when changing the way they are handled - because if an interrupt happens in the middle of that, who knows what would happen. We then set up the VBLANK interrupt and have it call a function called on_vblank when it occurs.

Lastly we need our function which responds to these:


void on_vblank() {
    /* disable interrupts for now and save current state of interrupt */
    *interrupt_enable = 0;
    unsigned short temp = *interrupt_state;

    /* display frames of sound remaining */
    char a_remaining[32], b_remaining[32];
    sprintf(a_remaining, "A = %d    ", channel_a_vblanks_remaining);
    sprintf(b_remaining, "B = %d    ", channel_b_vblanks_remaining);
    set_text(a_remaining, 1, 0);
    set_text(b_remaining, 2, 0);

    /* look for vertical refresh */
    if ((*interrupt_state & INTERRUPT_VBLANK) == INTERRUPT_VBLANK) {

        /* update channel A */
        if (channel_a_vblanks_remaining == 0) {
            /* restart the sound again when it runs out */
            channel_a_vblanks_remaining = channel_a_total_vblanks;
            *dma1_control = 0;
            *dma1_source = (unsigned int) zelda_music_16K_mono;
            *dma1_control = DMA_DEST_FIXED | DMA_REPEAT | DMA_32 |
                DMA_SYNC_TO_TIMER | DMA_ENABLE;
        } else {
            channel_a_vblanks_remaining--;
        }

        /* update channel B */
        if (channel_b_vblanks_remaining == 0) {
            /* disable the sound and DMA transfer on channel B */
            *sound_control &= ~(SOUND_B_RIGHT_CHANNEL | SOUND_B_LEFT_CHANNEL | SOUND_B_FIFO_RESET);
            *dma2_control = 0;
        }
        else {
            channel_b_vblanks_remaining--;
        }
    }

    /* restore/enable interrupts */
    *interrupt_state = temp;
    *interrupt_enable = 1;
}

This function is part of an example program which appears below. It starts by disabling interrupts (so as to avoid getting an interrupt while handling this one). It then displays how many VBLANK periods are left for each channel on the screen (just so we can see how it works). Then it checks for the VBLANK interrupt. In this case, that is the only interrupt we are handling, but there could be others.

When the interrupt occurs, it checks if the number of VBLANK periods is 0 for each channel. If not, it decrements the count. If so, it stops channel B, which we use for sound effects, by disabling the sound channel and DMA 2. For channel A, which is used for looped music, it sets the sound up to play again.


 

The Sound Program

The sample sound program plays music and sounds from "The Legend of Zelda: A Link to the Past". We've seen most of the program already, the main simply sets up the graphics system so we can write text, enables interrupts, and then handles the sounds:


int main() {
    /* set the graphics mode */
    *display_control = MODE0 | BG0_ENABLE;

    /* create custom interrupt handler for vblank - whole point is to turn off sound at right time
     * we disable interrupts while changing them, to avoid breaking things */
    *interrupt_enable = 0;
    *interrupt_callback = (unsigned int) &on_vblank;
    *interrupt_selection |= INTERRUPT_VBLANK;
    *display_interrupts |= 0x08;
    *interrupt_enable = 1;

    /* clear the sound control initially */
    *sound_control = 0;

    /* set the music to play on channel A */
    play_sound(zelda_music_16K_mono, zelda_music_16K_mono_bytes, 16000, 'A');

    /* just so we can see some text on the screen */
    setup_background();
    set_text("Playing sounds...", 0, 0);

    while (1) {
        /* if buttons are pressed, and channel B is off, play a sound */
        if (button_pressed(BUTTON_A) && (channel_b_vblanks_remaining == 0)) {
            play_sound(zelda_secret_16K_mono, zelda_secret_16K_mono_bytes, 16000, 'B');
        }

        if (button_pressed(BUTTON_B) && (channel_b_vblanks_remaining == 0)) {
            play_sound(zelda_treasure_16K_mono, zelda_treasure_16K_mono_bytes, 16000, 'B');
        }
    }

    return 0;
}

To handle sounds, we start a music sound to play on channel A right away - this is the one that loops. In the main loop, we check for buttons 'A' and 'B' which each trigger a sound effect to play on channel B. We also make sure that channel B has finished to avoid starting a sound while one is playing.

The complete program including the data files can be downloaded here.


 

Conclusion

Like everything in a computer, sound is represented as binary data. Sound is stored digitally as a number of samples which represent the height of a sound wave in time. To play a sound, we must have that data and play it back on hardware with the right sample rate.

Doing this well on the GBA involves using DMA, timers and interrupts to control the sound playback speed and stop when the sound is done.

Copyright © 2024 Ian Finlayson | Licensed under a Attribution-NonCommercial 4.0 International License.