Showing posts with label OpenSL ES. Show all posts
Showing posts with label OpenSL ES. Show all posts

Sunday, October 27, 2013

Shards - Work In Progress II



 Here comes our second and last work in progress video. The last one as we are almost finished. The game currently runs on Android, bada and even Tizen - new upcoming Linux based mobile OS.


 Compare our progress with previous WIP video here.

 The game uses our own small cross-platform engine. Beside above mentioned mobile platforms it also runs on desktop Windows thanks to OpenGL ES simulation libraries from Imagination Technologies. For physics simulation it uses great Box2D library.

 And yes ...! That great music is also from Shards.





Sunday, March 10, 2013

Shards - Work In Progress I



This is short info on our upcoming game - Shards - we are working on. The game will be clone of well known arcade classic with glassy look and very nice fractal backgrounds. The music in video is just a placeholder I found on the web - it sounds to make me sure the sound engine works.


 Making the game we are using these technologies:
  • box2D for physics,
  • notice the usage of our universal box2D debug draw that works for Open GL ES 1.x and 2.0,
  • our cross platform engine -  the video is taken from desktop Windows while the game runs on Android as well,
  • PicOpt - our sprite atlas creation tool that is free to download,
  • for Android NDK: textures are loaded through this way, while sound (not too much of them in the game presently) is coded like this,
  • for ads we plan to utilize our simple ads manager.

Monday, February 4, 2013

Simple streaming audio mixer for Android with OpenSL ES - part 2




Previous parts
 This is second part of small tutorial how to build simple streaming audio mixer for Android using OpenSL ES. In first part we initialize OpenSL Engine and created AudioPlayer. We also set callback routine that gets called when playing of buffer with sound data is finished. What we have to do now is to implement prepareSoundBuffer() and swapSoundBuffers() routines as well as some routine (PlaySound) that will initiate new sounds.

The Logic


 If you remember we cleared and sent two sound buffers when initializing the AudioOutput:
 // prepare mixer and enqueue 2 buffers
 // clear buffers
 memset(mAudioOutSoundData1, 0, sizeof(s16) * SBC_AUDIO_OUT_BUFFER_SIZE);
 memset(mAudioOutSoundData2, 0, sizeof(s16) * SBC_AUDIO_OUT_BUFFER_SIZE);
 // point to first one
 mActiveAudioOutSoundBuffer = mAudioOutSoundData1;

 // send two buffers
 sendSoundBuffer();
 sendSoundBuffer();

The logic is like this:
 1) in the beginning we send two buffers,
 2) as we set callback we are informed when playing of the first buffer is finished,
 3) when callback is called we fill the finished buffer with new data while the second buffer is currently being played,
 4) we enque buffer with new data and all the process repeats from 2).


Mixer


 As the name says the mixer mix something. Sounds in our case. How many sounds it can mix together depends on number of channels we create. The channel data structure looks like this:
typedef struct sSoundInfo
{
 bool mUsed;
 u16 mSoundID;
 s16* mData;
 u32 mLength;
 u32 mPosition;
 u32 mStarted;
} SoundInfo;

 meaning of the variables is like this:
mUsed - channel is used (not free for playing sound),
mSoundID - any ID you consider as useful for your game. It helps you to identify which channel is playing which sound and do something with it (like stop storm and flash sounds when storm is over ...),
mData - pointer to raw PCM data,
mLength - length of the sound data,
mPosition - current position within sound data (offset from start)
mStarted - time when sound started in milliseconds

the mixer structure then looks like this: 
 // mixer
 s32 mTmpSoundBuffer[SBC_AUDIO_OUT_BUFFER_SIZE * 4];
 s16 mAudioOutSoundData1[SBC_AUDIO_OUT_BUFFER_SIZE * 2];
 s16 mAudioOutSoundData2[SBC_AUDIO_OUT_BUFFER_SIZE * 2];
 s16* mActiveAudioOutSoundBuffer;
 // mixer channels
 SoundInfo mSounds[SBC_AUDIO_OUT_CHANNELS];

 Here you can see two buffers we are speaking about - mAudioOutSoundData1, mAudioOutSoundData2. There is also 1 temporary buffer mTmpSoundBuffer with twice as much bit width than needed for 16 bit sound. It will be clear shortly what is its purpose. Next, there is pointer to currently active buffer from the two of them. Writing data will be done into the buffer this pointer points to. After writing the data the pointer will switch to second one. The last line says how many sound channels our mixer has.

 sendSoundBuffer routine is the place where the empty buffer is first filled with data and then enquened to play. I will repeat the code from last article here:
