singe/thirdparty/SDL3_mixer/src/decoder_flac.c
2026-09-03 22:01:35 -05:00

517 lines
22 KiB
C

/*
SDL_mixer: An audio mixer library based on the SDL library
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#ifdef DECODER_FLAC_LIBFLAC
#include "SDL_mixer_internal.h"
#include <FLAC/stream_decoder.h>
#if defined(FLAC_DYNAMIC) && defined(SDL_ELF_NOTE_DLOPEN)
SDL_ELF_NOTE_DLOPEN(
"flac",
"Support for FLAC audio using libFLAC",
SDL_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
FLAC_DYNAMIC
)
#endif
#ifdef FLAC_DYNAMIC
#define MIX_LOADER_DYNAMIC FLAC_DYNAMIC
#endif
#define MIX_LOADER_FUNCTIONS \
MIX_LOADER_FUNCTION(true,FLAC__StreamDecoder*,FLAC__stream_decoder_new,(void)) \
MIX_LOADER_FUNCTION(true,void,FLAC__stream_decoder_delete,(FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__StreamDecoderInitStatus,FLAC__stream_decoder_init_stream,(FLAC__StreamDecoder *,FLAC__StreamDecoderReadCallback,FLAC__StreamDecoderSeekCallback,FLAC__StreamDecoderTellCallback,FLAC__StreamDecoderLengthCallback,FLAC__StreamDecoderEofCallback,FLAC__StreamDecoderWriteCallback,FLAC__StreamDecoderMetadataCallback,FLAC__StreamDecoderErrorCallback,void *)) \
MIX_LOADER_FUNCTION(true,FLAC__StreamDecoderInitStatus,FLAC__stream_decoder_init_ogg_stream,(FLAC__StreamDecoder *,FLAC__StreamDecoderReadCallback,FLAC__StreamDecoderSeekCallback,FLAC__StreamDecoderTellCallback,FLAC__StreamDecoderLengthCallback,FLAC__StreamDecoderEofCallback,FLAC__StreamDecoderWriteCallback,FLAC__StreamDecoderMetadataCallback,FLAC__StreamDecoderErrorCallback,void *)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_finish,(FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_flush,(FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_process_single,(FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_process_until_end_of_metadata,(FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_process_until_end_of_stream,(FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_seek_absolute,(FLAC__StreamDecoder *decoder,FLAC__uint64 sample)) \
MIX_LOADER_FUNCTION(true,FLAC__StreamDecoderState,FLAC__stream_decoder_get_state,(const FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__uint64,FLAC__stream_decoder_get_total_samples,(const FLAC__StreamDecoder *decoder)) \
MIX_LOADER_FUNCTION(true,FLAC__bool,FLAC__stream_decoder_set_metadata_respond,(FLAC__StreamDecoder *decoder,FLAC__MetadataType type)) \
#define MIX_LOADER_MODULE flac
#include "SDL_mixer_loader.h"
typedef struct FLAC_AudioData
{
bool is_ogg_stream;
MIX_OggLoop loop;
SDL_PropertiesID props;
} FLAC_AudioData;
typedef struct FLAC_TrackData
{
const FLAC_AudioData *adata;
SDL_IOStream *io;
FLAC__StreamDecoder *decoder;
SDL_AudioStream *stream;
SDL_AudioSpec spec;
int bits_per_sample;
Uint8 *cvtbuf;
size_t cvtbuflen;
Sint64 current_iteration;
Sint64 current_iteration_frames;
} FLAC_TrackData;
static FLAC__StreamDecoderReadStatus FLAC_IoRead(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *userdata)
{
if (*bytes > 0) {
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
*bytes = SDL_ReadIO(tdata->io, buffer, *bytes);
const SDL_IOStatus status = SDL_GetIOStatus(tdata->io);
if (status == SDL_IO_STATUS_READY || status == SDL_IO_STATUS_NOT_READY) {
return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
} else if (status == SDL_IO_STATUS_EOF) {
return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
}
}
return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
}
static FLAC__StreamDecoderSeekStatus FLAC_IoSeek(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, void *userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
if (SDL_SeekIO(tdata->io, (Sint64)absolute_byte_offset, SDL_IO_SEEK_SET) < 0) {
return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
}
return FLAC__STREAM_DECODER_SEEK_STATUS_OK;
}
static FLAC__StreamDecoderTellStatus FLAC_IoTell(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, void *userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
const Sint64 pos = SDL_TellIO(tdata->io);
if (pos < 0) {
return FLAC__STREAM_DECODER_TELL_STATUS_ERROR;
}
*absolute_byte_offset = (FLAC__uint64)pos;
return FLAC__STREAM_DECODER_TELL_STATUS_OK;
}
static FLAC__StreamDecoderLengthStatus FLAC_IoLength(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, void *userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
const Sint64 iolen = SDL_GetIOSize(tdata->io);
if (iolen < 0) {
return FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR;
}
*stream_length = (FLAC__uint64)iolen;
return FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
}
static FLAC__bool FLAC_IoEOF(const FLAC__StreamDecoder *decoder, void *userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
return SDL_TellIO(tdata->io) >= SDL_GetIOSize(tdata->io);
}
static FLAC__StreamDecoderWriteStatus FLAC_IoWriteNoOp(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *userdata)
{
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; // we don't need this data at this moment.
}
static bool SDLCALL FLAC_seek(void *track_userdata, Uint64 frame);
static FLAC__StreamDecoderWriteStatus FLAC_IoWrite(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
SDL_AudioStream *stream = tdata->stream;
const int channels = (int) frame->header.channels;
// !!! FIXME: this is kinda gross, but FLAC 3-channel is FL, FR, FC, whereas SDL is FL, FR, LFE...we don't have a "front center" channel until 5.1 output. :/ The channel mask thing Sam wants in SDL3 would fix this.
const int sdlchannels = (channels == 3) ? 6 : channels;
// change the stream format if we're suddenly getting data in a different format. I assume this can happen if you chain FLAC files together.
if ((tdata->spec.freq != (int) frame->header.sample_rate) || (tdata->spec.channels != sdlchannels) || (tdata->bits_per_sample != (int) frame->header.bits_per_sample)) {
tdata->bits_per_sample = (int) frame->header.bits_per_sample;
tdata->spec.freq = (int) frame->header.sample_rate;
tdata->spec.channels = sdlchannels;
// decoded FLAC data is always int, from 4 to 32 bits, apparently.
if (tdata->bits_per_sample <= 8) {
tdata->spec.format = SDL_AUDIO_S8;
} else if (tdata->bits_per_sample <= 16) {
tdata->spec.format = SDL_AUDIO_S16;
} else if (tdata->bits_per_sample <= 32) {
tdata->spec.format = SDL_AUDIO_S32;
}
SDL_SetAudioStreamFormat(stream, &tdata->spec, NULL);
}
size_t amount = (size_t) frame->header.blocksize;
const size_t onebuflen = ((size_t) SDL_AUDIO_BYTESIZE(tdata->spec.format)) * amount;
const size_t buflen = onebuflen * channels;
if (tdata->cvtbuflen < buflen) {
void *ptr = SDL_realloc(tdata->cvtbuf, buflen);
if (!ptr) {
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
tdata->cvtbuf = (Uint8 *) ptr;
tdata->cvtbuflen = buflen;
}
const MIX_OggLoop *loop = &tdata->adata->loop;
if (tdata->current_iteration < 0) {
if (loop->active && ((tdata->current_iteration_frames + (Sint64)amount) >= loop->start)) {
tdata->current_iteration = 0; // we've hit the start of the loop point.
tdata->current_iteration_frames = (tdata->current_iteration_frames - loop->start); // so adding `amount` corrects this later.
}
}
if (tdata->current_iteration >= 0) {
SDL_assert(loop->active);
SDL_assert(tdata->current_iteration_frames <= loop->len);
const Sint64 available = loop->len - tdata->current_iteration_frames;
if ((Sint64)amount > available) {
amount = available;
}
SDL_assert(tdata->current_iteration_frames <= loop->len);
if ((tdata->current_iteration_frames + (Sint64)amount) >= loop->len) { // time to loop?
bool should_loop = false;
if (loop->count < 0) { // negative==infinite loop
tdata->current_iteration = 0;
should_loop = true;
} else {
tdata->current_iteration++;
SDL_assert(tdata->current_iteration <= loop->count);
if (tdata->current_iteration < loop->count) {
should_loop = true;
}
}
if (should_loop) {
const Uint64 nextframe = ((Uint64) loop->start) + ( ((Uint64) loop->len) * ((Uint64) tdata->current_iteration) );
if (!FLAC_seek(tdata, nextframe)) {
return false;
}
} else {
tdata->current_iteration = -1;
}
tdata->current_iteration_frames = 0;
}
}
if (amount > 0) {
void *channel_arrays[32];
SDL_assert(SDL_arraysize(channel_arrays) >= (size_t)channels);
// (If we padded out to 5.1 to get a front-center channel, the unnecessary channels happen
// to be at the end, so it'll assume those padded channels are silent.)
for (int i = 0; i < channels; i++) {
channel_arrays[i] = tdata->cvtbuf + (i * onebuflen);
}
const int shift = SDL_AUDIO_BITSIZE(tdata->spec.format) - tdata->bits_per_sample;
#define PREP_CHANNEL_ARRAY(typ) { \
for (int channel = 0; channel < channels; channel++) { \
const Sint32 *src = (const Sint32 *) buffer[channel]; \
typ *dst = (typ *) channel_arrays[channel]; \
for (size_t i = 0; i < amount; i++) { \
dst[i] = (typ) (src[i] << shift); \
} \
} \
}
switch (tdata->spec.format) {
case SDL_AUDIO_S8: PREP_CHANNEL_ARRAY(Sint8); break;
case SDL_AUDIO_S16: PREP_CHANNEL_ARRAY(Sint16); break;
case SDL_AUDIO_S32: PREP_CHANNEL_ARRAY(Sint32); break;
default: SDL_assert(!"Unexpected audio data type"); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
#undef PREP_CHANNEL_ARRAY
SDL_PutAudioStreamPlanarData(stream, (const void * const *) channel_arrays, channels, amount);
tdata->current_iteration_frames += amount;
}
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
static void FLAC_IoMetadata(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) userdata;
FLAC_AudioData *adata = (FLAC_AudioData *) tdata->adata; // cast away constness here. This metadata callback is when we're initializing adata.
if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
tdata->spec.freq = metadata->data.stream_info.sample_rate;
tdata->spec.channels = metadata->data.stream_info.channels; // (if we need the 3-channel map magic, it'll notice spec.channels is wrong when we get to FLAC_IoWrite and set it up.)
tdata->bits_per_sample = (int) metadata->data.stream_info.bits_per_sample;
// decoded FLAC data is always int, from 4 to 32 bits, apparently.
if (tdata->bits_per_sample <= 8) {
tdata->spec.format = SDL_AUDIO_S8;
} else if (tdata->bits_per_sample <= 16) {
tdata->spec.format = SDL_AUDIO_S16;
} else if (tdata->bits_per_sample <= 32) {
tdata->spec.format = SDL_AUDIO_S32;
}
} else if (metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) {
const FLAC__StreamMetadata_VorbisComment *vc = &metadata->data.vorbis_comment;
const int num_comments = (int) vc->num_comments;
char **comments = (char **) SDL_malloc(num_comments * sizeof (*comments));
if (comments) {
for (int i = 0; i < num_comments; i++) {
comments[i] = (char *) vc->comments[i].entry;
}
MIX_ParseOggComments(adata->props, tdata->spec.freq, (const char *) vc->vendor_string.entry, (const char * const *) comments, num_comments, &adata->loop);
SDL_free(comments);
}
}
}
static void FLAC_IoError(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data)
{
// (currently) don't care.
}
static void FLAC_IoMetadataNoOp(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *userdata)
{
// don't care about metadata at this point.
}
static bool SDLCALL FLAC_init(void)
{
if (!LoadModule_flac()) {
return false;
}
return true;
}
static void SDLCALL FLAC_quit(void)
{
UnloadModule_flac();
}
static bool SDLCALL FLAC_init_audio(SDL_IOStream *io, SDL_AudioSpec *spec, SDL_PropertiesID props, Sint64 *duration_frames, void **audio_userdata)
{
// just load the bare minimum from the IOStream to verify it's a FLAC file (if it's an Ogg stream, we'll let libFLAC try to parse it out).
bool is_ogg_stream = false;
char magic[4];
if (SDL_ReadIO(io, magic, 4) != 4) {
return false;
} else if (SDL_memcmp(magic, "OggS", 4) == 0) {
is_ogg_stream = true; // MAYBE flac, might be vorbis, etc.
} else if (SDL_memcmp(magic, "fLaC", 4) != 0) {
return SDL_SetError("Not a FLAC audio stream");
} else if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) < 0) { // rewind, let libFLAC process through the metadata, so we know it's definitely a FLAC file and we have the props.
return false;
}
FLAC_TrackData tdata;
SDL_zero(tdata);
tdata.io = io;
tdata.decoder = flac.FLAC__stream_decoder_new();
if (!tdata.decoder) {
return SDL_SetError("FLAC__stream_decoder_new() failed");
}
flac.FLAC__stream_decoder_set_metadata_respond(tdata.decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC_AudioData *adata = (FLAC_AudioData *) SDL_calloc(1, sizeof (*adata));
if (!adata) {
flac.FLAC__stream_decoder_delete(tdata.decoder);
return false;
}
adata->is_ogg_stream = is_ogg_stream;
adata->props = props;
tdata.adata = adata;
FLAC__StreamDecoderInitStatus ret;
if (is_ogg_stream) {
ret = flac.FLAC__stream_decoder_init_ogg_stream(tdata.decoder, FLAC_IoRead, FLAC_IoSeek, FLAC_IoTell, FLAC_IoLength, FLAC_IoEOF, FLAC_IoWriteNoOp, FLAC_IoMetadata, FLAC_IoError, &tdata);
} else {
ret = flac.FLAC__stream_decoder_init_stream(tdata.decoder, FLAC_IoRead, FLAC_IoSeek, FLAC_IoTell, FLAC_IoLength, FLAC_IoEOF, FLAC_IoWriteNoOp, FLAC_IoMetadata, FLAC_IoError, &tdata);
}
if (ret != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
flac.FLAC__stream_decoder_delete(tdata.decoder);
return SDL_SetError("FLAC__stream_decoder_init_stream() failed");
}
const bool rc = !!flac.FLAC__stream_decoder_process_until_end_of_metadata(tdata.decoder);
const Sint64 total_frames = rc ? (Sint64) flac.FLAC__stream_decoder_get_total_samples(tdata.decoder) : -1;
// Dump this stream no matter what. Either we failed, or we know it's a real FLAC and have its metadata pushed to `props`.
flac.FLAC__stream_decoder_finish(tdata.decoder);
flac.FLAC__stream_decoder_delete(tdata.decoder);
if (!rc) {
SDL_free(adata);
return SDL_SetError("FLAC__stream_decoder_process_until_end_of_metadata() failed");
}
// metadata processing should have filled in several things in `adata`.
adata->props = 0; // metadata callbacks needed this, but don't store this past this function.
SDL_copyp(spec, &tdata.spec);
const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
if (ignore_loops || (adata->loop.end > total_frames)) {
adata->loop.active = false;
}
if (adata->loop.active) {
*duration_frames = (adata->loop.count < 0) ? MIX_DURATION_INFINITE : (total_frames * adata->loop.count);
} else {
*duration_frames = total_frames;
}
*audio_userdata = adata;
return true;
}
static bool SDLCALL FLAC_init_track(void *audio_userdata, SDL_IOStream *io, const SDL_AudioSpec *spec, SDL_PropertiesID props, void **track_userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) SDL_calloc(1, sizeof (*tdata));
if (!tdata) {
return false;
}
const FLAC_AudioData *adata = (const FLAC_AudioData *) audio_userdata;
tdata->adata = adata;
tdata->io = io;
tdata->decoder = flac.FLAC__stream_decoder_new();
tdata->current_iteration = -1;
if (!tdata->decoder) {
SDL_free(tdata);
return SDL_SetError("FLAC__stream_decoder_new() failed");
}
FLAC__StreamDecoderInitStatus ret;
if (adata->is_ogg_stream) {
ret = flac.FLAC__stream_decoder_init_ogg_stream(tdata->decoder, FLAC_IoRead, FLAC_IoSeek, FLAC_IoTell, FLAC_IoLength, FLAC_IoEOF, FLAC_IoWrite, FLAC_IoMetadataNoOp, FLAC_IoError, tdata);
} else {
ret = flac.FLAC__stream_decoder_init_stream(tdata->decoder, FLAC_IoRead, FLAC_IoSeek, FLAC_IoTell, FLAC_IoLength, FLAC_IoEOF, FLAC_IoWrite, FLAC_IoMetadataNoOp, FLAC_IoError, tdata);
}
if (ret != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
flac.FLAC__stream_decoder_delete(tdata->decoder);
SDL_free(tdata);
return SDL_SetError("FLAC__stream_decoder_init_stream() failed");
}
if (!flac.FLAC__stream_decoder_process_until_end_of_metadata(tdata->decoder)) {
flac.FLAC__stream_decoder_finish(tdata->decoder);
flac.FLAC__stream_decoder_delete(tdata->decoder);
SDL_free(tdata);
SDL_SetError("FLAC__stream_decoder_process_until_end_of_metadata() failed");
}
SDL_copyp(&tdata->spec, spec);
*track_userdata = tdata;
return true;
}
static bool SDLCALL FLAC_decode(void *track_userdata, SDL_AudioStream *stream)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) track_userdata;
tdata->stream = stream;
if (!flac.FLAC__stream_decoder_process_single(tdata->decoder)) { // write callback will fill in stream. Might fill 0 if it hit a metadata block, but the higher level loops to get what it needs.
return false;
} else if (flac.FLAC__stream_decoder_get_state(tdata->decoder) == FLAC__STREAM_DECODER_END_OF_STREAM) {
return false; // we're done.
}
return true;
}
static bool SDLCALL FLAC_seek(void *track_userdata, Uint64 frame)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) track_userdata;
const MIX_OggLoop *loop = &tdata->adata->loop;
Sint64 final_iteration = -1;
Sint64 final_iteration_frames = 0;
// frame has hit the loop point?
if (loop->active && ((Sint64)frame >= loop->start)) {
// figure out the _actual_ frame in the vorbis file we're aiming for.
if ((loop->count < 0) || ((Sint64)frame < (loop->len * loop->count))) { // literally in the loop right now.
frame -= loop->start; // make logical frame index relative to start of loop.
final_iteration = (loop->count < 0) ? 0 : (frame / loop->len); // decide what iteration of the loop we're on (stays at zero for infinite loops).
frame %= loop->len; // drop iterations so we're an offset into the loop.
final_iteration_frames = frame;
frame += loop->start; // convert back into physical frame index.
} else { // past the loop point?
SDL_assert(loop->count > 0); // can't be infinite loop if we passed it.
frame -= loop->len * loop->count; // drop the iterations to get the physical frame index.
}
}
if (!flac.FLAC__stream_decoder_seek_absolute(tdata->decoder, frame)) {
if (flac.FLAC__stream_decoder_get_state(tdata->decoder) == FLAC__STREAM_DECODER_SEEK_ERROR) {
flac.FLAC__stream_decoder_flush(tdata->decoder);
}
return SDL_SetError("Seeking of FLAC stream failed: libFLAC seek failed.");
}
tdata->current_iteration = final_iteration;
tdata->current_iteration_frames = final_iteration_frames;
return true;
}
static void SDLCALL FLAC_quit_track(void *track_userdata)
{
FLAC_TrackData *tdata = (FLAC_TrackData *) track_userdata;
tdata->stream = NULL;
flac.FLAC__stream_decoder_finish(tdata->decoder);
flac.FLAC__stream_decoder_delete(tdata->decoder);
SDL_free(tdata->cvtbuf);
SDL_free(tdata);
}
static void SDLCALL FLAC_quit_audio(void *audio_userdata)
{
FLAC_AudioData *adata = (FLAC_AudioData *) audio_userdata;
SDL_free(adata);
}
const MIX_Decoder MIX_Decoder_FLAC = {
"FLAC",
FLAC_init,
FLAC_init_audio,
FLAC_init_track,
FLAC_decode,
FLAC_seek,
NULL, // jump_to_order
FLAC_quit_track,
FLAC_quit_audio,
FLAC_quit
};
#endif