gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

Audio > Mixing/Double buffering example

#1617 - Vortex - Mon Jan 20, 2003 7:44 pm

Hello,

Since I was not able to find any GBA exaples how to do mixing (i.e. more that one sound at a time) I spend some time this weekend and here is the code:

Code:

/////////////////////////////////////////////////////////////////////////////
// sound.c
// ver: 0.4.3

#include "gba.h"
#include "screenmode.h"
#include "keypad.h"

#include "sound21.c"
// #include "sound25.c"

///////////////////////////////////////////////////////////////////////////////
// Sound Control

#define FIFO_A (0x040000A0)
#define FIFO_B (0x040000A4)

struct SountCntH
{
   int Ch1to4OutputRatio          : 2;
   int DSndAOutRatio              : 1;
   int DSndBOutRatio              : 1;
   int unused                     : 4;

   int EnableDSndAToRightSpeaker  : 1;
   int EnableDSndAToLeftSpeaker   : 1;
   int DSndASampleRateTimer       : 1;
   int DSndAFIFOReset             : 1;

   int EnableDSndBToRightSpeaker  : 1;
   int EnableDSndBToLeftSpeaker   : 1;
   int DSndBSampleRateTimer       : 1;
   int DSndBFIFOReset             : 1;

};

///////////////////////////////////////////////////////////////////////////////
// Sound Buffer Definitions and Prototypes

#define PLAYBACK_FREQ     (16384)
#define SOUND_BUFFER_SIZE (16384)

s8  SoundBuffer1[SOUND_BUFFER_SIZE] __attribute__ ((section (".ewram"))) = { 0 };
s8  SoundBuffer2[SOUND_BUFFER_SIZE] __attribute__ ((section (".ewram"))) = { 0 };
s8* SoundBuffer __attribute__ ((section (".ewram"))) = SoundBuffer1;
u8  activeSoundBuffer __attribute__ ((section (".ewram"))) = 0;

void sbClear(void);
void sbFlip(void);
void sbInsertSample( const s8* sound_data, u16 sound_data_size, u16 offset );

void InterruptProcess(void) __attribute__ ((section(".iwram"))); //the interrupt handler from crt0.s

///////////////////////////////////////////////////////////////////////////////
// Interrupt Handler

void InterruptProcess(void)
{
   // Init DMA1 Source
   REG_DMA1SAD = (unsigned long) SoundBuffer; // DMA1 source

   //clear the interrupt(s)
   REG_IF |= REG_IF;
}

///////////////////////////////////////////////////////////////////////////////
// Sound Buffer Functions

void sbFlip(void)
{
   if( activeSoundBuffer == 0 )
   {
      SoundBuffer = SoundBuffer2;
      activeSoundBuffer = 1;
   }
   else
   {
      SoundBuffer = SoundBuffer1;
      activeSoundBuffer = 0;
   }
}

void sbClear(void)
{
   u32* buffer = (u32*) SoundBuffer;
   u32 i;

   for( i = 0; i < SOUND_BUFFER_SIZE / 4; i++ )
      buffer[i] = 0;
}

void sbClearAll(void)
{
   u32* buffer1;
   u32* buffer2;
   u32 i;

   buffer1 = (u32*) SoundBuffer1;
   buffer2 = (u32*) SoundBuffer2;

   for( i = 0; i < SOUND_BUFFER_SIZE / 4; i++ )
   {
      buffer1[i] = 0;
      buffer2[i] = 0;
   }
}

// sound_data_size must be divisible by 4 !!!
void sbInsertSampleDMA( const s8* sound_data, u16 sound_data_size )
{
   // The sample is too big
   if( sound_data_size > SOUND_BUFFER_SIZE )
      return;

   REG_DMA3SAD   = (u32) sound_data;
   REG_DMA3DAD   = (u32) SoundBuffer;
   REG_DMA3CNT_L = sound_data_size / 4;
   REG_DMA3CNT_H = 0x8400;      // DMA Enable | 32-bit transfer
}

void sbInsertSample( const s8* sound_data, u16 sound_data_size, u16 offset )
{
   u32 i;
   u16 pos = offset;

   // The sample is too large
   if( sound_data_size > SOUND_BUFFER_SIZE )
      return;

   for( i = 0; i < sound_data_size; i++ )
   {
      SoundBuffer[pos++] = sound_data[i];

      if( pos == SOUND_BUFFER_SIZE )
         pos = 0;
   }
}

u32 sbGetCurrentPosition(void)
{
   return 0;
}

