Wednesday, February 8, 2012

Android OpenSL audio latency woes...

I've finally got around to converting iPhone HiHowAreYou to Android (although this is actually going to be HiHowAreYou2 for iPad/iPhone/Android!).

For faster frame rate on the iPhone (OpenAL audio mixer is very easy to beat with some asm mixing) I wrote my own audio mixer that simply fills in a buffer which I just send to a low level / latency audio unit on the iPhone. The nearest thing on Android is the OpenSL system. It's pretty easy to setup a similar buffer scheme, but the problem is the Kindle Fire has really bad audio latency with this method (almost 0.5 seconds). It looks really bad in the game when you tap the jump button and the jump sound plays 0.5 seconds later! Has anyone else found a good work around for this problem on the Kindle Fire? The latency is much less on the Samsung Galaxy Mp3 Player 5.0, but I'd like my audio to be good on all Android devices - and the Kindle fire is quite powerful so I was surprised by the poor audio performance!

Here's my current android audio driver. Pretty simple code really, but no matter what I set BUFFER_COUNT or BUFFER_SAMPLES, or the output mix playback rate, the latency is always 0.5 seconds :(

//==============================================================================
//
// Audio_ANDROID.cpp
//
//==============================================================================

//==============================================================================
// INCLUDES
//==============================================================================

// Engine
#include "xEnginePCH.h"
#include "xAudio.h"
#include "xAudio_Defines.h"

// Android native audio
#include
#include

//==============================================================================
// DEFINES
//==============================================================================

#define BUFFER_COUNT 2
#define BUFFER_SAMPLES 512
#define BUFFER_SIZE (BUFFER_SAMPLES * 2 * 2) // 16 bit, stereo

//==============================================================================
// DATA
//==============================================================================

// Engine interfaces
static SLObjectItf m_EngineObjectItf = NULL;
static SLEngineItf m_EngineItf = NULL;

// Output mix interface
static SLObjectItf m_OutputMixItf = NULL;

// Buffer queue player interfaces
static SLObjectItf m_PlayerObjectItf = NULL;
static SLPlayItf m_PlayItf = NULL;
static SLAndroidSimpleBufferQueueItf m_BufferQueueItf = NULL;

// Buffer data
static xU32 m_NextBuffer = 0;
static xBYTE m_Buffers[BUFFER_COUNT][BUFFER_SIZE] = {0};

//==============================================================================
// MIXING
//==============================================================================

// this callback handler is called every time a buffer finishes playing
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
SLresult result;

ASSERT(bq == m_BufferQueueItf);
ASSERT(NULL == context);
// Fill buffer that has just finished being played
xS16* pBuffer = (xS16*)&m_Buffers[m_NextBuffer];
xAudio::Mix( pBuffer, BUFFER_SAMPLES );

// Enqueue buffer
result = (*m_BufferQueueItf)->Enqueue(m_BufferQueueItf, pBuffer, BUFFER_SIZE);
ASSERT(SL_RESULT_SUCCESS == result);

// Update buffer index that is now playing
m_NextBuffer = (m_NextBuffer+1) & (BUFFER_COUNT-1);
}

//==============================================================================
// FUNCTIONS
//==============================================================================

// Initializes
xBOOL xAudio::Hardware_Init( void )
{
SLresult result;
//==========================================================================
// Create sound engine
//==========================================================================

// Create engine
result = slCreateEngine(&m_EngineObjectItf, 0, NULL, 0, NULL, NULL);
ASSERT(SL_RESULT_SUCCESS == result);

// Realize the engine
result = (*m_EngineObjectItf)->Realize(m_EngineObjectItf, SL_BOOLEAN_FALSE);
ASSERT(SL_RESULT_SUCCESS == result);

// Get the engine interface, which is needed in order to create other objects
result = (*m_EngineObjectItf)->GetInterface(m_EngineObjectItf, SL_IID_ENGINE, &m_EngineItf);
ASSERT(SL_RESULT_SUCCESS == result);

//==========================================================================
// Create output mix
//==========================================================================

// Create output mix, with environmental reverb specified as a non-required interface
result = (*m_EngineItf)->CreateOutputMix(m_EngineItf, &m_OutputMixItf, 0, NULL, NULL);
ASSERT(SL_RESULT_SUCCESS == result);

// Realize the output mix
result = (*m_OutputMixItf)->Realize(m_OutputMixItf, SL_BOOLEAN_FALSE);
ASSERT(SL_RESULT_SUCCESS == result);

//==========================================================================
// Create buffer queue audio player
//==========================================================================

// Configure audio source
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, BUFFER_COUNT};
SLDataFormat_PCM format_pcm =
{ SL_DATAFORMAT_PCM, // formatType
2, // numChannels
SL_SAMPLINGRATE_44_1, // samplesPerSec
SL_PCMSAMPLEFORMAT_FIXED_16, // bitsPerSample
SL_PCMSAMPLEFORMAT_FIXED_16, // containerSize
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // channelMask
SL_BYTEORDER_LITTLEENDIAN // endianness
};
SLDataSource audioSrc = {&loc_bufq, &format_pcm};

