Let me preface this article by saying that of all the hobbyist programming I have done, I enjoy developing for the Gameboy Advance the most. It strips away a lot the of the complexity normally associated with developing for the so-called "more powerful" machines. There's no hassling with drivers, complicated graphics APIs, or (cough) unruly operating systems. You get to program directly to the metal. And come on, how often does a hobbyist get to see his or her creation running on an actual commercial game console? On top of that, the dev hardware is reasonably cheap (a flash linker and cartridge will run you about $120), the standard C and C++ libraries are free (see Jason Wilkins Unofficial GameBoy Advance Software Development Kit), and the hardware documentation is abundant. Hmm... Well, maybe not that last one.
While it's true that information about the video hardware of the GBA is plentiful on the 'net, the well appears to dry up as soon as one starts investigating the system's audio capabilities. (The notable exception to this is the Audio Advance website, but even it doesn't provide any examples of sample mixing) As you might have guessed, that's where this series of articles come in. By the time it is finished, I will have documented in careful detail the steps necessary to create a fully functional, eight channel, 22KHz sample mixer that uses (at most) 10% of the CPU. I should note that I will not be covering the Sound Channels 1-4, as they appear to be little more than a holdover from the Gameboy Color, and are not involved in the playback of recorded samples. Instead, I will cover what are called the "Direct Sound" hardware channels.
There will be three articles in this series by the time it is finished. A rough outline of their content is as follows:
- The first article (this one) will introduce the different hardware systems that control the playback of digital audio on the GBA. It will touch on the topics of Timers, DMA, and the Direct Sound channels.
- The second article will cover a C implementation of a multi-channel sound mixer. The focus will be on the logic behind sound mixing on the GBA rather than on performance (ie. it will be slow, but hopefully very easy to understand).
- The last article will tighten the screws on the implementation presented in Article #2, and will bump up performance quite a bit. The largest change will involve re-writing the core of the mixer in ARM assembly. If assembly coding scares you, don't worry because a) it won't be more than a few simple instructions, and b) it will be heavily commented. Is everybody ready? Ok then, let's get started.
A fundamental concept that any GBA programmer should be familiar with is that all of the specialized functionality of the hardware is memory-mapped. This means that tasks like changing the video mode, reading input from the keypad, playing sounds, etc. can all be performed simply by writing to or reading from specific memory locations. For example, if I wanted to read the state of the keypad (ie. which of the buttons are currently being pressed), I would just read a 16-bit value from memory address 0x4000130 and interpret it accordingly. What makes this such a powerful concept is that it essentially boils down control of the specialized hardware to a few simple "switches." I would suggest checking out either Dovoto's The Pern Project or Brian Sowers GBA Development From the Ground Up for more information about the basic functionality of the GBA.
[size="5"]Turning the thing on
Before we can make any noises with the GBA, we have to tell the hardware to turn the sound chip on. Luckily for you beginners, this is also a classically simple example of the memory-mapping I was just talking about. To enable the audio hardware, all we have to do is set bit 7 of REG_SOUNDCNT_X (a 32-bit value located at address 0x04000084) to 1. It amounts to a simple C statement:
Kind of cryptic, huh? Let's add a couple of #defines to preserve our sanity, like this:
*(u32*)0x04000084 = 0x00000080;
Now our statement makes a lot more sense when we look at it.
#define REG_SOUNDCNT_X *(u32*)0x04000084
#define SND_ENABLED 0x00000080
REG_SOUNDCNT_X = SND_ENABLED;
The GBA has two digital-to-analog converters (dubbed Direct Sound A and Direct Sound B) for playing back signed 8-bit PCM samples. Functionally these two channels are identical and can be used at the same time, but you really only need to use one. To get the GBA to play a sound, all you have to do is feed these channels the correct data at the correct rate and the hardware will take care of the rest. Sounds simple enough, right?
[bquote]I assume there are two Direct Sound channels to facilitate stereo sound in games that need it. However, since the GBA only has a single monaural speaker on the right side of its casing, the only way you're going to hear stereo sound is if you use the headphone jack.[/bquote]
Well, as is usually the case, it's a little more complicated than that. It turns out you can't just send sample data directly to the playback hardware. Instead, Direct Sound decides according to its own internal criteria (which we'll cover shortly) when it needs data. It then reads a single sample (one byte) out of a queue called the FIFO (First In First Out), and sends it along to the sound chip. There are actually two FIFOs; one for Direct Sound A and the other for Direct Sound B. If I lost anyone there, don't worry about it. After the initial setup, all we have to be concerned with from a programmer perspective is keeping the FIFO(s) full.
The Direct Sound channels are controlled by REG_SOUND_CNT_H (address 0x04000082). A layout of the bits in this register can be seen below. Everything should look familiar except for the Timer designation, which we will cover below.
Bits Function 0-1 Output sound ratio for channels 1 - 4 (00 = 25%, 01 = 50%, 10 = 100%) 2 Direct Sound A output ratio (0 = 50%, 1 = 100%) 3 Direct Sound B output ratio (0 = 50%, 1 = 100%) 4-7 Unused 8 Enable Direct Sound A output to right speaker (0 = No, 1 = Yes) 9 Enable Direct Sound A output to left speaker (0 = No, 1 = Yes) A Direct Sound A sampling rate Timer (0 = Timer 0, 1 = Timer 1) B Direct Sound A FIFO reset (write a 1 here to reset the FIFO) C Enable Direct Sound B output to right speaker (0 = No, 1 = Yes) D Enable Direct Sound B output to left speaker (0 = No, 1 = Yes) E Direct Sound B sampling rate Timer (0 = Timer 0, 1 = Timer 1) F Direct Sound B FIFO reset (write a 1 here to reset the FIFO)
So why can't you just feed your sample data to Direct Sound all at once? Well, let's consider an example. Say you have an audio clip recorded at a sampling rate of 22050 Hz. This means that when we play the clip back, we want to process 22050 samples per second and no more. The GBA's CPU runs at 16.7 MHz (or 2 ^ 24 Hz) so a little math tells us that we only need to send a sample once every 761 CPU cycles.
As you can see, there needs to be a fixed rate at which we send samples to the playback hardware. This is where the Timers come in. The GBA has four hardware Timers that count up to 0xFFFF (65535) and can be set to increment after every 1, 64, 256, or 1024 CPU cycles. Since the Timer values are 16-bit, when a Timer with a count of 0xFFFF is incremented it is said to overflow back to whatever value it was originally set with (ie. if you set a Timer to count up from 0xBBBB, when it overflows it will reset to 0xBBBB). For our purposes, we can use the overflow of Timers 0 and 1 to tell Direct Sound that it is time to send a sample to the playback hardware. You can see in the above definition of REG_SOUNDCNT_H that bits A and E are used to specify a Timer. When you set these bits, you're basically telling Direct Sound to watch one of these Timers, and to send a one-byte sample to the playback hardware when it overflows. Before we go any further, here is the layout of the Timer control registers.
REG_TMxCNT (replace x with the Timer number 0 - 3)
REG_TMxD (replace x with the Timer number 0 - 3)
Bits Function 0-1 Timer frequency (0 = 1 cycle, 1 = 64 cycles, 2 = 256 cycles, 3 = 1024 cycles) 2 Increment Timer x on Timer x-1 overflow (0 = No, 1 = Yes) 3-5 Unused 6 Timer will generate an interrupt on overflow (0 = No, 1 = Yes) 7 Enable Timer (0 = No (disable), 1 = Yes (enable)) 8-15 Unused
So to start a Timer, we set the initial value that we want to count up from in REG_TMxD, and then set the appropriate bits in REG_TMxCNT (for us, this will only involve setting bit 7 to enable the Timer). Now, back to our example. We have established that we want to send a one-byte sample from the FIFO to the playback hardware once every 761 CPU cycles, and we know that the sending of a sample is triggered by the overflow of the Timer(s) specified in REG_SOUNDCNT_H. Now, because we want the specified Timer to overflow every 761 CPU cycles, we will set its initial value to 65536 (or 0xFFFF) - 761. My calculator comes up with 64774 (or 0xFD06).
Bits Function 0-15 Timer start value
Now that we've gotten all that down, we have all the information we need to initialize Direct Sound. As before, we're going to add a couple of #defines to improve readability. This will allow you to logically OR a series of bit-flags together to simplify setting the register. You'll notice here that I refer to register REG_SOUNDCNT_L, which controls the sound channels 1-4 (among other things). We won't be using them in this series, but since they are part of the sound system, it's a good idea to zero the register out.
[bquote]For those that haven't seen the | operator before, it represents for a logical OR, and it can be used to easily combine flags when setting a register. For example, if you wanted to set just the first and last bits of a 16-bit number, you would assign it the value of 0x0001 | 0x8000, which evaluates to 0x8001.
// register definitions[/bquote]
#define REG_SOUNDCNT_L *(u16*)0x04000080
#define REG_SOUNDCNT_H *(u16*)0x04000082
#define SND_ENABLED 0x00000080
#define SND_OUTPUT_RATIO_25 0x0000
#define SND_OUTPUT_RATIO_50 0x0001
#define SND_OUTPUT_RATIO_100 0x0002
#define DSA_OUTPUT_RATIO_50 0x0000
#define DSA_OUTPUT_RATIO_100 0x0004
#define DSA_OUTPUT_TO_RIGHT 0x0100
#define DSA_OUTPUT_TO_LEFT 0x0200
#define DSA_OUTPUT_TO_BOTH 0x0300
#define DSA_TIMER0 0x0000
#define DSA_TIMER1 0x0400
#define DSA_FIFO_RESET 0x0800
#define DSB_OUTPUT_RATIO_50 0x0000
#define DSB_OUTPUT_RATIO_100 0x0008
#define DSB_OUTPUT_TO_RIGHT 0x1000
#define DSB_OUTPUT_TO_LEFT 0x2000
#define DSB_OUTPUT_TO_BOTH 0x3000
#define DSB_TIMER0 0x0000
#define DSB_TIMER1 0x4000
#define DSB_FIFO_RESET 0x8000
// we don't want to mess with sound channels 1-4
REG_SOUNDCNT_L = 0;
// enable and reset Direct Sound channel A, at full volume, using
// Timer 0 to determine frequency
REG_SOUNDCNT_H = SND_OUTPUT_RATIO_100 |
Now that we understand how Direct Sound uses the data in the FIFO, all we have to worry about is keeping it filled with fresh samples. On the surface, this might appear to be problematic. Obviously a real game isn't going to want to stop every 761 CPU cycles and make sure the FIFO isn't empty. Wouldn't it be nice if we could just tell the FIFO to refill itself whenever its supply of fresh samples is running low? Well, luckily for us, the GBA has just such a solution in the form of the DMA (Direct Memory Access) channels 1-2. For those of you that are unfamiliar with the concept, the DMA channels are responsible for shuttling data around the system without the aid of the CPU (note that their usefulness also extends far beyond the realm of audio playback). Their operation is very simple, and is controlled by three memory-mapped registers, which are outlined below.
REG_DMAxSAD (replace x with the channel number 0 - 3)
Bits Function 0-31 Source address of the data being sent
REG_DMAxDAD (replace x with the channel number 0 - 3)
Bits Function 0-31 Destination address of the data being sent
REG_DMAxCNT (replace x with the channel number 0 - 3)
Bits Function 0-15 The word count of the data being sent 16-20 Unused 21-22 Destination register update
00 = increment the destination counter after each word transfer
01 = decrement the destination counter after each word transfer
10 = destination is always the Direct Sound FIFO
11 = increment the destination register after each word transfer and reset on DMA completion (used to create so-called Mode 7 effects)
23-24 Source register update
00 = increment the source counter after each word transfer
01 = decrement the source counter after each word transfer
10 = fixed (the source counter is the same for each word transfer)
11 = invalid setting
25 DMA repeat (causes the DMA to be repeated on the interval specified by the timing bits below)
0 = No
1 = Yes
26 Transfer word size (0 = 2 byte words, 1 = 4 byte words) 27 Unused 28-29 Timing control (determines when the DMA begins)
00 = DMA starts immediately
01 = DMA starts at the next vblank
10 = DMA starts at the next hblank
11 = DMA starts when the FIFO needs re-filling
30 Causes an interrupt upon completion of the data transfer (0 = No, 1 = Yes) 31 Enable the transfer (0 = transfer disabled, 1 = transfer enabled)
To initiate a DMA transfer we first set the source and destination registers of the data we are sending, and then set the appropriate bits in the control register to enable the transfer.
Using the above register definitions, the steps for setting up a DMA to the FIFOs are fairly straightforward. The code below will initiate a DMA transfer to FIFO A using Timer 0.
// DMA channel 1 register definitions
#define REG_DMA1SAD *(u32*)0x40000BC // source address
#define REG_DMA1DAD *(u32*)0x40000C0 // destination address
#define REG_DMA1CNT *(u32*)0x40000C4 // control register
// DMA flags
#define WORD_DMA 0x04000000
#define HALF_WORD_DMA 0x00000000
#define ENABLE_DMA 0x80000000
#define START_ON_FIFO_EMPTY 0x30000000
#define DMA_REPEAT 0x02000000
#define DEST_REG_SAME 0x00400000
// Timer 0 register definitions
#define REG_TM0D *(u16*)0x4000100
#define REG_TM0CNT *(u16*)0x4000102
// Timer flags
#define TIMER_ENABLED 0x0080
// FIFO address defines
#define REG_FIFO_A 0x040000A0
#define REG_FIFO_B 0x040000A4
// our Timer interval that we calculated earlier (note that this
// value depends on our playback frequency and is therefore not set in
#define TIMER_INTERVAL (0xFFFF - 761)
// set the timer to overflow at the appropriate frequency and start it
REG_TM0D = TIMER_INTERVAL;
REG_TM0CNT = TIMER_ENABLED;
// start the DMA transfer (assume that pSample is a (signed char*)
// pointer to the buffer containing our sound data)
REG_DMA1SAD = (u32)(pSample);
REG_DMA1DAD = (u32)REG_FIFO_A;
REG_DMA1CNT = ENABLE_DMA | START_ON_FIFO_EMPTY | WORD_DMA | DMA_REPEAT;
Note that DMA channels 1 and 2 appear to be hardwired to feed FIFOs A and B respectively. This means that you must use DMA channel 1 to send sound data to FIFO A and DMA channel 2 send sound data to FIFO B.
[size="5"]Stopping a sample
Now that we've got the sound playing, it would be helpful if we knew how to shut it off. This turns out to be a little more difficult than it sounds. You see, the DMA controller doesn't care about the word count setting (even if you include it in the control register) on a DMA to the FIFOs. Unless you manually disable the transfer by setting the enable bit (31) to zero upon the sample's completion, it will continue to feed the FIFOs whatever data happens to follow your sample in memory (other variables, code, garbage, etc.). Clearly, this isn't a good thing.
There are a couple of different ways to determine when a sample has finished. One of the more popular methods involves using another Timer to keep track of the number of samples played, and having it generate an interrupt upon its overflow, which was cleverly pre-calculated to occur right as the sample is finishing. However, I don't want to get into interrupt handlers for this demo (we're trying to keep it simple, after all). A more facile solution (and the one we're going to use) is to count the vblanks. You see, like a television, the GBA updates its screen approximately 60 times per second (59.727 to be exact). If we know the length of a sample, we can easily calculate the number of screen refreshes that will occur during its playback. Then all we have to do is count the vblanks (a much simpler task than setting up an interrupt handler) and stop the sample accordingly. I'm not going to cover this method in too much detail because it's not directly related to sound playback. However, it will be implemented in the sample program, so feel free to take a look at it.
[size="5"]Putting it all together
We now have all the information we need to play a single sample on either of the Direct Sound channels. A high-level overview of the steps would go something like this:
- Enable the sound hardware by setting REG_SOUNDCNT_X.
- Set up the Direct Sound playback parameters in register REG_SOUNDCNT_H.
- Set the appropriate Timer to overflow according to the frequency (sampling rate) of the sample(s).
- Set up and enable a DMA of the audio data to the appropriate FIFO.
- Stop the sound using whatever method you are comfortable with.
I have created a demo program that plays back two different samples (mapped to the GBA buttons A and B), along with the commented source. It should look pretty straightforward after all we have covered. The only thing in there that I haven't discussed yet is the SAMPLE structure, which is used to store attributes of the sample and reference the raw audio data. It looks like this:
typedef struct _sample
s8 *pData; // a pointer to the raw sound data (which
// is essentially a large array of signed
u32 length; // the length of the sample in bytes
// (padded to 1/60th of a second)
I would advise you to play around with the demo and make sure you understand what's going on under the hood. Go ahead and tinker with some of the values. I have also included a utility program called Simon that reads raw PCM files and outputs the audio information in the form of C header files that you can include in the project workspace (note that this utility will automatically pad a sample with silence to a 1/60[sup]th[/sup] of a second boundary). A couple of good learning exercises might be:
- Switching the program to use Direct Sound channel B instead of channel A.
- Adjusting the frequency to produce faster/slower playback.
- Creating and playing your own samples using the included utility.
Make sure you're boned up for next time when we take what we've learned in this session, tear it all apart and rebuild it into a real, functional, multi-channel sound effects mixer!
If you're interested in learning more about the Gameboy Advance and how it works, I recommend checking out the sites below. They have been a tremendous help to me in writing this article and should provide you with a much better understanding of this remarkable little machine.
GBA Development From the Ground Up at Gamedev.net
The Pern Project
The Audio Advance
GameBoy Advance Dev'rs
Unofficial GameBoy Advance Software Development Kit
GBADev at Yahoo Groups
If you have any questions, comments, or constructive criticisms about this article, feel free to contact me at [email="Anarcrothe@hotmail.com"]Anarcrothe@hotmail.com[/email].