//////////////////////////////////////////////////////////////////
// Resources Used: Timer0, Timer1, DMA1
void InitSoundSystem()
{
   const u32 cpuFreq = 16777216;
   struct SountCntH snd_ctrl;

   activeSoundBuffer = 0;
   SoundBuffer = SoundBuffer1;
   sbClearAll();

   snd_ctrl.Ch1to4OutputRatio          = 0;
   snd_ctrl.DSndAOutRatio              = 1;   // 100%
   snd_ctrl.DSndBOutRatio              = 0;   
   snd_ctrl.unused                     = 0;

   snd_ctrl.EnableDSndAToRightSpeaker  = 1;
   snd_ctrl.EnableDSndAToLeftSpeaker   = 1;
   snd_ctrl.DSndASampleRateTimer       = 0;   //Timer 0
   snd_ctrl.DSndAFIFOReset             = 1;

   snd_ctrl.EnableDSndBToRightSpeaker  = 0;
   snd_ctrl.EnableDSndBToLeftSpeaker   = 0;
   snd_ctrl.DSndBSampleRateTimer       = 0;   // Timer 0
   snd_ctrl.DSndBFIFOReset             = 0;

   ///////////////////////////////////////////////////////////////////////////////
   // Play a mono sound at PLAYBACK_FREQ
   // Uses timer 0 as sampling rate source
   // Uses timer 1 to count the samples played in order to stop the sound

   // REG_SOUNDCNT_H = 0x0b0F; //enable DS A&B + fifo reset + use timer0 + max volume to L and R

   REG_SOUNDCNT_H = *(u16*) &snd_ctrl;
   REG_SOUNDCNT_X = 0x0080; // Turn sound chip on

   // Clear the FIFO
   // u32* fifo_a = (u32*) FIFO_A;
   // *fifo_a = 0;

   // Direct Sound Channel A
   REG_DMA1SAD   = (unsigned long) SoundBuffer;   // DMA1 source
   REG_DMA1DAD   = FIFO_A;                     // Write to FIFO A address
   REG_DMA1CNT_H = 0xb600;                     // DMA control: DMA enabled+ start on FIFO+32bit+repeat+increment source&dest
   REG_DMA1CNT_L = 0;

   REG_TM1CNT_L  = 0xFFFF - SOUND_BUFFER_SIZE;      // 0xffff-the number of samples to play
   REG_TM1CNT_H  = 0xC4;                     // Enable Timer1 + IRQ and cascade from Timer0

   REG_IE        = 0x10;   // Enable IRQ for timer 1
   REG_IME       = 1;      // Master enable interrupts

   // Formula for playback frequency is: 0xFFFF-round(cpuFreq/PLAYBACK_FREQ)
   // REG_TM0CNT_L = 0xFBE8; //16khz playback freq
   // REG_TM0CNT_L = 0xFD07; // 22.05 khz playback freq

   REG_TM0CNT_L = 0xFFFF - (cpuFreq/PLAYBACK_FREQ);
   REG_TM0CNT_H = 0x0080;  // Enable Timer0
}

void AgbMain (void)
{
   InitSoundSystem();

   SetMode( MODE_3 | BG2_ENABLE );

   while(1)
   {
      if(!(*KEYS & KEY_A))
      {
         while(!(*KEYS & KEY_A));

         sbInsertSample((const s8*) SOUND21_DATA, SOUND21_LENGTH, SOUND_BUFFER_SIZE - 3000 );
         // sbInsertSampleDMA((const s8*) SOUND21_DATA, SOUND21_LENGTH);
      }
      else if(!(*KEYS & KEY_B))
      {
         while(!(*KEYS & KEY_B));

         sbClear();      
      }
   }
}



All of the sound-related functions start with sb prefix. The program uses double buffering. I am planning to rewrite some of the functions in ASM once I am happy with the overall implementation. Right now the program output is mono, but in the next version I will double the buffers and implement a stereo output. The next step will be to add some FX functions as delay, reverb and echo. Please let me know if you think there is a way to optimize the code and/or if you find any errors.

Thanks

#1633 - xonox - Mon Jan 20, 2003 9:20 pm

The easiest way to mix two samples is to take them one byte at a time.
A slow but working way is to divide both bytes by two (to lower their volume to prevent clipping) and then add them. It's a simple way to do that and i can't put in more details 'cause i am writing this from my break at work. I'll write again later if i find better ideas.

#1637 - Vortex - Mon Jan 20, 2003 9:53 pm

