GB101:Sound
GB101 Class Notes | |
---|---|
|
Introduction[edit]
So far we've focused exclusively on graphics. Sound is important too, so let's dig into that a bit. We've been primarily pulling from TONC and the Pern Project up until now. Much of the information from this section comes from The Audio Advance
GBA sound hardware[edit]
The GBA has the four sound channels of the original Gameboy, plus 2 DAC channels. The channels are as follows:
- Channels 1 and 2 are square wave generators. This means that you set a couple of registers and they generate a tone. They are identical except that channel 1 also has a sweep function, which can make the frequency rise or drop exponentially while it's played.
- Channel 3 is a 4-bit DAC that plays a user defined pattern of 64 samples.
- Channel 4 is a noise generator
- DirectSound (not related to DirectX) channels A and B. DACs that play 8 bit samples from memory.
Channels 1-4 are referred to as the DMG channels.
More naming confusion[edit]
The references to the registers you'll find around are varied. At some point they were renamed in libgba. TONC attempts to disambiguate by adding yet another set of names. Here's a table of the registers, they're various names, and functions.
Offset | Function | Old | New | TONC |
---|---|---|---|---|
60h | channel 1 (sqr) sweep | REG_SG10 | SOUND1CNT_L | REG_SND1SWEEP |
62h | channel 1 (sqr) len, duty, env | SOUND1CNT_H | REG_SND1CNT | |
64h | channel 1 (sqr) freq, on | REG_SG11 | SOUND1CNT_X | REG_SND1FREQ |
68h | channel 2 (sqr) len, duty, env | REG_SG20 | SOUND2CNT_L | REG_SND2CNT |
6Ch | channel 2 (sqr) freq, on | REG_SG21 | SOUND2CNT_H | REG_SND1FREQ |
70h | channel 3 (wave) mode | REG_SG30 | SOUND3CNT_L | REG_SND3SEL |
72h | channel 3 (wave) len, vol | SOUND3CNT_H | REG_SND3CNT | |
74h | channel 3 (wave) freq, on | REG_SG31 | SOUND3CNT_X | REG_SND3FREQ |
78h | channel 4 (noise) len, vol, env | REG_SG40 | SOUND4CNT_L | REG_SND4CNT |
7Ch | channel 4 (noise) freq, on | REG_SG41 | SOUND4CNT_H | REG_SND4FREQ |
80h | DMG master control | REG_SGCNT0 | SOUNDCNT_L | REG_SNDDMGCNT |
82h | DSound master control | SOUNDCNT_H | REG_SNDDSCNT | |
84h | sound status | REG_SGCNT1 | SOUNDCNT_X | REG_SNDSTAT |
Square Wave Generators[edit]
Channels 1 and 2 can make very simple beeping tones. The steps for doing so are as follows:
- Turn on the sound channels
- Set the channel volume, envelope, duty cycle
- Set the frequency of the tone to generate
- Repeat
// Turn sound on
REG_SNDSTAT= SSTAT_ON;
// Channel 1 on left/right ; both full volume
REG_SNDDMGCNT = SDMG_BUILD_LR(SDMG_SQR1, 7);
// DMG ratio to 100%
REG_SNDDSCNT= SDS_DMG100;
// No frequency sweep on channel 1
REG_SND1SWEEP= SSW_OFF;
// Set the envelope:
// vol=12, decay, max step time (7) ; 50% duty
REG_SND1CNT= SSQR_ENV_BUILD(12, 0, 7) | SSQR_DUTY1_2;
// Set the frequency to zero
REG_SND1FREQ= 0;
Duty and Envelope[edit]
The duty cycle determines how the tone will sound and the envelope is what determines how the sound will fade in/out. Let's start with the duty cycle..
The duty cycle is the ratio between the period (time between) of the wave and the parameter (time on) of the wave. If the duty is 1/2 then the sound will be more tonal. The other cycles sound more farty or noisy. You're used to hearing the 1/4 or 3/4's duty on gameboy. The 1/8 duty has a higher pitch, sounds more like a harpsichord.
The envelope controls attack and decay. You can do zero or one, but not both. You'll set the initial volume, the direction, and the time between changes. With TONC we can just use the macro SSQR_ENV_BUILD(ivol,decay/attack,time).
#define SSQR_DUTY1_8 0 // 12.5% duty cycle (#-------)
#define SSQR_DUTY1_4 0x0040 // 25% duty cycle (##------)
#define SSQR_DUTY1_2 0x0080 // 50% duty cycle (####----)
#define SSQR_DUTY3_4 0x00C0 // 75% duty cycle (######--) Equivalent to 25%
#define SSQR_INC 0 // Increasing volume
#define SSQR_DEC 0x0800 // Decreasing volume
// ivol=0(silent)..15(full) dir=0(decreasing)..1(increasing) time=(0..7)*1/64th secs
#define SSQR_ENV_BUILD(ivol, dir, time) \
( ((ivol)<<12) | ((dir)<<11) | (((time)&7)<<8) )
Frequency[edit]
Frequency is what determines the pitch of the tone itself. Of course, we cannot simply plug in standard frequency values in hz in there, this is a Gameboy! TONC refers to this value as the rate. The one great thing about rates is that we can reach another octave of the same note by a shift-left or shift-right. We have to do some conversion first:
rate = 2048 − 217 / frequency
Given that divides are expensive, we'll just use some macros that have already been defined.
typedef enum
{
NOTE_C=0, NOTE_CIS, NOTE_D, NOTE_DIS,
NOTE_E, NOTE_F, NOTE_FIS, NOTE_G,
NOTE_GIS, NOTE_A, NOTE_BES, NOTE_B
} eSndNoteId;
// Rates for traditional notes in octave +5
const u32 __snd_rates[12]=
{
8013, 7566, 7144, 6742, // C , C#, D , D#
6362, 6005, 5666, 5346, // E , F , F#, G
5048, 4766, 4499, 4246 // G#, A , A#, B
};
#define SND_RATE(note, oct) ( 2048-(__snd_rates[note]>>(4+(oct))) )
Implementation[edit]
Let's do a quick demo. This code defines a song by packing 32-bit ints with the note, octave, and delay that we want to use.
#include <tonc.h>
// We'll pack note, octive and delay into each int
#define OCTIVE(i) (i<<4)
#define DELAY(i) (i<<8)
// Define our song as a series of 32-bit ints
int songLength = 6;
int song[] = {
NOTE_C | OCTIVE(0) | DELAY(16),
NOTE_D | OCTIVE(0) | DELAY(16),
NOTE_E | OCTIVE(0) | DELAY(16),
NOTE_F | OCTIVE(0) | DELAY(32),
NOTE_D | OCTIVE(0) | DELAY(16),
NOTE_F | OCTIVE(0) | DELAY(128)
};
int main(void) {
int i=0;
// Turn sound on
REG_SNDSTAT= SSTAT_ON;
// Channel 1 on left/right ; both full volume
REG_SNDDMGCNT = SDMG_BUILD_LR(SDMG_SQR1, 7);
// DMG ratio to 100%
REG_SNDDSCNT= SDS_DMG100;
// No sweep
REG_SND1SWEEP= SSW_OFF;
// envelope: vol=12, decay, 3/64s step time; 25% duty
REG_SND1CNT= SSQR_ENV_BUILD(12, 0, 3) | SSQR_DUTY1_4;
REG_SND1FREQ= 0;
while(1) {
// Roll through the song..
REG_SND1FREQ = SFREQ_RESET | SND_RATE(song[i] & 15, song[i]>>4 & 15);
// Use the VBlank for delay
VBlankIntrDelay(song[i]>>8);
// Loop our song
i++;
if (i>=songLength) {
i=0;
}
}
}
DirectSound Channels[edit]
With the DirectSound channels you can play actual sampled audio. First you're going to have to get some audio converted. Use Audacity on whatever piece of audio you want to put into your game, down-sample it and the project to 16000hz, then export. Choose an output format of raw, with 8-bit signed PCM audio. Save it as foo.raw. Then you can use the raw2c utility that comes with devkitPro to turn that into .c and .h files, like you get with bitmaps.
How DirectSound works[edit]
There are two modes for DirectSound, interrupt and DMA driven. The easiest and most powerful is the DMA mode. In either mode the basic idea is this:
- Set up the directsound registers
- Set up a timer that controls how fast DirectSound plays (16000hz in our example)
- Fill up the FIFO buffer now and whenever DirectSound empties it
Timers[edit]
DirectSound uses a timer to trigger how fast it should empty the FIFO queue. Timers work by incrementing a 16-bit integer and then triggering an interrupt when it overflows. Timers can have a resolution of a single cycle up to 1024 cycles and they can be tied together to cascade events from one to another. Let's take a look at some timer registers:
#define REG_TM0D *(vu16*)(REG_BASE+0x0100) //!< Timer 0 data
#define REG_TM0CNT *(vu16*)(REG_BASE+0x0102) //!< Timer 0 control
#define REG_TM1D *(vu16*)(REG_BASE+0x0104) //!< Timer 1 data
#define REG_TM1CNT *(vu16*)(REG_BASE+0x0106) //!< Timer 1 control
#define REG_TM2D *(vu16*)(REG_BASE+0x0108) //!< Timer 2 data
#define REG_TM2CNT *(vu16*)(REG_BASE+0x010A) //!< Timer 2 control
#define REG_TM3D *(vu16*)(REG_BASE+0x010C) //!< Timer 3 data
#define REG_TM3CNT *(vu16*)(REG_BASE+0x010E) //!< Timer 3 control
#define TM_FREQ_SYS 0 //!< System clock timer (16.7 Mhz)
#define TM_FREQ_1 0 //!< 1 cycle/tick (16.7 Mhz)
#define TM_FREQ_64 0x0001 //!< 64 cycles/tick (262 kHz)
#define TM_FREQ_256 0x0002 //!< 256 cycles/tick (66 kHz)
#define TM_FREQ_1024 0x0003 //!< 1024 cycles/tick (16 kHz)
#define TM_CASCADE 0x0004 //!< Increment when preceding timer overflows
#define TM_IRQ 0x0040 //!< Enable timer irq
#define TM_ON 0x0080 //!< Enable timer
DMA controller[edit]
The DMA controller isn't something we've covered yet, but I'll give a quick rundown. The DMA controller is very useful when you have to move around large chunks of data. Because it operates independently of the CPU you can set a few registers and then go back to processing and get an interrupt when it's done. This is useful for our sound engine because we don't have to have the CPU tied up with making sure the FIFO buffer is full. Let's look at the DMA reigsters:
#define REG_DMA0SAD *(vu32*)(REG_BASE+0x00B0) //!< DMA 0 Source address
#define REG_DMA0DAD *(vu32*)(REG_BASE+0x00B4) //!< DMA 0 Destination address
#define REG_DMA0CNT *(vu32*)(REG_BASE+0x00B8) //!< DMA 0 Control
#define REG_DMA1SAD *(vu32*)(REG_BASE+0x00BC) //!< DMA 1 Source address
#define REG_DMA1DAD *(vu32*)(REG_BASE+0x00C0) //!< DMA 1 Destination address
#define REG_DMA1CNT *(vu32*)(REG_BASE+0x00C4) //!< DMA 1 Control
#define REG_DMA2SAD *(vu32*)(REG_BASE+0x00C8) //!< DMA 2 Source address
#define REG_DMA2DAD *(vu32*)(REG_BASE+0x00CC) //!< DMA 2 Destination address
#define REG_DMA2CNT *(vu32*)(REG_BASE+0x00D0) //!< DMA 2 Control
#define REG_DMA3SAD *(vu32*)(REG_BASE+0x00D4) //!< DMA 3 Source address
#define REG_DMA3DAD *(vu32*)(REG_BASE+0x00D8) //!< DMA 3 Destination address
#define REG_DMA3CNT *(vu32*)(REG_BASE+0x00DC) //!< DMA 3 Control
// Controls how the destination address is changed on subsequent copies. Normally we'll
// increment it, but in some cases, like for DirectSound we'll want it to do other things..
#define DMA_DST_INC 0 //!< Incrementing destination address
#define DMA_DST_DEC 0x00200000 //!< Decrementing destination
#define DMA_DST_FIX 0x00400000 //!< Fixed destination
#define DMA_DST_RESET 0x00600000 //!< Increment destination, reset after full run
// Controls how the source address is changed on subsequent copies. Notice that RESET is not
// available for source.
#define DMA_SRC_INC 0 //!< Incrementing source address
#define DMA_SRC_DEC 0x00800000 //!< Decrementing source address
#define DMA_SRC_FIX 0x01000000 //!< Fixed source address
#define DMA_REPEAT 0x02000000 //!< Repeat transfer at next start condition
// Controls whether we should copy in 16-bit halfwords or 32-bit words
#define DMA_16 0 //!< Transfer by halfword
#define DMA_32 0x04000000 //!< Transfer by word
// Controls when we should start the next subsequent copy. Notice that SPEC, FIFO and REFRESH
// are the same bit. The behavior of this bit changes depending on the DMA channel.
#define DMA_AT_NOW 0 //!< Start transfer now
#define DMA_AT_VBLANK 0x10000000 //!< Start transfer at VBlank
#define DMA_AT_HBLANK 0x20000000 //!< Start transfer at HBlank
#define DMA_AT_SPEC 0x30000000 //!< Start copy at 'special' condition. Channel dependent
#define DMA_AT_FIFO 0x30000000 //!< Start at FIFO empty (DMA0/DMA1)
#define DMA_AT_REFRESH 0x30000000 //!< VRAM special; start at VCount=2 (DMA3)
#define DMA_IRQ 0x40000000 //!< Enable DMA irq
#define DMA_ON 0x80000000 //!< Enable DMA
Implementation[edit]
#include <tonc.h>
#include <string.h>
#include "fanfare.h"
void timer() {
se_puts(0,96,"Sample finished..",0);
//Sample finished! Stop DMA mode Direct sound
REG_TM0CNT=0; //disable timer 0
REG_DMA1CNT=0; //stop DMA
}
int main(void) {
// Set mode 0, bg 0, sprites enabled, 1d sprites
REG_DISPCNT = DCNT_OBJ | DCNT_OBJ_1D | DCNT_MODE(0) | DCNT_BG0;
// Initialize the text engine
txt_init_std();
// Set up the text tiles on BG0
txt_init_se(0, BG_CBB(0)|BG_SBB(31), 0, CLR_RED, 1);
se_puts(0,0,"Setting up directsound..",0);
// Turn on DirectSound Channel A, Left/Right Channels, Use FIFO reset for DMA, full volume
REG_SNDDSCNT = SDS_AR | SDS_AL | SDS_ARESET | SDS_A100;
// Turn on sound, don't enable any DMG channels
REG_SNDSTAT = SSTAT_ON;
// Set up an IRQ so we can stop playback
se_puts(0,16,"Setting up irqs..",0);
irq_init(NULL);
irq_add(II_TM1,timer);
// Set up timer0 to drive DirectSound
// Formula for playback frequency is: 0xFFFF-round(2^24/playbackFreq)
se_puts(0,32,"Setting up timer..",0);
REG_TM0D = 0xFBE8; //16khz playback freq
REG_TM0CNT = TM_ON; //enable timer0
// Turn on DMA copying. It will copy from fanfare to REG_FIFOA (make sure you)
// specify address!! Use the FIFO refresh to tell DMA when to start.
se_puts(0,48,"Setting up DMA.",0);
REG_DMA1SAD = (u32)fanfare;
REG_DMA1DAD = (u32)&(REG_FIFOA);
REG_DMA1CNT = DMA_32 | DMA_REPEAT | DMA_AT_REFRESH | DMA_ON;
// We'll use a second timer that cascades from the first. We want this to overflow when
// it reaches the number of samples in our playback.
char str[32];
siprintf(str,"%d samples..",fanfare_size);
se_puts(0,64,str,0);
se_puts(0,80,"Setting up stop timer..",0);
// Max value before overflow, minus sample size, plus a little extra to prevent a pop.
REG_TM1D = 0xffff - fanfare_size + 4;
REG_TM1CNT = TM_ON | TM_CASCADE | TM_IRQ;
while(1) {
vid_vsync();
}
}
Noise Generator[edit]
DMG channel 4 is the noise generator. It can be be used to make hisses, explosion noises, and great fart noises. The envelope and duty controls are the same as the square wave generator, but the controls are different. There are no macros within TONC for the noise generators, so here are some I made up.
// Click divider frequency (0,1,2..8 = f*2,f,f/2..f/7)
#define SNS_FREQ(i) (i)
// Counter stages
#define SNS_CNT_7 (1<<2)
#define SNS_CNT_15 0
// Counter pre-stepper freqency (0..13)
#define SNS_STEP(i) (i<<3)
// Timed mode
#define SNS_CONTINUOUS 0
#define SNS_TIMED (1<<14)
// Reset
#define SNS_RESET (1<<15)
To be honest I'm not exactly sure how these work, but lower frequencies and steps make higher pitched noises. The counter stages seem to have an effect at the edges of the divider frequency and pre-stepper frequency. The continuous mode keeps the noise going until the envelope kills it. You can read more about this in The Audio Advance.
Implementation[edit]
#include <tonc.h>
// Click divider frequency (0,1,2..8 = f*2,f,f/2..f/7)
#define SNS_FREQ(i) (i)
// Counter stages
#define SNS_CNT_7 (1<<2)
#define SNS_CNT_15 0
// Counter pre-stepper freqency (0..13)
#define SNS_STEP(i) (i<<3)
// Timed mode
#define SNS_CONTINUOUS 0
#define SNS_TIMED (1<<14)
// Reset
#define SNS_RESET (1<<15)
int main(void) {
char str[32];
int freq = 8;
int step = 13;
int counter = 1;
int timed = 1;
int y = 0;
// Turn sound on
REG_SNDSTAT = SSTAT_ON;
// Channel 4 on left/right ; both full volume
REG_SNDDMGCNT = SDMG_BUILD_LR(SDMG_NOISE, 7);
// DMG ratio to 100%
REG_SNDDSCNT = SDS_DMG100;
// envelope: vol=12, decay, 7/64s step time; 50% duty
REG_SND4CNT = SSQR_ENV_BUILD(12, 0, 7) | SSQR_DUTY1_2;
REG_SND4FREQ = 0;
while(1) {
vid_vsync();
REG_DISPCNT = DCNT_OBJ | DCNT_OBJ_1D | DCNT_MODE(0) | DCNT_BG0;
txt_init_std();
txt_init_se(0, BG_CBB(0)|BG_SBB(31), 0, CLR_RED, 1);
REG_SND4FREQ = SNS_RESET | SNS_FREQ(freq) | (counter<<2) | SNS_STEP(step) | (timed<<14);
if (y >= VID_HEIGHT - 8) {
y = 0;
SBB_CLEAR(31);
}
siprintf(str,"freq=%d step=%d count=%d cont=%d",freq,step,(counter ? 7 : 15),!timed);
se_puts(0,y,str,0);
y+=8;
// Wait for a key to be pressed.
do {
key_poll();
freq += key_tri_vert();
freq = CLAMP(freq,0,9);
step += key_tri_horz();
step = CLAMP(step,0,14);
if (key_hit(KEY_A))
counter ^= 1;
if (key_hit(KEY_B))
timed ^= 1;
}
while (!key_curr_state());
// Wait for all keys to be released.
while (key_curr_state()) {
key_poll();
}
VBlankIntrDelay(8);
}
}