2016-07-26 15:34:26 +00:00
|
|
|
#include <OpenAL/alc.h>
|
|
|
|
|
|
|
|
#define STB_VORBIS_HEADER_ONLY
|
|
|
|
#include <STB/stb_vorbis.c>
|
|
|
|
|
|
|
|
#include "Dengine/Assets.h"
|
|
|
|
#include "Dengine/Audio.h"
|
|
|
|
#include "Dengine/Debug.h"
|
|
|
|
|
|
|
|
#define AL_CHECK_ERROR() alCheckError_(__FILE__, __LINE__);
|
|
|
|
void alCheckError_(const char *file, int line)
|
|
|
|
{
|
2016-07-27 06:52:10 +00:00
|
|
|
// NOTE(doyle): OpenAL error stack is 1 deep
|
2016-07-26 15:34:26 +00:00
|
|
|
ALenum errorCode;
|
|
|
|
while ((errorCode = alGetError()) != AL_NO_ERROR)
|
|
|
|
{
|
|
|
|
printf("OPENAL ");
|
|
|
|
switch(errorCode)
|
|
|
|
{
|
|
|
|
case AL_INVALID_NAME:
|
|
|
|
printf("INVALID_NAME | ");
|
|
|
|
break;
|
|
|
|
case AL_INVALID_ENUM:
|
|
|
|
printf("INVALID_ENUM | ");
|
|
|
|
break;
|
|
|
|
case AL_INVALID_VALUE:
|
|
|
|
printf("INVALID_VALUE | ");
|
|
|
|
break;
|
|
|
|
case AL_INVALID_OPERATION:
|
|
|
|
printf("INVALID_OPERATION | ");
|
|
|
|
break;
|
|
|
|
case AL_OUT_OF_MEMORY:
|
|
|
|
printf("OUT_OF_MEMORY | ");
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
printf("UNRECOGNISED ERROR CODE | ");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
printf("Error %08x, %s (%d)\n", errorCode, file, line);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
const i32 audio_init(AudioManager *audioManager)
|
2016-07-26 15:34:26 +00:00
|
|
|
{
|
2016-07-27 12:50:14 +00:00
|
|
|
#ifdef DENGINE_DEBUG
|
|
|
|
ASSERT(audioManager);
|
|
|
|
#endif
|
2016-07-27 06:52:10 +00:00
|
|
|
/* Clear error stack */
|
2016-07-26 15:34:26 +00:00
|
|
|
alGetError();
|
2016-07-27 06:52:10 +00:00
|
|
|
|
|
|
|
ALboolean enumerateAudioDevice =
|
|
|
|
alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT");
|
|
|
|
|
|
|
|
const ALCchar *device = NULL;
|
|
|
|
if (enumerateAudioDevice == AL_TRUE)
|
|
|
|
{
|
|
|
|
// TODO(doyle): Actually allow users to choose device to output
|
|
|
|
/*
|
|
|
|
The OpenAL specification says that the list of devices is organized as
|
|
|
|
a string devices are separated with a NULL character and the list is
|
|
|
|
terminated by two NULL characters.
|
|
|
|
|
|
|
|
alcGetString with NULL = get all device spcifiers not a particular one
|
|
|
|
*/
|
|
|
|
device = alcGetString(NULL, ALC_DEVICE_SPECIFIER);
|
|
|
|
const ALCchar *next = device + 1;
|
|
|
|
size_t len = 0;
|
|
|
|
|
|
|
|
printf("Devices list:\n");
|
|
|
|
printf("----------\n");
|
|
|
|
while (device && *device != '\0' && next && *next != '\0')
|
|
|
|
{
|
|
|
|
printf("%s\n", device);
|
|
|
|
len = common_strlen(device);
|
|
|
|
device += (len + 1);
|
|
|
|
next += (len + 2);
|
|
|
|
}
|
|
|
|
printf("----------\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Get audio device */
|
|
|
|
ALCdevice *deviceAL = alcOpenDevice(device);
|
2016-07-26 15:34:26 +00:00
|
|
|
if (!deviceAL)
|
|
|
|
{
|
|
|
|
printf("alcOpenDevice() failed: Failed to init OpenAL device.\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2016-07-27 06:52:10 +00:00
|
|
|
/* Set device context */
|
2016-07-26 15:34:26 +00:00
|
|
|
ALCcontext *contextAL = alcCreateContext(deviceAL, NULL);
|
|
|
|
alcMakeContextCurrent(contextAL);
|
|
|
|
if (!contextAL)
|
|
|
|
{
|
|
|
|
printf("alcCreateContext() failed: Failed create AL context.\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
AL_CHECK_ERROR();
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
for (i32 i = 0; i < ARRAY_COUNT(audioManager->sourceList); i++)
|
|
|
|
{
|
|
|
|
alGenSources(1, &audioManager->sourceList[i].id);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-27 12:50:14 +00:00
|
|
|
|
|
|
|
// NOTE(doyle): If last entry, loop the free source to front of list
|
|
|
|
if (i + 1 >= ARRAY_COUNT(audioManager->sourceList))
|
|
|
|
audioManager->sourceList[i].nextFreeIndex = 0;
|
|
|
|
else
|
|
|
|
audioManager->sourceList[i].nextFreeIndex = i+1;
|
|
|
|
}
|
|
|
|
audioManager->freeSourceIndex = 0;
|
|
|
|
|
2016-07-27 06:52:10 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-07-27 15:55:21 +00:00
|
|
|
INTERNAL inline u32 getSourceId(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer)
|
|
|
|
{
|
|
|
|
u32 result = audioManager->sourceList[audioRenderer->sourceIndex].id;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
INTERNAL i32 rendererAcquire(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer)
|
2016-07-27 06:52:10 +00:00
|
|
|
{
|
2016-07-27 15:55:21 +00:00
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
i32 vacantSource = audioManager->freeSourceIndex;
|
2016-07-27 15:55:21 +00:00
|
|
|
|
|
|
|
#ifdef DENGINE_DEBUG
|
|
|
|
ASSERT(audioManager && audioRenderer);
|
|
|
|
ASSERT(vacantSource >= 0);
|
|
|
|
#endif
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
if (audioManager->sourceList[vacantSource].nextFreeIndex ==
|
|
|
|
AUDIO_NO_FREE_SOURCE)
|
|
|
|
{
|
|
|
|
// TODO(doyle): Error messaging return paths
|
2016-07-28 03:11:30 +00:00
|
|
|
DEBUG_LOG("rendererAcquire(): Failed to acquire free source, all busy");
|
2016-07-27 12:50:14 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
u32 checkSource = getSourceId(audioManager, audioRenderer);
|
|
|
|
if (alIsSource(checkSource) == AL_TRUE)
|
|
|
|
{
|
|
|
|
DEBUG_LOG(
|
|
|
|
"rendererAcquire(): Renderer has not been released before "
|
|
|
|
"acquiring, force release by stopping stream");
|
|
|
|
audio_streamStopVorbis(audioManager, audioRenderer);
|
|
|
|
}
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
/* Assign a vacant source slot to renderer */
|
|
|
|
audioRenderer->sourceIndex = vacantSource;
|
|
|
|
|
|
|
|
/* Update the immediate free source index */
|
|
|
|
audioManager->freeSourceIndex =
|
|
|
|
audioManager->sourceList[vacantSource].nextFreeIndex;
|
|
|
|
|
|
|
|
/* Mark current source as in use */
|
|
|
|
audioManager->sourceList[vacantSource].nextFreeIndex = AUDIO_NO_FREE_SOURCE;
|
2016-07-26 15:34:26 +00:00
|
|
|
|
|
|
|
/* Generate audio data buffers */
|
|
|
|
alGenBuffers(ARRAY_COUNT(audioRenderer->bufferId), audioRenderer->bufferId);
|
|
|
|
AL_CHECK_ERROR();
|
|
|
|
|
2016-07-27 08:39:51 +00:00
|
|
|
//alSourcef(audioRenderer->sourceId[0], AL_PITCH, 2.0f);
|
|
|
|
|
2016-07-26 15:34:26 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
INTERNAL const i32 rendererRelease(AudioManager *audioManager,
|
2016-07-27 12:50:14 +00:00
|
|
|
AudioRenderer *audioRenderer)
|
|
|
|
{
|
2016-07-27 15:55:21 +00:00
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
i32 result = 0;
|
2016-07-27 15:55:21 +00:00
|
|
|
u32 alSourceId = getSourceId(audioManager, audioRenderer);
|
2016-07-28 03:11:30 +00:00
|
|
|
if (alIsSource(alSourceId) == AL_FALSE)
|
|
|
|
{
|
|
|
|
DEBUG_LOG(
|
|
|
|
"rendererRelease(): Trying to release invalid source, early exit");
|
2016-07-27 15:55:21 +00:00
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
result = -1;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-07-28 03:36:16 +00:00
|
|
|
alSourceUnqueueBuffers(alSourceId, ARRAY_COUNT(audioRenderer->bufferId),
|
|
|
|
audioRenderer->bufferId);
|
|
|
|
alDeleteBuffers(ARRAY_COUNT(audioRenderer->bufferId),
|
|
|
|
audioRenderer->bufferId);
|
|
|
|
AL_CHECK_ERROR();
|
2016-07-27 15:55:21 +00:00
|
|
|
|
|
|
|
for (i32 i = 0; i < ARRAY_COUNT(audioRenderer->bufferId); i++)
|
|
|
|
{
|
|
|
|
audioRenderer->bufferId[i] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
audioRenderer->audio = NULL;
|
|
|
|
audioRenderer->numPlays = 0;
|
|
|
|
|
|
|
|
u32 sourceIndexToFree = audioRenderer->sourceIndex;
|
|
|
|
audioRenderer->sourceIndex = AUDIO_SOURCE_UNASSIGNED;
|
2016-07-27 12:50:14 +00:00
|
|
|
|
|
|
|
audioManager->sourceList[sourceIndexToFree].nextFreeIndex =
|
|
|
|
audioManager->freeSourceIndex;
|
|
|
|
audioManager->freeSourceIndex = sourceIndexToFree;
|
2016-07-27 15:55:21 +00:00
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
return result;
|
2016-07-27 12:50:14 +00:00
|
|
|
}
|
|
|
|
|
2016-07-26 15:34:26 +00:00
|
|
|
#define AUDIO_CHUNK_SIZE_ 65536
|
2016-07-28 03:11:30 +00:00
|
|
|
const i32 audio_streamPlayVorbis(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer,
|
|
|
|
AudioVorbis *vorbis, i32 numPlays)
|
2016-07-26 15:34:26 +00:00
|
|
|
{
|
2016-07-27 12:50:14 +00:00
|
|
|
#ifdef DENGINE_DEBUG
|
|
|
|
ASSERT(audioManager && audioRenderer && vorbis);
|
|
|
|
if (numPlays != AUDIO_REPEAT_INFINITE && numPlays <= 0)
|
|
|
|
{
|
2016-07-27 15:55:21 +00:00
|
|
|
DEBUG_LOG("audio_streamPlayVorbis() warning: Number of plays is less than 0");
|
2016-07-27 12:50:14 +00:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
i32 result = rendererAcquire(audioManager, audioRenderer);
|
|
|
|
if (result)
|
|
|
|
{
|
2016-07-27 15:55:21 +00:00
|
|
|
DEBUG_LOG("audio_streamPlayVorbis() failed: Could not acquire renderer");
|
2016-07-28 03:11:30 +00:00
|
|
|
return result;
|
2016-07-27 12:50:14 +00:00
|
|
|
}
|
|
|
|
|
2016-07-26 15:34:26 +00:00
|
|
|
/* Determine format */
|
|
|
|
audioRenderer->format = AL_FORMAT_MONO16;
|
|
|
|
if (vorbis->info.channels == 2)
|
|
|
|
audioRenderer->format = AL_FORMAT_STEREO16;
|
|
|
|
else if (vorbis->info.channels != 1)
|
2016-07-27 08:39:51 +00:00
|
|
|
{
|
|
|
|
#ifdef DENGINE_DEBUG
|
2016-07-27 12:50:14 +00:00
|
|
|
DEBUG_LOG(
|
2016-07-28 03:11:30 +00:00
|
|
|
"audio_streamPlayVorbis() warning: Unexpected channel format");
|
2016-07-27 08:39:51 +00:00
|
|
|
#endif
|
|
|
|
}
|
2016-07-26 15:34:26 +00:00
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
audioRenderer->audio = vorbis;
|
|
|
|
audioRenderer->numPlays = numPlays;
|
2016-07-28 03:11:30 +00:00
|
|
|
|
|
|
|
return 0;
|
2016-07-26 15:34:26 +00:00
|
|
|
}
|
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
const i32 audio_streamStopVorbis(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer)
|
2016-07-27 15:55:21 +00:00
|
|
|
{
|
2016-07-28 03:11:30 +00:00
|
|
|
i32 result = 0;
|
2016-07-27 15:55:21 +00:00
|
|
|
u32 alSourceId = getSourceId(audioManager, audioRenderer);
|
2016-07-28 03:11:30 +00:00
|
|
|
if (alIsSource(alSourceId) == AL_TRUE)
|
|
|
|
{
|
|
|
|
alSourceStop(alSourceId);
|
|
|
|
AL_CHECK_ERROR();
|
|
|
|
result = rendererRelease(audioManager, audioRenderer);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
DEBUG_LOG("audio_streamStopVorbis(): Tried to stop invalid source");
|
|
|
|
result = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
2016-07-27 15:55:21 +00:00
|
|
|
}
|
|
|
|
|
2016-07-28 03:11:30 +00:00
|
|
|
const i32 audio_streamPauseVorbis(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer)
|
|
|
|
{
|
|
|
|
i32 result = 0;
|
|
|
|
u32 alSourceId = getSourceId(audioManager, audioRenderer);
|
|
|
|
if (alIsSource(alSourceId) == AL_TRUE)
|
|
|
|
{
|
|
|
|
alSourcePause(alSourceId);
|
|
|
|
AL_CHECK_ERROR();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
DEBUG_LOG("audio_streamPauseVorbis(): Tried to pause invalid source");
|
|
|
|
result = -1;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const i32 audio_streamResumeVorbis(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer)
|
|
|
|
{
|
|
|
|
i32 result = 0;
|
|
|
|
u32 alSourceId = getSourceId(audioManager, audioRenderer);
|
|
|
|
if (alIsSource(alSourceId) == AL_TRUE)
|
|
|
|
{
|
|
|
|
alSourcePlay(alSourceId);
|
|
|
|
AL_CHECK_ERROR();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
DEBUG_LOG("audio_streamResumeVorbis(): Tried to resume invalid source");
|
|
|
|
result = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const i32 audio_updateAndPlay(AudioManager *audioManager,
|
|
|
|
AudioRenderer *audioRenderer)
|
2016-07-26 15:34:26 +00:00
|
|
|
{
|
|
|
|
AudioVorbis *audio = audioRenderer->audio;
|
2016-07-28 03:11:30 +00:00
|
|
|
if (!audio) return 0;
|
2016-07-27 12:50:14 +00:00
|
|
|
|
|
|
|
if (audioRenderer->numPlays != AUDIO_REPEAT_INFINITE &&
|
|
|
|
audioRenderer->numPlays <= 0)
|
|
|
|
{
|
2016-07-28 03:11:30 +00:00
|
|
|
i32 result = rendererRelease(audioManager, audioRenderer);
|
|
|
|
return result;
|
2016-07-26 15:40:22 +00:00
|
|
|
}
|
2016-07-26 15:34:26 +00:00
|
|
|
|
2016-07-27 15:55:21 +00:00
|
|
|
u32 alSourceId = getSourceId(audioManager, audioRenderer);
|
2016-07-28 03:11:30 +00:00
|
|
|
if (alIsSource(alSourceId) == AL_FALSE)
|
|
|
|
{
|
|
|
|
DEBUG_LOG("audio_updateAndPlay(): Update failed on invalid source id");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2016-07-26 15:34:26 +00:00
|
|
|
ALint audioState;
|
2016-07-27 12:50:14 +00:00
|
|
|
alGetSourcei(alSourceId, AL_SOURCE_STATE, &audioState);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-26 15:34:26 +00:00
|
|
|
if (audioState == AL_STOPPED || audioState == AL_INITIAL)
|
|
|
|
{
|
|
|
|
if (audioState == AL_STOPPED)
|
|
|
|
{
|
2016-07-27 12:50:14 +00:00
|
|
|
if (audioRenderer->numPlays != AUDIO_REPEAT_INFINITE)
|
|
|
|
audioRenderer->numPlays--;
|
|
|
|
|
|
|
|
if (audioRenderer->numPlays == AUDIO_REPEAT_INFINITE ||
|
|
|
|
audioRenderer->numPlays > 0)
|
|
|
|
{
|
2016-07-27 15:55:21 +00:00
|
|
|
// TODO(doyle): Delete and recreate fixes clicking when reusing
|
|
|
|
// buffers
|
|
|
|
alDeleteBuffers(ARRAY_COUNT(audioRenderer->bufferId),
|
|
|
|
audioRenderer->bufferId);
|
|
|
|
AL_CHECK_ERROR();
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
alGenBuffers(ARRAY_COUNT(audioRenderer->bufferId),
|
|
|
|
audioRenderer->bufferId);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-27 12:50:14 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2016-07-28 03:11:30 +00:00
|
|
|
i32 result = rendererRelease(audioManager, audioRenderer);
|
|
|
|
return result;
|
2016-07-27 12:50:14 +00:00
|
|
|
}
|
2016-07-26 15:34:26 +00:00
|
|
|
}
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
// TODO(doyle): Possible bug! Multiple sources playing same file seeking
|
|
|
|
// file ptr to start may interrupt other stream
|
2016-07-26 15:34:26 +00:00
|
|
|
stb_vorbis_seek_start(audio->file);
|
|
|
|
for (i32 i = 0; i < ARRAY_COUNT(audioRenderer->bufferId); i++)
|
|
|
|
{
|
|
|
|
i16 audioChunk[AUDIO_CHUNK_SIZE_] = {0};
|
|
|
|
stb_vorbis_get_samples_short_interleaved(
|
|
|
|
audio->file, audio->info.channels, audioChunk,
|
|
|
|
AUDIO_CHUNK_SIZE_);
|
|
|
|
|
|
|
|
alBufferData(audioRenderer->bufferId[i], audioRenderer->format,
|
|
|
|
audioChunk, AUDIO_CHUNK_SIZE_ * sizeof(i16),
|
|
|
|
audio->info.sample_rate);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-26 15:34:26 +00:00
|
|
|
}
|
|
|
|
|
2016-07-27 12:50:14 +00:00
|
|
|
alSourceQueueBuffers(alSourceId, ARRAY_COUNT(audioRenderer->bufferId),
|
2016-07-26 15:34:26 +00:00
|
|
|
audioRenderer->bufferId);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-27 12:50:14 +00:00
|
|
|
alSourcePlay(alSourceId);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-26 15:34:26 +00:00
|
|
|
}
|
|
|
|
else if (audioState == AL_PLAYING)
|
|
|
|
{
|
|
|
|
ALint numProcessedBuffers;
|
2016-07-27 12:50:14 +00:00
|
|
|
alGetSourcei(alSourceId, AL_BUFFERS_PROCESSED,
|
2016-07-26 15:34:26 +00:00
|
|
|
&numProcessedBuffers);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-26 15:34:26 +00:00
|
|
|
if (numProcessedBuffers > 0)
|
|
|
|
{
|
|
|
|
ALint numBuffersToUnqueue = 1;
|
|
|
|
ALuint emptyBufferId;
|
2016-07-27 12:50:14 +00:00
|
|
|
alSourceUnqueueBuffers(alSourceId, numBuffersToUnqueue,
|
|
|
|
&emptyBufferId);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-26 15:34:26 +00:00
|
|
|
|
|
|
|
i16 audioChunk[AUDIO_CHUNK_SIZE_] = {0};
|
|
|
|
i32 sampleCount = stb_vorbis_get_samples_short_interleaved(
|
|
|
|
audio->file, audio->info.channels, audioChunk,
|
|
|
|
AUDIO_CHUNK_SIZE_);
|
|
|
|
|
|
|
|
/* There are still samples to play */
|
|
|
|
if (sampleCount > 0)
|
|
|
|
{
|
|
|
|
alBufferData(emptyBufferId, audioRenderer->format, audioChunk,
|
|
|
|
sampleCount * audio->info.channels * sizeof(i16),
|
|
|
|
audio->info.sample_rate);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-27 12:50:14 +00:00
|
|
|
alSourceQueueBuffers(alSourceId, 1, &emptyBufferId);
|
2016-07-27 15:55:21 +00:00
|
|
|
AL_CHECK_ERROR();
|
2016-07-26 15:34:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-07-28 03:11:30 +00:00
|
|
|
|
|
|
|
return 0;
|
2016-07-26 15:34:26 +00:00
|
|
|
}
|