Home New Tutorials Programs
1-R4DS Setup 2-Software Space Invaders
Old Tutorials DSEmu
1-Setup 4-Sound 7-FIFO
2-Framebuffer 5-SRAM 8-Interrupts
3-Keys 6-Filesystems 9-Microphone
10-Extended Rotation Background

Homebrew Nintendo DS Development Part 4 - Sound

This tutorial was tested and works with libnds dated 2005-12-12 and DSEmu 0.4.9 (Although sound will not work in DSEmu, the program will still run).

Part Three of this tutorial series went through how to handle key presses on the Nintendo DS. This part shows how to use some of the audio capability of the DS to play sound samples.

Playing sounds turns out to cover quite a lot of ground. Only the ARM7 can access the sound hardware so we somehow have to have the ARM9 notify the ARM7 that it wants to play a sound. We also need to be able to store the sound files somewhere and access that data from the ARM7.

To keep this tutorial short I will only go through how to play sounds directly from the ARM7 and a future tutorial will go through how to signal the ARM7 from the ARM9 that it should play a sound.

Sound Registers and Macros

There are a number of registers that are used to control the playing of a sound sample. The ndslib library has some macros to make the registers easy to use.

MacroDescription
SOUND_VOL(n)Used to set the volume of a sound channel. The value can range from 0x00 (silent) to 0x7f (Full).
SOUND_FREQ(n)Sets the frequency of the sound sample to be played.
SOUND_ENABLEEnables sound support.
SCHANNEL_ENABLEEnables sound support for a given channel.
SOUND_ONE_SHOTA bitflag used to indicate the sample should be played once instead of looped.
SOUND_CRI think this is a register for setting defaults for all sound channels (enabling, volume, etc).
SCHANNEL_TIMER(c)Determines playback frequency of the sample. Setting it to the result of SOUND_FREQ(n) will play the sample at the given frequency. 'c' is the channel number (0-15).
SCHANNEL_SOURCE(c)The pointer to the sample data should be assigned to this register.
SCHANNEL_LENGTH(c)The length, in words (a word is 32 bits) of the sample data.
SCHANNEL_CR(c)Assigning to this will set things like volume, one shot, enabling, etc for a specific channel.

Given a pointer to an 8bit sample, recorded at 22,050Hz, where 'sample' is the start of the data, and 'sample_end' is the end, the following would set things up to play on channel 0:

  SCHANNEL_TIMER(0) = SOUND_FREQ(22050);
  SCHANNEL_SOURCE(0) = (uint32)sample;
  SCHANNEL_LENGTH(0) = ((int)sample_end - (int)sample) >> 2;
  SCHANNEL_CR(0) = SCHANNEL_ENABLE | SOUND_ONE_SHOT | SOUND_8BIT | SOUND_VOL(0x3F);

The sample length is divided by four (the '>> 2' shift does this) as the length computed by subtracting start from end is in bytes. We need it in words.

Converting and Storing the Samples

The samples need to be stored somewhere for the program to read and play. The easiest way of doing this is to link the data directly into the executable. This does limit the amount of data we can store though (the DS only has 4MB of RAM). I'll explore other options in later tutorials.

The samples also need to be in 'raw' format. Most music formats are either compressed and/or have some header information. We want uncompressed data. I use an open source tool called 'Sox' to convert some WAV files to RAW format. A Win32 download of Sox is available from the Sox website or locally here.

Sox is very easy to use, and by default does what we want when converting a WAV file to RAW:

d:\sound_demo1\wav>sox -V Ufo.wav Ufo.raw
sox: Detected file format type: wav

sox: WAV Chunk fmt
sox: WAV Chunk data
sox: Reading Wave file: Microsoft PCM format, 1 channel, 11127 samp/sec
sox:         11127 byte/sec, 1 block align, 8 bits/samp, 1802 data bytes
sox: Input file Ufo.wav: using sample rate 11127
        size bytes, encoding unsigned, 1 channel
sox: Output file Ufo.raw: using sample rate 11127
        size bytes, encoding unsigned, 1 channel
sox: Output file: comment "Processed by SoX"

By passing '-V' we get 'verbose' information which tells us the frequency we should use for playing this back on the NDS. In this case it is 11,127.

The raw file produced by Sox must be converted to an 'object' file for linking into the ARM7 executable. This 'object' file contains symbols that our program can use to get a pointer to the start and end of the data. The command to do this is 'arm-elf-objcopy'. This command takes a dizzying array of arguments so I'll show the command I use then explain the details:

  arm-elf-objcopy -I binary -O elf32-littlearm -B arm \
      --rename-section .data=.rodata,readonly,data,contents,alloc \
      --redefine-sym _binary_Ufo_raw_start=sound_ufo \
      --redefine-sym _binary_Ufo_raw_end=sound_ufo_end \
      --redefine-sym _binary_Ufo_raw_size=sound_ufo_size \
      Ufo.raw Ufo.o

The first three arguments of '-I binary', '-O elf32-littlearm' and '-B arm' just set the output file format to be an ARM object file.

The 'rename-section' is used to set the area in memory where this data will be stored.

By default, arm-elf-objcopy, creates the following symbols (Where 'filename' and 'ext' is replaced with the input filename and extension respectively):