void SoundService::sendSoundBuffer()
{
 SLuint32 result;

 prepareSoundBuffer();
 result = (*mSoundQueue)->Enqueue(mSoundQueue, mActiveAudioOutSoundBuffer,
   sizeof(s16) * SBC_AUDIO_OUT_BUFFER_SIZE);
 if (result != SL_RESULT_SUCCESS)
  LOGE("enqueue method of sound buffer failed");
 swapSoundBuffers();
}

 It is apparent, that sound buffer is prepared in prepareSoundBuffer(), so let's take closer look at it. First, we clear the temporary buffer:
void SoundService::prepareSoundBuffer()
{
 s32* tmp = mTmpSoundBuffer;

 // clear tmp buffer
 memset(mTmpSoundBuffer, 0, sizeof(s32) * SBC_AUDIO_OUT_BUFFER_SIZE);

 Next we go through our channels. First we check whether the channel is active. Then we calculate which is shorter - whether the sound buffer or remaining sound data in sample for our channel. We adjust position for next time and fill the temporary buffer with data. We are adding the sound data to data that are currently in temporary buffer. As we are playing 16bit PCM data it may happen that two or more amplitudes will meet at the same position. If we put data directly into buffer we would overflow the top or bottom limit. With temporary buffer wide twice as much we are safe.
 // fill tmp buffer
 for (s32 i = 0; i < SBC_AUDIO_OUT_CHANNELS; i++)
 {
  SoundInfo& sound = mSounds[i];
  if (sound.mUsed == true)
  {
   s32 addLength = Math::min(SBC_AUDIO_OUT_BUFFER_SIZE, sound.mLength - sound.mPosition);
   s16* addData = sound.mData + sound.mPosition;
   if (sound.mPosition + addLength >= sound.mLength)
    sound.mUsed = false;
   else
    sound.mPosition += addLength;

   for (s32 j = 0; j < addLength; j++)
    tmp[j] += addData[j];
  }
 }

 Finally, we need to clip the data in temporary int buffer to fit into final short buffer. This is done with simple loop while checking the range.
 // finalize (clip) output buffer
 s16* dataOut = mActiveAudioOutSoundBuffer;
 for (s32 i = 0; i < SBC_AUDIO_OUT_BUFFER_SIZE; i++)
 {
  if (tmp[i] > SHRT_MAX)
   dataOut[i] = SHRT_MAX;
  else if (tmp[i] < SHRT_MIN)
   dataOut[i] = SHRT_MIN;
  else
   dataOut[i] = tmp[i];
 }
}


Playing the sound(s)


 Until now we implemented all that is need to set up OpenSL ES and to mix sounds. Below is my routine that starts playing the sound. The class ISoundInfoProvider is interface class with only one pure virtual method:
virtual void getSoundInfo(SoundInfo& aSoundInfo) = 0;

 My sound asset classes are derived from common Asset class as other assets (textures, ...). Additionally it has to implement ISoundInfoProvider interface. From method parameters it can be seen that structure for channel is handed to it. The implementation of method should fill all necessary information needed to play the sound - where its data are stored, where it starts, how long it is, etc. For example: one of my sound assets are .WAV files. The implementation skips the .WAV header, says where the sound data are stored in memory and how much of them is there.
 With this approach you can use different file formats as far as you return requested fields in SoundInfo structure. Additionally your "System" part of engine (see Simple cross-platform game engine - Introduction) does not need to know about anything specific for currently developed game nor for "Engine" part of engine.

 The playSound first looks for free channel. If no channel is free and priority is zero then no sound is played. If priority is higher than zero then the longest playing channel is selected. Here much better handling of priorities can be done. But using 8 channels was enough for me so far so I had no problems. I used the priority only when I wanted to be 100% sure that sound will play. In the end I ask the ISoundInfoProcider implementation for filling the needed initial channel data and also the time when playing started is stored.
bool SoundService::playSound(ISoundInfoProvider* aSoundInfoProvider, s32 aPriority)
{
 // get sound info (where are data, how long it is, ...) from sound info provider
 SoundInfo* soundInfo = NULL;

 // find free sound slot
 for (s32 i = 0; i < SBC_AUDIO_OUT_CHANNELS; i++)
 {
  if (!mSounds[i].mUsed)
  {
   soundInfo = &mSounds[i];
   break;
  }
 }

 // not any free slot?
 if (soundInfo == NULL)
 {
  if (aPriority == 0)
   return false;
  else
  {
   // find oldest sound
   u32 started = 0x7FFFFFFF;
   s32 slot = -1;
   for (s32 i = 0; i < SBC_AUDIO_OUT_CHANNELS; i++)
   {
    if (mSounds[i].mStarted < started)
    {
     started = mSounds[i].mStarted;
     slot = i;
    }
   }

   if (slot == -1)
    return false;

   soundInfo = &mSounds[slot];
  }
 }

 // load sound info into free slot
 aSoundInfoProvider->getSoundInfo(*soundInfo);
 soundInfo->mUsed = true;
 soundInfo->mStarted = TimeService::getTickCount();

 return true;
}