xonox wrote:
The easiest way to mix two samples is to take them one byte at a time.
A slow but working way is to divide both bytes by two (to lower their volume to prevent clipping) and then add them. It's a simple way to do that and i can't put in more details 'cause i am writing this from my break at work. I'll write again later if i find better ideas.


xonox,

You are right - the described way of mixing works very well. A primitive delay was included as well. Here is the updated code:

Code:

/////////////////////////////////////////////////////////////////////////////
// sound.c
// ver: 0.5.1

#include "gba.h"
#include "screenmode.h"
#include "keypad.h"
#include "interrupt.h"

#include "sound21.c"
#include "sound25.c"

///////////////////////////////////////////////////////////////////////////////
// Sound Control

#define FIFO_A (0x040000A0)
#define FIFO_B (0x040000A4)

struct SountCntH
{
   int Ch1to4OutputRatio          : 2;
   int DSndAOutRatio              : 1;
   int DSndBOutRatio              : 1;
   int unused                     : 4;

   int EnableDSndAToRightSpeaker  : 1;
   int EnableDSndAToLeftSpeaker   : 1;
   int DSndASampleRateTimer       : 1;
   int DSndAFIFOReset             : 1;

   int EnableDSndBToRightSpeaker  : 1;
   int EnableDSndBToLeftSpeaker   : 1;
   int DSndBSampleRateTimer       : 1;
   int DSndBFIFOReset             : 1;

};

///////////////////////////////////////////////////////////////////////////////
// Sound Buffer Definitions and Prototypes

#define PLAYBACK_FREQ     (16384)

// Allocating 2 secs sound buffer
#define SOUND_BUFFER_SIZE (PLAYBACK_FREQ*2)

#define MAX_NUMBER_OF_SOUNDS_MIXED 2

s8  SoundBuffer1[SOUND_BUFFER_SIZE] __attribute__ ((section (".ewram"))) = { 0 };
s8  SoundBuffer2[SOUND_BUFFER_SIZE] __attribute__ ((section (".ewram"))) = { 0 };

volatile s8* SoundBuffer        __attribute__ ((section (".ewram"))) = SoundBuffer1;
volatile u8  activeSoundBuffer  __attribute__ ((section (".ewram"))) = 0;
volatile u32 currentSample      __attribute__ ((section (".ewram"))) = 0;
 
void sbClearBuffer(u32* buffer)  __attribute__ ((section(".iwram")));
void sbClearAll(void);
void sbClear(void);
void sbFlip(void) __attribute__ ((section(".iwram")));
void sbInsertSample( const s8* sound_data, u16 sound_data_size, u16 offset );

void InterruptProcess(void) __attribute__ ((section(".iwram"))); //the interrupt handler from crt0.s

///////////////////////////////////////////////////////////////////////////////
// Interrupt Handler

void InterruptProcess(void)
{
   if(REG_IF & IF_TIMER1_OVERFLOW)
   {
      if( activeSoundBuffer == 0 )
         sbClearBuffer((u32*) SoundBuffer1 );
      else
         sbClearBuffer((u32*) SoundBuffer2 );

      sbFlip();

      // Init DMA1 Source
      REG_DMA1SAD = (unsigned long) SoundBuffer; // DMA1 source

      currentSample = 0;
   }
   else if(REG_IF & IF_TIMER0_OVERFLOW)
      currentSample ++;

   // Clear the interrupt(s)
   REG_IF |= REG_IF;
}

///////////////////////////////////////////////////////////////////////////////
// Sound Buffer Functions

void sbFlip(void)
{
   if( activeSoundBuffer == 0 )
   {
      SoundBuffer = SoundBuffer2;
      activeSoundBuffer = 1;
   }
   else
   {
      SoundBuffer = SoundBuffer1;
      activeSoundBuffer = 0;
   }
}

void sbClear(void)
{
   u32* buffer = (u32*) SoundBuffer;
   u32 i;

   for( i = 0; i < SOUND_BUFFER_SIZE / 4; i++ )
      buffer[i] = 0;
}

void sbClearBuffer(u32* buffer)
{
   u32 i;

   for( i = 0; i < SOUND_BUFFER_SIZE / 4; i++ )
      buffer[i] = 0;
}

void sbClearAll(void)
{
   u32* buffer1;
   u32* buffer2;
   u32 i;

   buffer1 = (u32*) SoundBuffer1;
   buffer2 = (u32*) SoundBuffer2;

   for( i = 0; i < SOUND_BUFFER_SIZE / 4; i++ )
   {
      buffer1[i] = 0;
      buffer2[i] = 0;
   }
}

