GameCube audio back end, format conversion

Hello,

I’m in the middle of writing a GameCube back end for SDL (1.2) and I
think I need some advice regarding the audio system.

First, I had better write some info about how the GameCube’s audio
system is exposed in the homebrew API:

The GC only supports two sample rates - 32 and 48KHz, and samples are
always stereo, signed 16 bits and big endian. It operates using DMA,
where I supply a callback function and it’ll call that during an
interrupt when more audio is needed to fill one of its buffers.

Because the GC doesn’t need threads to supply audio, I’m writing my
back end in a way that the user’s callback will get called during my
DMA callback. Is this safe? (I may have to go for threading if it
turns out people put lots of processing in their mix callbacks.)

I’m using the loopwave sample to help me debug the back end, and
during my callback I’m converting audio to the GC format using the
SDL_AudioCVT at current_audio->convert.

What I’m seeing though, is that while samples are being converted to
big endian and from mono to stereo, no rate conversion is being done
(22050 -> 32000).

I’m also seeing that the data length after conversion is not the same
as the length I put in the spec during my OpenAudio function. As far
as I can tell, it’s half.

So I guess the real question is, should I use current_audio->convert
to convert my audio? I would have thought so (SDL_RunAudio does), but
it doesn’t seem to do the conversion properly. Maybe I’m not setting
something up right.

If it’s possible I would love to have someone peek at my source code
to see if I’m doing anything wrong. I’ve attached it to this mail.

Thanks for your time,
Peter
-------------- next part --------------
/*
SDL - Simple DirectMedia Layer
Copyright © 1997-2006 Sam Lantinga

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

Sam Lantinga
slouken at libsdl.org

*/
#include “SDL_config.h”

// Public includes.
#include “SDL_timer.h”

// Audio internal includes.
#include “SDL_audio.h”
#include “…/SDL_audiomem.h”
#include “…/SDL_sysaudio.h”
#include “…/SDL_audio_c.h”

// GameCube audio internal includes.
#include <ogc/audio.h>
#include <ogc/cache.h>
#include “SDL_gamecubeaudio.h”

#define ALIGNED(x) attribute((aligned(x)));
#define SAMPLES_PER_DMA_BUFFER 8192

typedef Uint32 Sample;
typedef Sample DMABuffer[SAMPLES_PER_DMA_BUFFER];

static const char GAMECUBEAUD_DRIVER_NAME[] = “gamecube”;
static DMABuffer dma_buffers[2] ALIGNED(32);
static size_t current_dma_buffer = 0;
static Sample silence[SAMPLES_PER_DMA_BUFFER] ALIGNED(32);

// Called whenever more audio data is required.
static void StartDMA(void)
{
// Is the device ready?
if (current_audio && (!current_audio->paused))
{
DMABuffer* const dma_buffer = &dma_buffers[current_dma_buffer];

	// Is conversion required?
	if (current_audio->convert.needed)
	{
		// Dump out the conversion info.
		printf("----\n");
		printf("conversion is needed\n");
		printf("\tsrc_format = 0x%x\n", current_audio->convert.src_format);
		printf("\tdst_format = 0x%x\n", current_audio->convert.dst_format);
		printf("\trate_incr  = %f\n", (float) current_audio->convert.rate_incr);
		printf("\tbuf        = 0x%08x\n", current_audio->convert.buf);
		printf("\tlen        = %d\n", current_audio->convert.len);
		printf("\tlen_cvt    = %d\n", current_audio->convert.len_cvt);
		printf("\tlen_mult   = %d\n", current_audio->convert.len_mult);
		printf("\tlen_ratio  = %f\n", (float) current_audio->convert.len_ratio);

		// Get the client to produce audio.
		current_audio->spec.callback(
			current_audio->spec.userdata,
			current_audio->convert.buf,
			current_audio->convert.len);

		// Convert the audio.
		SDL_ConvertAudio(&current_audio->convert);

		// Sanity check.
		if (sizeof(DMABuffer) != current_audio->convert.len_cvt)
		{
			printf("The size of the DMA buffer (%u) doesn't match the converted buffer (%u)\n",
				sizeof(DMABuffer), current_audio->convert.len_cvt);
		}

		// Copy from SDL buffer to DMA buffer.
		memcpy(&(*dma_buffer)[0], current_audio->convert.buf, current_audio->convert.len_cvt);
	}
	else
	{
		printf("conversion is not needed\n");

		// Not implemented yet.
		memset(&(*dma_buffer)[0], 0, sizeof(DMABuffer));
	}

	// Set up the DMA.
	AUDIO_InitDMA((Uint32) &(*dma_buffer)[0], sizeof(DMABuffer));

	// Flush the data cache.
	DCFlushRange(&(*dma_buffer)[0], sizeof(DMABuffer));

	// Use the other DMA buffer next time.
	current_dma_buffer = 1 - current_dma_buffer;
}
else
{
	// Set up the DMA.
	AUDIO_InitDMA((Uint32) silence, sizeof(silence));
}

// Start the DMA.
AUDIO_StartDMA();

}