Problems


 As you already guessed the new sound starts playing when buffer it is in starts playing. So there is some delay between the time you request the playing and time it starts because the buffer waits in two buffer queue. So you may attempt to make the buffer smaller. But doing this may lead to another type of problem. The buffers are so short you are not fast enough to fill them with data and the sound is choppy. So, you have to balance the size of the buffer.

 Here are values that work good for me:
#define SBC_AUDIO_OUT_BUFFER_SIZE 256
#define SBC_AUDIO_OUT_CHANNELS 8
#define SBC_AUDIO_OUT_SAMPLE_RATE 11025


Conclusion


 If you followed both parts of this article you should have now enough information on how to handle the OpenSL ES setting and how to simply mix sounds. This mixer we used for playing sounds in our Deadly Abyss 2 and Mahjong Tris games.



Saturday, January 12, 2013

Simple streaming audio mixer for Android with OpenSL ES - part 1




Previous parts

Today I will write some notes on how to write simple mixer for streaming audio. I will use OpenSL ES and pure C/C++ code (no java needed). Building simple sound mixer for Android includes two areas:
  • working with OpenSL ES - with its object, interfaces, ...,
  • creating some logic to build buffers with data and sending them to output.
 The code pieces are taken from my small cross-platform engine and you can see / hear it in action in Deadly Abyss 2 and Mahjong Tris games. Class SoundService is responsible for interaction with sound device of target platform. When the object of the class is created all of the needed member variables ("m" prefixed) are cleared. I am showing the routine here mainly because it lists the variables I will use later:

SoundService::SoundService()
{
 // engine
 mEngineObj = NULL;
 mEngine = NULL;
 // output
 mOutPutMixObj = NULL;
 // sound
 mSoundPlayerObj = NULL;
 mSoundPlayer = NULL;
 mSoundVolume = NULL;
 mSoundQueue = NULL;
 // sound mixer
 mActiveAudioOutSoundBuffer = NULL;
}

Initialization


 Initializing with OpenSL takes quite a lot of code. OpenSL ES objects are first created but no resources are allocated. According to OpenSL ES 1.1 Specification (see it at khronos.org site) the object is: "an abstraction of a set of resources, assigned for a well-defined set of tasks, and the state of these resources." To allocate the resources the object must be Realized.
 To access features the object offers you have to acquire interface object. Interfaces are defined as: " an abstraction of a set of related features that a certain object provides."

 First we have to define Engine object which is entry point into OpenSL ES API:

s32 SoundService::start()
{
 LOGI("Starting sound service");

 SLresult result;

 // engine
 const SLuint32 engineMixIIDCount = 1;
 const SLInterfaceID engineMixIIDs[] = {SL_IID_ENGINE};
 const SLboolean engineMixReqs[] = {SL_BOOLEAN_TRUE};

 // create engine
 result = slCreateEngine(&mEngineObj, 0, NULL,
   engineMixIIDCount, engineMixIIDs, engineMixReqs);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;
 // realize
 result = (*mEngineObj)->Realize(mEngineObj, SL_BOOLEAN_FALSE);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;
 // get interfaces
 result = (*mEngineObj)->GetInterface(mEngineObj, SL_IID_ENGINE, &mEngine);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;

 With slCreateEngine we are creating Engine object that is returned in first parameter. Next two parameters specify optional features. Last three parameter refers to const values you can see in code list and are related to number of interfaces, which interfaces are requested and whether these interfaces are required or optional. We are requesting only for one interface (SL_IID_ENGINE).
 Just now no resources are allocated yet. We have to Realize the object. The second parameter says whether it should be asynchronous. We want synchronous realization.
 Now we can cache interfaces. Here we have only one and we will store it in mEngine variable (the last parameter is for output now) .

 Next we are going to create output mix - object that is in the end and sends our data to HW device. The creation takes the same logic as for Engine but this time we have zero interfaces.

 // mixed output
 const SLuint32 outputMixIIDCount = 0;
 const SLInterfaceID outputMixIIDs[] = {};
 const SLboolean outputMixReqs[] = {};

 // create output
 result = (*mEngine)->CreateOutputMix(mEngine, &mOutPutMixObj,
   outputMixIIDCount, outputMixIIDs, outputMixReqs);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;
 result = (*mOutPutMixObj)->Realize(mOutPutMixObj, SL_BOOLEAN_FALSE);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;

 return 0;

ERROR:
 LOGE("Starting SoundService failed");
 stop();
 return -1;
}