// Configure audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, m_OutputMixItf};
SLDataSink audioSnk = {&loc_outmix, NULL};

// Create audio player
const SLInterfaceID ids[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
result = (*m_EngineItf)->CreateAudioPlayer(m_EngineItf, &m_PlayerObjectItf, &audioSrc, &audioSnk, 1, ids, req);
ASSERT(SL_RESULT_SUCCESS == result);

// Realize the player
result = (*m_PlayerObjectItf)->Realize(m_PlayerObjectItf, SL_BOOLEAN_FALSE);
ASSERT(SL_RESULT_SUCCESS == result);

// Get the play interface
result = (*m_PlayerObjectItf)->GetInterface(m_PlayerObjectItf, SL_IID_PLAY, &m_PlayItf);
ASSERT(SL_RESULT_SUCCESS == result);

// Get the buffer queue interface
result = (*m_PlayerObjectItf)->GetInterface(m_PlayerObjectItf, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &m_BufferQueueItf);
ASSERT(SL_RESULT_SUCCESS == result);

// Register callback on the buffer queue
result = (*m_BufferQueueItf)->RegisterCallback(m_BufferQueueItf, bqPlayerCallback, NULL);
ASSERT(SL_RESULT_SUCCESS == result);

// Enqueue all buffers
for( xS32 i = 0; i < BUFFER_COUNT; i++ )
{
result = (*m_BufferQueueItf)->Enqueue(m_BufferQueueItf, &m_Buffers[i], BUFFER_SIZE);
ASSERT(result == SL_RESULT_SUCCESS);
}

// Start playing
result = (*m_PlayItf)->SetPlayState(m_PlayItf, SL_PLAYSTATE_PLAYING);
ASSERT(SL_RESULT_SUCCESS == result);

return TRUE;
}

//==============================================================================

// Closes down audio
void xAudio::Hardware_Kill( void )
{
// Destroy buffer queue audio player object, and invalidate all associated interfaces
if(m_PlayerObjectItf != NULL)
{
(*m_PlayerObjectItf)->Destroy(m_PlayerObjectItf);
m_PlayerObjectItf = NULL;
m_PlayItf = NULL;
m_BufferQueueItf = NULL;
}

// Destroy output mix object, and invalidate all associated interfaces
if(m_OutputMixItf != NULL)
{
(*m_OutputMixItf)->Destroy(m_OutputMixItf);
m_OutputMixItf = NULL;
}

// Destroy engine object, and invalidate all associated interfaces
if(m_EngineObjectItf != NULL)
{
(*m_EngineObjectItf)->Destroy(m_EngineObjectItf);
m_EngineObjectItf = NULL;
m_EngineItf = NULL;
}
}

//==============================================================================

void xAudio::Hardware_Update( const xF32 )
{
// Nothing to do for Android
}

//==============================================================================
// Interruption
//==============================================================================

void xAudio::Hardware_BeginInterruption( void )
{
SLresult result;

// Pause playing
result = (*m_PlayItf)->SetPlayState(m_PlayItf, SL_PLAYSTATE_PAUSED);
ASSERT(SL_RESULT_SUCCESS == result);
}

//==============================================================================

void xAudio::Hardware_EndInterruption( void )
{
SLresult result;

// Resume playing
result = (*m_PlayItf)->SetPlayState(m_PlayItf, SL_PLAYSTATE_PLAYING);
ASSERT(SL_RESULT_SUCCESS == result);
}

//==============================================================================

Saturday, December 12, 2009

My first post!

For the last year or so I've running my own independent studio www.SmashingStudios.com and have been developing games on the iPhone which has been a very interesting experience.

It is a very nice platform to quickly get ramped up on. All you need is a mac mini, an ipod-touch (or/and iphone), and a $99 to become a registered developer. And that's it.

My first project on the iPhone was creating a Sega Genesis/Megadrive Emulator which has been used to ship the classic Sega titles such as Sonic the Hedgehog, Golden Axe, Streets of Rage, etc with more coming soon.

My next two titles were independently created with Pete Franco from www.DrFunFun.com
"Hi How Are You" - a physics platformer based on the art and music of indie artist Daniel Johnston, and "A Frantic Santa Situation" which is a fun race again the clock physics game.

Over the course of this blog I'll talk about the challenges of each project and I'd be happy to go into any of the tech specifics.


-Steve.