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 9

Microphone

This tutorial was tested and works with libnds dated 2005-12-12 and DSEmu 0.4.9 (Although the microphone doesn't actually record in DSEmu, the program will still run).

The first person to successfully use the microphone in a homebrew application was neimod, maintainer of dstek. He kindly provided the source code and this tutorial is heavily based on that code.

The Nintendo DS microphone is accessed through the Serial Peripheral Interface (SPI) on the ARM7 in a very similar way to the touchscreen is read. Usually we don't need to get down that low level as libraries like ndslib provide functions which do all the work. In the case of the touchscreen for example that is touchRead. For the microphone none of those functions exist in ndslib yet (but I'm sure they will shortly).

In the meantime I've provided a microphone7.h and microphone7.cpp in this tutorial which uses neimod's routines, ported to use ndslib's definitions.

Microphone Routines

All these microphone access routines are hidden inside a simple microphone library written for this demo, providing the following two main functions in microphone7.h and microphone7.cpp:

void StartRecording(s8* buffer, int length);

Turns the microphone on and starts recording captured sound directly into the buffer provided. The sound data is raw signed data at 16kHz. 'length' should be the size of the buffer. Once the buffer is full no more sound will be put into it.

int StopRecording();

Stops recording from the microphone and turns it off. Returns the number of bytes recorded in the 'buffer' passed to StartRecording.

Library

To use these routines in your own code, include microphone7.h and compile and link microphone7.cpp into the ARM7 executable.

Microphone Implementation

Turning the microphone on and off

All the microphone routines are only accessable from the ARM7. To use the microphone the first thing that must be done is to turn the power on to the amplifier. This is done through the Serial Peripheral interface, accessing the power controller. Neimod provided the function PM_SetAmp which accesses the SPI and can turn the Amp on or off:

// Turn on the Microphone Amp. Code based on neimod's example.
void PM_SetAmp(u8 control)
{
  while(SERIAL_CR & SERIAL_BUSY)
    swiDelay(1);

  SERIAL_CR = SERIAL_ENABLE | SPI_DEVICE_POWER | SPI_BAUDRATE_1Mhz | SPI_CONTINUOUS;
  SERIAL_DATA = PM_AMP_OFFSET;

  while(SERIAL_CR & SERIAL_BUSY)
    swiDelay(1);

  SERIAL_CR = SERIAL_ENABLE | SPI_DEVICE_POWER | SPI_BAUDRATE_1Mhz;
  SERIAL_DATA = control;
}

Calling this function with the parameter set to PM_AMP_ON will turn the amplifier on. PM_AMP_OFF will turn it off. These values are:

#define PM_AMP_ON         1
#define PM_AMP_OFF    	  0

In this demo's microphone7.h there are two functions that do this, TurnMicrophoneOn and TurnMicrophoneOff:

void TurnOnMicrophone()
{
  PM_SetAmp(PM_AMP_ON);
}

void TurnOffMicrophone()
{
  PM_SetAmp(PM_AMP_OFF);
}

Recording data

Once the amplifier is on you can start recording data. Neimod's function for this is 'MIC_GetData8':

// Read a byte from the microphone. Code based on neimod's example.
u8 MIC_GetData8()
{
  u16 result, result2;
  
  while(SERIAL_CR & SERIAL_BUSY)
    swiDelay(1);
 
  SERIAL_CR = SERIAL_ENABLE | SPI_DEVICE_TOUCH | SPI_BAUDRATE_2Mhz | SPI_CONTINUOUS;
  SERIAL_DATA = 0xEC;  // Touchscreen command format for AUX
  
  while(SERIAL_CR & SERIAL_BUSY)
    swiDelay(1);

  SERIAL_DATA = 0x00;

  while(SERIAL_CR & SERIAL_BUSY)
    swiDelay(1);

  result = SERIAL_DATA;
  SERIAL_CR = SERIAL_ENABLE | SPI_DEVICE_TOUCH | SPI_BAUDRATE_2Mhz;
  SERIAL_DATA = 0x00; 

  while(SERIAL_CR & SERIAL_BUSY)
    swiDelay(1);

  result2 = SERIAL_DATA;

  return (((result & 0x7F) << 1) | ((result2>>7)&1));
}

It again uses the SPI and returns an unsigned byte of data from the microphone. This routine should be called at a regular interval, and the data stored in a buffer to hold the recorded sound. This is best done using a timer.

Using a timer