AudioPlayer


 Now we are going to build the sound player that will be responsible for keeping queue with sound data full. It will be attached to Engine object and it will send its output to created output mix.

 In following routine we are also encountering pieces of the second area - the mixer logic. When we meet them I will just mention it, describe it briefly and skip for now as the mixer logic will be explained in second part of this article.

 The initial part is related to mixer logic - it marks all sound channels of the mixer as unused.

s32 SoundService::startSoundPlayer()
{
 // clear sounds
 for (s32 i = 0; i < SBC_AUDIO_OUT_CHANNELS; i++)
  mSounds[i].mUsed = false;

 First we define data locator - we say where the data we want to play comes from.

 SLresult result;

 // INPUT
 // audio source with maximum of two buffers in the queue
 // where are data
 SLDataLocator_AndroidSimpleBufferQueue dataLocatorInput;
 dataLocatorInput.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
 dataLocatorInput.numBuffers = 2;

 We say that the data will be in memory buffer and that we have two buffers. If we wanted to play some mp3 music stored in file we would use SL_DATALOCATOR_ANDROIDFD with different additional parameters.

 Then we define the format of the data that will be stored in memory buffers:

 // format of data
 SLDataFormat_PCM dataFormat;
 dataFormat.formatType = SL_DATAFORMAT_PCM;
 dataFormat.numChannels = 1; // Mono sound.
 dataFormat.samplesPerSec = SL_SAMPLINGRATE_11_025;
 dataFormat.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
 dataFormat.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
 dataFormat.channelMask = SL_SPEAKER_FRONT_CENTER;
 dataFormat.endianness = SL_BYTEORDER_LITTLEENDIAN;

 The parameters are self-explaining. We will create buffers with raw PCM data. Our playback rate will be 11025 Hz and the data will be 16 bit little endian.

 Now we can combine the location of data with its format to create SLDataSource object that describes the input data:

 // combine location and format into source
 SLDataSource dataSource;
 dataSource.pLocator = &dataLocatorInput;
 dataSource.pFormat = &dataFormat;

 We have finished the description of input so now we have to describe the output. We will send data to output mix we created when initializing in start() method:

 // OUTPUT
 SLDataLocator_OutputMix dataLocatorOut;
 dataLocatorOut.locatorType = SL_DATALOCATOR_OUTPUTMIX;
 dataLocatorOut.outputMix = mOutPutMixObj;

 SLDataSink dataSink;
 dataSink.pLocator = &dataLocatorOut;
 dataSink.pFormat = NULL;

 Now it is time to create the sound player object. the object will be attached to Engine, its data will be as described (raw 16-bit PCM stored in memory buffers) and it will output it dataSink that will forward it to output mix. We will follow again the OpenSL ES logic - create object, realize it (to allocate resources ...), get interfaces. Notice that we have three interfaces. SL_IID_PLAY will allow us to start, stop, pause the playing. SL_IID_BUFFERQUEUE will allow us to control the queue with buffers (we have two of them). The last interface will allow us to control the volume:

 // create sound player
 const SLuint32 soundPlayerIIDCount = 3;
 const SLInterfaceID soundPlayerIIDs[] = {SL_IID_PLAY, SL_IID_BUFFERQUEUE, SL_IID_VOLUME};
 const SLboolean soundPlayerReqs[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};

 result =(*mEngine)->CreateAudioPlayer(mEngine, &mSoundPlayerObj, &dataSource, &dataSink,
   soundPlayerIIDCount, soundPlayerIIDs, soundPlayerReqs);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;
 result = (*mSoundPlayerObj)->Realize(mSoundPlayerObj, SL_BOOLEAN_FALSE);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;

 Object is created and realized - get all the three interfaces:

 // get interfaces
 result = (*mSoundPlayerObj)->GetInterface(mSoundPlayerObj, SL_IID_PLAY, &mSoundPlayer);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;
 result = (*mSoundPlayerObj)->GetInterface(mSoundPlayerObj, SL_IID_BUFFERQUEUE, &mSoundQueue);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;
 result = (*mSoundPlayerObj)->GetInterface(mSoundPlayerObj, SL_IID_VOLUME, &mSoundVolume);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;

 At this point we have initialized OpenSL ES Engine, we created audio player and we can start sending the data (in defined format) to it. We said we have two memory buffers. We can fill it with data and enqueue it but how we know that the playing finished and we should send next data? We can register callback routine through the buffer queue interface. When playing of buffer in queue is finished our custom routine (soundPlayerCallback) will get called and we can prepare and send next buffer.

 // register callback for queue
 result = (*mSoundQueue)->RegisterCallback(mSoundQueue, soundPlayerCallback, this);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;

 If we had only one buffer the audio may get choppy as there would be missing data in queue. So in the very beginning we clear both the buffers (fill it with silence) and we send both of them to queue. When playing of the first is finished our callback gets called and we can fill the first buffer with new data. While we are doing so there are still data in second buffer that is playing. Following snippet if more related to mixer logic that will be described in second part. But shortly - there are 2 buffers and one pointer that flips between them.

 // prepare mixer and enqueue 2 buffers
 // clear buffers
 memset(mAudioOutSoundData1, 0, sizeof(s16) * SBC_AUDIO_OUT_BUFFER_SIZE);
 memset(mAudioOutSoundData2, 0, sizeof(s16) * SBC_AUDIO_OUT_BUFFER_SIZE);
 // point to first one
 mActiveAudioOutSoundBuffer = mAudioOutSoundData1;

 // send two buffers
 sendSoundBuffer();
 sendSoundBuffer();

 I was wandering whether the data are copied into the queue upon sending and thus I could have only one buffer. But it seems it is not safe as Specification reads: "The buffers that are queued in a player object are used in place and are not required to be copied by the device, although this may be implementation-dependent. The application developer should be aware that modifying the content of a buffer after it has been queued is undefined and can cause audio corruption."

 Finally we can finish our long routine and start playing:

 // start playing
 result = (*mSoundPlayer)->SetPlayState(mSoundPlayer, SL_PLAYSTATE_PLAYING);
 if (result != SL_RESULT_SUCCESS)
  goto ERROR;

 // no problems
 return 0;

ERROR:
 LOGE("Creating sound player failed");
 return -1;
}