The 'redefine-sym' option renames these symbols to something more readable for our program to access. The result of running this command produces a 'Ufo.o' file which can be linked into our program (Note that it needs to be linked into the ARM7 binary):

  arm-elf-g++ -g -mthumb-interwork -mno-fpu \
     -specs=ds_arm7.specs arm7_main.o wav/Ufo.o  \
     -Ld:\devkitpro\libnds\lib -lnds7 -o arm7.elf

A pointer to the data can be retrieved by reading the 'sound_ufo' symbol. A simple macro creates 'extern' definitions for these in our program:

#define EXTERNAL_DATA(name) \
  extern const uint8  name[]; \
  extern const uint8 name##_end[]; \
  extern const uint32 name##_size

EXTERNAL_DATA(sound_ufo);

This data can then be used to populate the sound registers. Note the frequency is set to 11127 as given by the Sox output:

  SCHANNEL_TIMER(0) = SOUND_FREQ(11127);
  SCHANNEL_SOURCE(0) = (uint32)sound_ufo;
  SCHANNEL_LENGTH(0) = ((int)sound_ufo_end - (int)sound_ufo) >> 2;
  SCHANNEL_CR(0) = SCHANNEL_ENABLE | SOUND_ONE_SHOT | SOUND_8BIT | SOUND_VOL(0x3F);

Linking the sound samples into the ARM7 executable is not the best idea for anything but simple test cases. From a posting on the GBADEV forums it was mentioned that in DevKitPRO, the ARM7 executable is limited to 64KB of code. So even small samples could cause this limit to be reached. You can either link the samples into the ARM9 or use a filesystem. In both those cases you'll need to have the ARM9 send messages to the ARM7 with a pointer to the data to play. This is covered in detail in tutorial six.

Simple example program

The example program demonstrating this is 'sound_demo1'. The ARM9 code is in arm9_main.cpp and is exactly the same code as in Tutorial Three.

The ARM7 code, in arm7_main.cpp does the sound related code. It is basically the default ARM7 code used in the previous tutorials with some sound code added. The first thing we do is in 'main', to 'power up' the sound and set the sound defaults:

int main(int argc, char ** argv) {
  [...]

  // Turn on Sound
  powerON(POWER_SOUND);

  // Set up sound defaults.
  SOUND_CR = SOUND_ENABLE | SOUND_VOL(0x7F);

  [...]
}

The sound data I used is the WAV files which are recorded samples of the the 'space invader' aracde game. These WAV files are:

These are converted to object files as described previously and the symbols are made available to the ARM7 using the EXTERNAL_DATA macro:

EXTERNAL_DATA(sound_base_hit);
EXTERNAL_DATA(sound_inv_hit);
EXTERNAL_DATA(sound_shot);
EXTERNAL_DATA(sound_ufo);
EXTERNAL_DATA(sound_ufo_hit);
EXTERNAL_DATA(sound_walk1);
EXTERNAL_DATA(sound_walk2);
EXTERNAL_DATA(sound_walk3);
EXTERNAL_DATA(sound_walk4);

In the vertical blank interrupt I only play some sounds if the user presses the X or Y key. Each key plays a different sound on a different channel. One sound, sound_ufo_hit, is a couple of seconds long while the other, sound_inv_hit, is short. Pressing the 'X' key will play the long sound and the 'Y' key plays the short sound. As they are on different channels pressing the long and the short will play them both at the same time:

    // Y Key
    if((~REG_KEYXY) & (1 << 1)) {
      SCHANNEL_TIMER(0) = SOUND_FREQ(11127);
      SCHANNEL_SOURCE(0) = (uint32)sound_inv_hit;
      SCHANNEL_LENGTH(0) = ((int)sound_inv_hit_end - (int)sound_inv_hit) >> 2;
      SCHANNEL_CR(0) = SCHANNEL_ENABLE | SOUND_ONE_SHOT | SOUND_8BIT | SOUND_VOL(0x3F);
    }

    // X Key
    if((~REG_KEYXY) & (1 << 0)) {
      SCHANNEL_TIMER(1) = SOUND_FREQ(11127);
      SCHANNEL_SOURCE(1) = (uint32)sound_ufo_hit;
      SCHANNEL_LENGTH(1) = ((int)sound_ufo_hit_end - (int)sound_ufo_hit) >> 2;
      SCHANNEL_CR(1) = SCHANNEL_ENABLE | SOUND_ONE_SHOT | SOUND_8BIT | SOUND_VOL(0x3F);
    }

The key handling code should be familiar to you from the previous tutorial.

Building

There is an additional step in building this program in that you need to convert the WAV files. A simple Makefile file is supplied to run the sound conversion commands as well as the compiler commands.

The complete source code is supplied in sound_demo1.zip and you can download the sound_demo1.nds and sound_demo1.nds.gba files for running on the emulators or hardware. There's not much point running it on an emulator at this stage as I don't think any of the emulators support audio yet.

Conclusion

This tutorial showed how to play some simple sound samples from the ARM7 and how to store those samples in the executable.

A lot more can be done and a lot more questions need to be answered. Some things I need to work out (and would appreciate a pointer to information if available) are:

I'd like to thank the author of the 'audio_sample' demo which helped me understand many of the issues involved in playing samples. In particular, how to link the samples into the executable.

As always, any comments or suggestions are welcomed. See my contact details below.

See Tutorial Six for further information on how to structure the application for playing sound.