static int GAMECUBEAUD_OpenAudio(_THIS, SDL_AudioSpec *spec)
{
// Set up actual spec.
spec->freq = 32000;
spec->format = AUDIO_S16MSB;
spec->channels = 2;
spec->samples = SAMPLES_PER_DMA_BUFFER;
spec->padding = 0;
SDL_CalculateAudioSpec(spec);

// Initialise the GameCube side of the audio system.
AUDIO_Init(0);
AUDIO_SetDSPSampleRate(AI_SAMPLERATE_32KHZ);
AUDIO_RegisterDMACallback(StartDMA);

// Start the first chunk of audio playing.
StartDMA();

// Magic number which indicates that an audio thread is not required.
return 2;

}

static void GAMECUBEAUD_CloseAudio(_THIS)
{
// Forget the DMA callback.
AUDIO_RegisterDMACallback(0);

// Stop any DMA going on.
AUDIO_StopDMA();

}

static void GAMECUBEAUD_DeleteDevice(SDL_AudioDevice *device)
{
SDL_free(device->hidden);
SDL_free(device);
}

static SDL_AudioDevice *GAMECUBEAUD_CreateDevice(int devindex)
{
SDL_AudioDevice *this;

/* Initialize all variables that we clean on shutdown */
this = (SDL_AudioDevice *)SDL_malloc(sizeof(SDL_AudioDevice));
if ( this ) {
	SDL_memset(this, 0, (sizeof *this));
	this->hidden = (struct SDL_PrivateAudioData *)
			SDL_malloc((sizeof *this->hidden));
}
if ( (this == NULL) || (this->hidden == NULL) ) {
	SDL_OutOfMemory();
	if ( this ) {
		SDL_free(this);
	}
	return(0);
}
SDL_memset(this->hidden, 0, (sizeof *this->hidden));

/* Set the function pointers */
this->OpenAudio = GAMECUBEAUD_OpenAudio;
this->CloseAudio = GAMECUBEAUD_CloseAudio;
this->free = GAMECUBEAUD_DeleteDevice;

return this;

}

static int GAMECUBEAUD_Available(void)
{
return 1;
}

AudioBootStrap GAMECUBEAUD_bootstrap = {
GAMECUBEAUD_DRIVER_NAME, “SDL GameCube audio driver”,
GAMECUBEAUD_Available, GAMECUBEAUD_CreateDevice
};

A Gamecube backend? Sweet. :slight_smile:

Because the GC doesn’t need threads to supply audio, I’m writing my
back end in a way that the user’s callback will get called during my
DMA callback. Is this safe? (I may have to go for threading if it
turns out people put lots of processing in their mix callbacks.)

Look at what the Mac OS 9 target does. It fires the SDL audio callback
in a hardware interrupt, and takes some effort to deal with long-running
callbacks, without using real threads for any of it.

http://www.libsdl.org/cgi/viewvc.cgi?view=rev&revision=579

Look for “SDL_LockAudio on MacOS” emails in the mailing list archives
around March 2002 for all the discussion about how this works.

What I’m seeing though, is that while samples are being converted to
big endian and from mono to stereo, no rate conversion is being done
(22050 -> 32000).

SDL 1.2 will only resample audio if it’s a power of two (11025 -> 22050
-> 44100, etc, but not 44100 -> 48000). If you have to resample to
something else, bad things happen.

This is long overdue to be fixed, and will be fixed for SDL 1.3.

If it’s possible I would love to have someone peek at my source code
to see if I’m doing anything wrong. I’ve attached it to this mail.

In this case, it’s probably 100% not your bug. :confused:

–ryan.

Hello,

Thank you Ryan for your detailed reply.

A Gamecube backend? Sweet. :slight_smile:

Yes I hope with the release of the Wii that GameCube homebrew will see
a bit of a revival. The toolchain is pretty much all there now. (For
anyone interested, I should be releasing a Quake port inside a week’s
time. Probably not using SDL unfortunately.)

Look at what the Mac OS 9 target does. It fires the SDL audio callback
in a hardware interrupt, and takes some effort to deal with long-running
callbacks, without using real threads for any of it.

http://www.libsdl.org/cgi/viewvc.cgi?view=rev&revision=579

Look for “SDL_LockAudio on MacOS” emails in the mailing list archives
around March 2002 for all the discussion about how this works.

Thanks, I’ll do that.

SDL 1.2 will only resample audio if it’s a power of two (11025 -> 22050
-> 44100, etc, but not 44100 -> 48000). If you have to resample to
something else, bad things happen.

Ahh I was afraid of that.

This is long overdue to be fixed, and will be fixed for SDL 1.3.

That would be cool. In the mean time, what would you say my best
option would be?

I could tell SDL that I’m using the sample rate it suggested but go on
to resample the audio when I write to the DMA buffer, but it may be
tricky for me to guarantee that buffer sizes are both still a power of
two for SDL and a multiple of 32 bytes for the GameCube.

Hmm, or maybe I can write an equation to figure out the buffer sizes,
I’ll have a think.

If it’s possible I would love to have someone peek at my source code
to see if I’m doing anything wrong. I’ve attached it to this mail.

In this case, it’s probably 100% not your bug. :confused:

Ah, well thanks again for looking!

Pete