void sbInsertSampleDelay( const s8* sound_data, u16 sound_data_size, u16 offset, u16 delay )
{
   sbInsertSample( sound_data, sound_data_size, offset );
   sbInsertSample( sound_data, sound_data_size, offset + delay );
}

// volume 0-255
void sbInsertSampleVolume( const s8* sound_data, u16 sound_data_size, u16 offset, u8 volume )
{
   u32 i;
   u16 pos = offset;

   s8*  Buffer;
   s8*  BackBuffer;

   // The sample is too large or Invalid offset
   if( sound_data_size > SOUND_BUFFER_SIZE || offset > SOUND_BUFFER_SIZE )
      return;
   
   if(activeSoundBuffer == 0)
   {
      Buffer      = SoundBuffer1;
      BackBuffer  = SoundBuffer2;
   }
   else
   {
      Buffer      = SoundBuffer2;
      BackBuffer  = SoundBuffer1;
   }


   for( i = 0; i < sound_data_size; i++ )
   {
      Buffer[pos] += ((sound_data[i] * volume) / 255);

      if( ++pos == SOUND_BUFFER_SIZE )
      {
         pos = 0;
         Buffer = BackBuffer;
      }
   }
}

void sbInsertSample( const s8* sound_data, u16 sound_data_size, u16 offset )
{
   u32 i;
   u16 pos = offset;

   s8*  Buffer;
   s8*  BackBuffer;

   // The sample is too large or Invalid offset
   if( sound_data_size > SOUND_BUFFER_SIZE || offset > SOUND_BUFFER_SIZE )
      return;
   
   if(activeSoundBuffer == 0)
   {
      Buffer      = SoundBuffer1;
      BackBuffer  = SoundBuffer2;
   }
   else
   {
      Buffer      = SoundBuffer2;
      BackBuffer  = SoundBuffer1;
   }


   for( i = 0; i < sound_data_size; i++ )
   {
      Buffer[pos] += sound_data[i] / MAX_NUMBER_OF_SOUNDS_MIXED;

      if( ++pos == SOUND_BUFFER_SIZE )
      {
         pos = 0;
         Buffer = BackBuffer;
      }
   }
}

u32 sbGetCurrentPosition(void)
{
   return currentSample;
}

//////////////////////////////////////////////////////////////////
// Resources Used: Timer0, Timer1, DMA1
void sbInitSoundSystem()
{
   const u32 cpuFreq = 16777216;
   struct SountCntH snd_ctrl;

   currentSample     = 0;
   activeSoundBuffer = 0;
   SoundBuffer       = SoundBuffer1;

   sbClearAll();

   snd_ctrl.Ch1to4OutputRatio          = 0;
   snd_ctrl.DSndAOutRatio              = 1;   // 100%
   snd_ctrl.DSndBOutRatio              = 0;   
   snd_ctrl.unused                     = 0;

   snd_ctrl.EnableDSndAToRightSpeaker  = 1;
   snd_ctrl.EnableDSndAToLeftSpeaker   = 1;
   snd_ctrl.DSndASampleRateTimer       = 0;   //Timer 0
   snd_ctrl.DSndAFIFOReset             = 1;

   snd_ctrl.EnableDSndBToRightSpeaker  = 0;
   snd_ctrl.EnableDSndBToLeftSpeaker   = 0;
   snd_ctrl.DSndBSampleRateTimer       = 0;   // Timer 0
   snd_ctrl.DSndBFIFOReset             = 0;

   ///////////////////////////////////////////////////////////////////////////////
   // Play a mono sound at PLAYBACK_FREQ
   // Uses timer 0 as sampling rate source
   // Uses timer 1 to count the samples played in order to stop the sound

   // REG_SOUNDCNT_H = 0x0b0F; //enable DS A&B + fifo reset + use timer0 + max volume to L and R

   REG_SOUNDCNT_H = *(u16*) &snd_ctrl;
   REG_SOUNDCNT_X = 0x0080; // Turn sound chip on

   // Clear the FIFO
   // u32* fifo_a = (u32*) FIFO_A;
   // *fifo_a = 0;

   // Direct Sound Channel A
   REG_DMA1SAD   = (unsigned long) SoundBuffer;   // DMA1 source
   REG_DMA1DAD   = FIFO_A;                     // Write to FIFO A address
   REG_DMA1CNT_H = 0xb600;                     // DMA control: DMA enabled+ start on FIFO+32bit+repeat+increment source&dest
   REG_DMA1CNT_L = 0;

   REG_TM1CNT_L  = 0xFFFF - SOUND_BUFFER_SIZE;      // 0xffff-the number of samples to play
   REG_TM1CNT_H  = 0xC4;                     // Enable Timer1 + IRQ and cascade from Timer0

   REG_IE        = 0x18;   // Enable IRQ for timer 1 and timer 0
//  REG_IE        = 0x10;   // Enable IRQ for timer 1
   REG_IME       = 1;      // Master enable interrupts

   // Formula for playback frequency is: 0xFFFF-round(cpuFreq/PLAYBACK_FREQ)
   // REG_TM0CNT_L = 0xFBE8; //16khz playback freq
   // REG_TM0CNT_L = 0xFD07; // 22.05 khz playback freq

   REG_TM0CNT_L = 0xFFFF - (cpuFreq/PLAYBACK_FREQ);
//  REG_TM0CNT_H = 0x0080;  // Enable Timer0
   REG_TM0CNT_H = 0x00C0;  // Enable Timer0 + IRQ
}