A timer is a hardware function of the DS that can be set to raise an interrupt at regular intervals. There are eight of these timers. Four accessable from the ARM7 and four from the ARM9. A future tutorial will go into timers in detail so the code here will only be briefly described.

The period of the timer defines the frequency of the sound data that is recorded. So if you use a 16kHz timer you'll end up with a 16Khz raw sound file. The following code will set up a 16kHz timer that will raise a timer interrupt whenever the correct time interval occurs:

  // Setup a 16kHz timer
  TIMER0_DATA = 0xF7CF;
  TIMER0_CR = TIMER_ENABLE | TIMER_DIV_1 | TIMER_IRQ_REQ;

When the timer 'fires' it will raise an IRQ_TIMER0 interrupt. This can be processed in the ARM7 interrupt handler if we have set up the interrupts to handle this:

  // Set up the interrupt handler
  REG_IME = 0;
  IRQ_HANDLER = on_irq;
  REG_IE = IRQ_VBLANK | IRQ_TIMER0;
  REG_IF = ~0;
  DISP_SR = DISP_VBLANK_IRQ;
  REG_IME = 1;

Inside the interrupt handler we check the 'IF' flag to see if it is IRQ_TIMER0 and call a function in our microphone code to handle the interrupt:

void on_irq() {
  [...]
  if(REG_IF & IRQ_TIMER0) {
    ProcessMicrophoneTimerIRQ();
    VBLANK_INTR_WAIT_FLAGS |= IRQ_TIMER0;
  }
  [...]
  REG_IF = REG_IF;
}

The ProcessMicrophoneTimerIRQ function reads the data from the microphone using MIC_GetData8 and stores it in a buffer, keeping track of the length of data written:

void  ProcessMicrophoneTimerIRQ()
{
  if(microphone_buffer && microphone_buffer_length > 0) {
    // Read data from the microphone. Data from the Mic is unsigned, 
    // subtracting 128 makes it signed.
    *microphone_buffer++ = MIC_GetData8() - 128;
    --microphone_buffer_length;
    current_length++;
  }

The function to actually start recording turns the microphone on, stores the sound buffer pointer and length in global variables, and starts the timer:

void StartRecording(s8* buffer, int length)
{
  microphone_buffer = buffer;
  microphone_buffer_length = length;
  current_length = 0;

  TurnOnMicrophone();

  // Setup a 16kHz timer
  TIMER0_DATA = 0xF7CF;
  TIMER0_CR = TIMER_ENABLE | TIMER_DIV_1 | TIMER_IRQ_REQ;
}

To stop the recording there is a 'StopRecording' function that disables the timer and turns off the microphone. It returns the length of data recorded so far:

int StopRecording()
{
  TIMER0_CR &= ~TIMER_ENABLE;
  TurnOffMicrophone();
  microphone_buffer = 0;
  return current_length;
}

Microphone Demo Progam

The demo program provides a simple user interface to allow the user to record some sound and play it back. Pressing 'A' will start recording and pressing it again will stop. Pressing 'B' will play back the recorded sound.

The keys are processed from the ARM9 so we need to have a way of sending our requests to the ARM7. I'm using the same method as in previous tutorials, the 'CommandControl' structure. See Tutorial Six for details on this.

Commands

There are three commands that we want to process. Playing a sound, starting recording and stopping recording. Tutorial Six already included the code to play a sound so we'll add the two new commands in command.h:

/* Enumeration of commands that the ARM9 can send to the ARM7 */
enum CommandType {
  PLAY_ONE_SHOT_SAMPLE,
  START_RECORDING,
  STOP_RECORDING
};

Only the START_RECORDING command requires data to be passed to the ARM7. This is the sound buffer and length:

/* Command parameters for starting to record from the microphone */
struct StartRecordingCommand
{
  s8* buffer;  
  int length;
};

/* The ARM9 fills out values in this structure to tell the ARM7 what
   to do. */
struct Command {
  CommandType commandType;
  union {
    void* data;  
    PlaySampleSoundCommand playSample;    
    StartRecordingCommand  startRecording;
  };
};

One of the commands, STOP_RECORDING, needs to return data back to the ARM9. I've added a member to 'CommandControl' to hold some return data which commands can use to send information back:

struct CommandControl {
  Command command[MAX_COMMANDS];
  int currentCommand;
  int return_data;
};

Now for the ARM9 to start and stop sounds it can instruct the ARM7 to do it with the following functions:

void CommandStartRecording(s8* buffer, int length)
{
  Command* command = &commandControl->command[commandControl->currentCommand];
  StartRecordingCommand* sr = &command->startRecording;

  command->commandType = START_RECORDING; 
  sr->buffer = buffer;
  sr->length = length;

  commandControl->currentCommand++;
  commandControl->currentCommand &= MAX_COMMANDS-1;
}

int CommandStopRecording()
{
  Command* command = &commandControl->command[commandControl->currentCommand];
  command->commandType = STOP_RECORDING; 
  commandControl->return_data = -1;
  commandControl->currentCommand++;
  commandControl->currentCommand &= MAX_COMMANDS-1;
  while(commandControl->return_data == -1)
    swiDelay(1);
  return commandControl->return_data;
}

This should all be relatively familiar from Tutorial Six. One difference in 'CommandStopRecording' is that the 'return_data' field of the CommandControl structure is set by the ARM7 code which stops the sound. The ARM9 code above first sets it to '-1' then loops until it changes. When it does change it returns the value. The ARM7 code to start and stop the recording is:

static void CommandStartRecording(StartRecordingCommand* sr)
{
  StartRecording(sr->buffer, sr->length);
  commandControl->return_data = 0;
}

static void CommandStopRecording()
{
  commandControl->return_data = StopRecording();
}

These functions are very simple as they just call the high level microphone routines described previously.

ARM7 Code

The ARM7 code has to be modified to call the microphone interrupt function, ProcessMicrophoneTimerIRQ, from our microphone library. This is done inside the interrupt handler and we also when setting up the interrupt handler we need to include IRQ_TIMER0 as one of the IRQ masks. This was shown previously in the interrupt section above.

ARM9 Code

The ARM9 processes the keys in the main loop and calls the Command routines:

  while(1) {
    swiWaitForVBlank();
    key_poll();

    if(!recording && key_hit(KEY_A)) {
      recording = 1;
      CommandStartRecording(buffer, buffer_length);
    }
    else if(recording && key_hit(KEY_A)) {
      recorded_length = CommandStopRecording();
      recording = 0;
    }

    if(key_hit(KEY_B)) {
      CommandPlayOneShotSample(0, 16384, buffer, recorded_length, 0x7f);
    }
  }

A variable 'recording' is stored to keep track of whether we are recording or not. The buffer is malloced in the 'main' routine, currently set to a large 1,000,000 bytes:

static int buffer_length = 1000000;
static s8* buffer = 0;
static int recorded_length = 0;
static int recording = 0;

int main(void)
{
  [...]
  buffer = (s8*)malloc(buffer_length);
  [...]
}

The display is updated in the vertical blank interrupt:

void on_irq() 
{	
  if(REG_IF & IRQ_VBLANK) {
    printf("\x1b[2J");
    printf("Microphone Demo Program\n\n");
    
    printf("Press 'A' to start recording.\n");
    printf("Press 'A' again to stop.\n");
    printf("Press 'B' to playback.\n\n");
    if(recording)
      printf("Recording...\n");
    else
      printf("Recorded %d bytes.\n", recorded_length);
   
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
    REG_IF |= IRQ_VBLANK;   
  }
  else {
    REG_IF = REG_IF;
  }
}

Example Application Usage

Pressing 'A' will start recording and pressing it again will stop. Pressing 'B' will play back the recorded sound. The size of the recorded sample will be displayed when recording stops.

Building the Demo

The complete example program is 'microphone_demo1'. The ARM9 code is in arm9_main.cpp. It uses the console routines to print information similar to the first tutorial. The ARM7 code is in arm7_main.cpp. The microphone code is stored in microphone7.h and microphone7.cpp. The command routines are in command7.h, command7.cpp and command9.cpp. A Makefile file is supplied to build everything.

The complete source code is supplied in microphone_demo1.zip and you can download the microphone_demo1.nds and microphone_demo1.nds.gba files for running on the emulators or hardware. DSEmu 0.4.5 and above will run this demo but won't actually record any sound as the microphone routines aren't implemented in the emulator.

Conclusion

Although the underlying serial interface to the microphone seems complicated it can be hidden behind quite a high level interface as seen here. The quality is not that good and very quiet but I'm sure this will be improved as more is learnt about the microphone. I'll keep this tutorial updated with new information.

Many thanks to neimod for sharing his code, without which this tutorial would not have been possible.

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