Callback


 The callback routine is as simple as this:

void SoundService::soundPlayerCallback(SLAndroidSimpleBufferQueueItf aSoundQueue, void* aContext)
{
 //LOGE("SOUND CALLBACK called");
 ((SoundService*) aContext)->sendSoundBuffer();
}

and sendBuffer() routine is the last piece in mosaic. All the routines called from it - prepareSoundBuffer() and swapSoundBuffers() are related to mixer logic and do not mess with OpenSL ES.

void SoundService::sendSoundBuffer()
{
 SLuint32 result;

 prepareSoundBuffer();
 result = (*mSoundQueue)->Enqueue(mSoundQueue, mActiveAudioOutSoundBuffer,
   sizeof(s16) * SBC_AUDIO_OUT_BUFFER_SIZE);
 if (result != SL_RESULT_SUCCESS)
  LOGE("enqueue method of sound buffer failed");
 swapSoundBuffers();
}


Cleaning


So far we described the initialization so it is time to show routines that will stop playing and clean. First clearing the sound player...:

void SoundService::stopSoundPlayer()
{
    if (mSoundPlayerObj != NULL)
    {
  SLuint32 soundPlayerState;
  (*mSoundPlayerObj)->GetState(mSoundPlayerObj, &soundPlayerState);

  if (soundPlayerState == SL_OBJECT_STATE_REALIZED)
  {
   (*mSoundQueue)->Clear(mSoundQueue);
   (*mSoundPlayerObj)->AbortAsyncOperation(mSoundPlayerObj);
   (*mSoundPlayerObj)->Destroy(mSoundPlayerObj);
   mSoundPlayerObj = NULL;
   mSoundPlayer = NULL;
   mSoundQueue = NULL;
   mSoundVolume = NULL;
  }
    }
}

... and clearing the Engine and sound output:

void SoundService::stop()
{
    // destroy sound player
 LOGI("Stopping and destroying sound player");
 stopSoundPlayer();

        LOGI("Destroying sound output");
 if (mOutPutMixObj != NULL)
 {
  (*mOutPutMixObj)->Destroy(mOutPutMixObj);
  mOutPutMixObj = NULL;
 }

 LOGI("Destroy sound engine");
 if (mEngineObj != NULL)
 {
  (*mEngineObj)->Destroy(mEngineObj);
  mEngineObj = NULL;
  mEngine = NULL;
 }
}

Conclusion


 So, for now we have:
  • initialized OpenSL ES Engine and sound output,
  • created AudioPlayer with defined input and output,
  • registered callback that will notify us when new data is needed
 We also know how to stop playing and destroy all we created.

 So far we are hearing only silence. In next part I will describe how to fill buffers with data and how to mix channels to  produce some sound.