void AgbMain (void)
{
   u8 volume = 127;

   sbInitSoundSystem();

   SetMode( MODE_3 | BG2_ENABLE );

   while(1)
   {
      if(!(*KEYS & KEY_A))
      {
         while(!(*KEYS & KEY_A));

         sbInsertSample((const s8*) SOUND21_DATA, SOUND21_LENGTH, currentSample );      
      }
      else if(!(*KEYS & KEY_B))
      {
         while(!(*KEYS & KEY_B));

         sbInsertSample((const s8*) SOUND25_DATA, SOUND25_LENGTH, currentSample );
      }
      else if(!(*KEYS & KEY_L))
      {
         while(!(*KEYS & KEY_L));

         // sbInsertSampleDelay((const s8*) SOUND21_DATA, SOUND21_LENGTH, currentSample, 5000 );      
         sbInsertSampleVolume((const s8*) SOUND21_DATA, SOUND21_LENGTH, currentSample, volume );      
      }
      else if(!(*KEYS & KEY_UP))
      {
         while(!(*KEYS & KEY_UP));
         volume++;
      }
      else if(!(*KEYS & KEY_DOWN))
      {
         while(!(*KEYS & KEY_DOWN));
         volume--;
      }
   }
}

#1685 - NEiM0D - Tue Jan 21, 2003 4:24 pm

Great, what you could also have done is look at my NMOD source, there's a C version included.

#1690 - col - Tue Jan 21, 2003 4:57 pm

Also check out this link

http://www.oxygen.it.net.au/mixing/

Its a tutorial on writing a mixer, it's intel based code, and some of the optimization tips are not relevant, but its a very good overview.

Check out the 'volumetable' idea(section5) as a simple yet very effective way to post process the volume to prevent noticable clipping - especially with more channels.

cheers

col

#1705 - tepples - Tue Jan 21, 2003 8:19 pm

col wrote:
Check out the 'volumetable' idea(section5) as a simple yet very effective way to post process the volume

That's efficient on hardware with fast memory and slow multiply instructions, but a lookup to a "volume table" in ROM costs more cycles than a simple ARM 'mul' instruction (3 cycles for 'mul' with an 8-bit multiplier, 4 cycles for 'mla').

Quote:
to prevent noticable clipping - especially with more channels.

Doing this through a table is also inefficient because clipping with ARM's conditional move instructions (four cycles) is just as fast as going through a table.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#1706 - NEiM0D - Tue Jan 21, 2003 8:22 pm

Not unless the table is in iwram

#1710 - col - Tue Jan 21, 2003 9:18 pm

tepples wrote:
col wrote:
Check out the 'volumetable' idea(section5) as a simple yet very effective way to post process the volume

That's efficient on hardware with fast memory and slow multiply instructions, but a lookup to a "volume table" in ROM costs more cycles than a simple ARM 'mul' instruction (3 cycles for 'mul' with an 8-bit multiplier, 4 cycles for 'mla').

Quote:
to prevent noticable clipping - especially with more channels.

Doing this through a table is also inefficient because clipping with ARM's conditional move instructions (four cycles) is just as fast as going through a table.


...erm, sorry my mistake - it's a while since i read that article, and i just skimmed it this time - what i'm refering to is not the volumetable but the 'post processing' table - (I call it 'clip-table').

cheers

col.