367 lines
13 KiB
C
367 lines
13 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.
|
|
*/
|
|
|
|
// This file supports Ogg Opus audio streams using libopusfile.
|
|
|
|
#ifdef DECODER_OPUS
|
|
|
|
#include "SDL_mixer_internal.h"
|
|
|
|
#include <opusfile.h>
|
|
|
|
#if defined(OPUS_DYNAMIC) && defined(SDL_ELF_NOTE_DLOPEN)
|
|
SDL_ELF_NOTE_DLOPEN(
|
|
"opus",
|
|
"Support for OPUS audio using opusfile",
|
|
SDL_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
|
|
OPUS_DYNAMIC
|
|
)
|
|
#endif
|
|
|
|
#ifdef OPUS_DYNAMIC
|
|
#define MIX_LOADER_DYNAMIC OPUS_DYNAMIC
|
|
#endif
|
|
|
|
#define MIX_LOADER_FUNCTIONS \
|
|
MIX_LOADER_FUNCTION(true,const OpusTags *,op_tags,(const OggOpusFile *,int)) \
|
|
MIX_LOADER_FUNCTION(true,OggOpusFile *,op_open_callbacks,(void *,const OpusFileCallbacks *,const unsigned char *,size_t,int *)) \
|
|
MIX_LOADER_FUNCTION(true,OggOpusFile *,op_test_callbacks,(void *,const OpusFileCallbacks *,const unsigned char *,size_t,int *)) \
|
|
MIX_LOADER_FUNCTION(true,void,op_free,(OggOpusFile *)) \
|
|
MIX_LOADER_FUNCTION(true,const OpusHead *,op_head,(const OggOpusFile *,int)) \
|
|
MIX_LOADER_FUNCTION(true,int,op_seekable,(const OggOpusFile *)) \
|
|
MIX_LOADER_FUNCTION(true,int,op_read_float,(OggOpusFile *, float *,int,int *)) \
|
|
MIX_LOADER_FUNCTION(true,int,op_raw_seek,(OggOpusFile *,opus_int64)) \
|
|
MIX_LOADER_FUNCTION(true,int,op_pcm_seek,(OggOpusFile *,ogg_int64_t)) \
|
|
MIX_LOADER_FUNCTION(true,ogg_int64_t,op_pcm_tell,(const OggOpusFile *)) \
|
|
MIX_LOADER_FUNCTION(true,ogg_int64_t,op_pcm_total,(const OggOpusFile *, int)) \
|
|
|
|
#define MIX_LOADER_MODULE opus
|
|
#include "SDL_mixer_loader.h"
|
|
|
|
|
|
typedef struct OPUS_AudioData
|
|
{
|
|
MIX_OggLoop loop;
|
|
} OPUS_AudioData;
|
|
|
|
typedef struct OPUS_TrackData
|
|
{
|
|
const OPUS_AudioData *adata;
|
|
OggOpusFile *of;
|
|
int current_channels;
|
|
int current_bitstream;
|
|
Sint64 current_iteration;
|
|
Sint64 current_iteration_frames;
|
|
} OPUS_TrackData;
|
|
|
|
|
|
static bool SDLCALL OPUS_init(void)
|
|
{
|
|
return LoadModule_opus();
|
|
}
|
|
|
|
static void SDLCALL OPUS_quit(void)
|
|
{
|
|
UnloadModule_opus();
|
|
}
|
|
|
|
static bool set_op_error(const char *function, int error)
|
|
{
|
|
#define HANDLE_ERROR_CASE(X) case X: return SDL_SetError("%s: %s", function, #X)
|
|
switch (error) {
|
|
HANDLE_ERROR_CASE(OP_FALSE);
|
|
HANDLE_ERROR_CASE(OP_EOF);
|
|
HANDLE_ERROR_CASE(OP_HOLE);
|
|
HANDLE_ERROR_CASE(OP_EREAD);
|
|
HANDLE_ERROR_CASE(OP_EFAULT);
|
|
HANDLE_ERROR_CASE(OP_EIMPL);
|
|
HANDLE_ERROR_CASE(OP_EINVAL);
|
|
HANDLE_ERROR_CASE(OP_ENOTFORMAT);
|
|
HANDLE_ERROR_CASE(OP_EBADHEADER);
|
|
HANDLE_ERROR_CASE(OP_EVERSION);
|
|
HANDLE_ERROR_CASE(OP_ENOTAUDIO);
|
|
HANDLE_ERROR_CASE(OP_EBADPACKET);
|
|
HANDLE_ERROR_CASE(OP_EBADLINK);
|
|
HANDLE_ERROR_CASE(OP_ENOSEEK);
|
|
HANDLE_ERROR_CASE(OP_EBADTIMESTAMP);
|
|
#undef HANDLE_ERROR_CASE
|
|
default: break;
|
|
}
|
|
return SDL_SetError("%s: unknown error %d", function, error);
|
|
}
|
|
|
|
static int OPUS_IoRead(void *datasource, unsigned char *ptr, int size)
|
|
{
|
|
return SDL_ReadIO((SDL_IOStream*)datasource, ptr, (size_t) size);
|
|
}
|
|
|
|
static int OPUS_IoSeek(void *datasource, opus_int64 offset, int whence)
|
|
{
|
|
return (SDL_SeekIO((SDL_IOStream*)datasource, offset, whence) < 0) ? -1 : 0;
|
|
}
|
|
|
|
static opus_int64 OPUS_IoTell(void *datasource)
|
|
{
|
|
return SDL_TellIO((SDL_IOStream*)datasource);
|
|
}
|
|
|
|
static int OPUS_IoClose(void *datasource)
|
|
{
|
|
(void)datasource;
|
|
return 0;
|
|
}
|
|
|
|
static const OpusFileCallbacks OPUS_IoCallbacks = { OPUS_IoRead, OPUS_IoSeek, OPUS_IoTell, OPUS_IoClose };
|
|
|
|
|
|
static bool SDLCALL OPUS_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 an Opus file.
|
|
// !!! FIXME: is op_open_callbacks going to return more slowly if this isn't an Opus file? It's probably better to just do the full open.
|
|
int rc = 0;
|
|
OggOpusFile *of = opus.op_test_callbacks(io, &OPUS_IoCallbacks, NULL, 0, &rc);
|
|
if (!of) {
|
|
return SDL_SetError("Not an Opus audio stream");
|
|
}
|
|
opus.op_free(of);
|
|
|
|
// Go back and do a proper load now to get metadata.
|
|
if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) < 0) {
|
|
return false;
|
|
}
|
|
|
|
OPUS_AudioData *adata = (OPUS_AudioData *) SDL_calloc(1, sizeof (*adata));
|
|
if (!adata) {
|
|
return false;
|
|
}
|
|
|
|
// now open the stream for serious processing.
|
|
of = opus.op_open_callbacks(io, &OPUS_IoCallbacks, NULL, 0, &rc);
|
|
if (!of) {
|
|
SDL_free(adata);
|
|
return set_op_error("ov_open_callbacks", rc);
|
|
}
|
|
|
|
const OpusHead *info = opus.op_head(of, -1);
|
|
if (!info) {
|
|
opus.op_free(of);
|
|
SDL_free(adata);
|
|
return SDL_SetError("Couldn't get Opus info; corrupt data?");
|
|
}
|
|
|
|
spec->format = SDL_AUDIO_F32;
|
|
spec->channels = info->channel_count;
|
|
spec->freq = 48000; // libopus only outputs at 48kHz.
|
|
|
|
const OpusTags *tags = opus.op_tags(of, -1);
|
|
if (tags != NULL) {
|
|
MIX_ParseOggComments(props, spec->freq, tags->vendor, (const char * const *) tags->user_comments, tags->comments, &adata->loop);
|
|
}
|
|
|
|
opus.op_raw_seek(of, 0); // !!! FIXME: it's not clear if this seek is necessary, but https://stackoverflow.com/a/72482773 suggests it might be, at least on older libvorbisfile releases...
|
|
const Sint64 full_length = (Sint64) opus.op_pcm_total(of, -1);
|
|
|
|
const bool ignore_loops = SDL_GetBooleanProperty(props, MIX_PROP_AUDIO_LOAD_IGNORE_LOOPS_BOOLEAN, false);
|
|
if (ignore_loops || (adata->loop.end > full_length)) {
|
|
adata->loop.active = false;
|
|
}
|
|
|
|
opus.op_free(of); // done with this instance. Tracks will maintain their own OggOpusFile object.
|
|
|
|
if (adata->loop.active) {
|
|
*duration_frames = (adata->loop.count < 0) ? MIX_DURATION_INFINITE : (full_length * adata->loop.count);
|
|
} else {
|
|
*duration_frames = full_length;
|
|
}
|
|
|
|
*audio_userdata = adata;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool SDLCALL OPUS_init_track(void *audio_userdata, SDL_IOStream *io, const SDL_AudioSpec *spec, SDL_PropertiesID props, void **track_userdata)
|
|
{
|
|
OPUS_TrackData *tdata = (OPUS_TrackData *) SDL_calloc(1, sizeof (*tdata));
|
|
if (!tdata) {
|
|
return false;
|
|
}
|
|
|
|
const OPUS_AudioData *adata = (const OPUS_AudioData *) audio_userdata;
|
|
|
|
// now open the stream for serious processing.
|
|
int rc = 0;
|
|
tdata->of = opus.op_open_callbacks(io, &OPUS_IoCallbacks, NULL, 0, &rc);
|
|
if (!tdata->of) {
|
|
SDL_free(tdata);
|
|
return set_op_error("op_open_callbacks", rc);
|
|
}
|
|
|
|
tdata->current_channels = spec->channels;
|
|
tdata->current_bitstream = -1;
|
|
tdata->current_iteration = -1;
|
|
tdata->adata = adata;
|
|
|
|
*track_userdata = tdata;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool SDLCALL OPUS_seek(void *track_userdata, Uint64 frame);
|
|
|
|
static bool SDLCALL OPUS_decode(void *track_userdata, SDL_AudioStream *stream)
|
|
{
|
|
OPUS_TrackData *tdata = (OPUS_TrackData *) track_userdata;
|
|
int bitstream = tdata->current_bitstream;
|
|
float samples[256];
|
|
|
|
const int channels = tdata->current_channels;
|
|
int amount = opus.op_read_float(tdata->of, samples, SDL_arraysize(samples), &bitstream);
|
|
if (amount < 0) {
|
|
return set_op_error("op_read_float", amount);
|
|
} else if (amount == 0) {
|
|
return false; // EOF
|
|
}
|
|
|
|
SDL_assert((amount * channels) <= (int)SDL_arraysize(samples));
|
|
|
|
if (bitstream != tdata->current_bitstream) {
|
|
const OpusHead *info = opus.op_head(tdata->of, -1);
|
|
if (info) { // this _shouldn't_ be NULL, but if it is, we're just going on without it and hoping the stream format didn't change.
|
|
if (tdata->current_channels != info->channel_count) {
|
|
const SDL_AudioSpec spec = { SDL_AUDIO_F32, info->channel_count, 48000 };
|
|
SDL_SetAudioStreamFormat(stream, &spec, NULL);
|
|
tdata->current_channels = info->channel_count;
|
|
}
|
|
}
|
|
tdata->current_bitstream = bitstream;
|
|
}
|
|
|
|
const MIX_OggLoop *loop = &tdata->adata->loop;
|
|
if (tdata->current_iteration < 0) {
|
|
if (loop->active && ((tdata->current_iteration_frames + 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 (amount > available) {
|
|
amount = available;
|
|
}
|
|
|
|
SDL_assert(tdata->current_iteration_frames <= loop->len);
|
|
if ((tdata->current_iteration_frames + 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 (!OPUS_seek(tdata, nextframe)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
tdata->current_iteration = -1;
|
|
}
|
|
tdata->current_iteration_frames = 0;
|
|
}
|
|
}
|
|
|
|
if (amount > 0) {
|
|
SDL_PutAudioStreamData(stream, samples, amount * tdata->current_channels * sizeof (float));
|
|
tdata->current_iteration_frames += amount;
|
|
}
|
|
|
|
return true; // had more data to decode.
|
|
}
|
|
|
|
static bool SDLCALL OPUS_seek(void *track_userdata, Uint64 frame)
|
|
{
|
|
OPUS_TrackData *tdata = (OPUS_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.
|
|
}
|
|
}
|
|
|
|
// !!! FIXME: I assume op_raw_seek is faster if we're seeking to start, but I could be wrong.
|
|
const int rc = (frame == 0) ? opus.op_raw_seek(tdata->of, 0) : opus.op_pcm_seek(tdata->of, (Sint64) frame);
|
|
if (rc != 0) {
|
|
return set_op_error("op_pcm_seek", rc);
|
|
}
|
|
|
|
tdata->current_iteration = final_iteration;
|
|
tdata->current_iteration_frames = final_iteration_frames;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void SDLCALL OPUS_quit_track(void *track_userdata)
|
|
{
|
|
OPUS_TrackData *tdata = (OPUS_TrackData *) track_userdata;
|
|
opus.op_free(tdata->of);
|
|
SDL_free(tdata);
|
|
}
|
|
|
|
static void SDLCALL OPUS_quit_audio(void *audio_userdata)
|
|
{
|
|
OPUS_AudioData *adata = (OPUS_AudioData *) audio_userdata;
|
|
SDL_free(adata);
|
|
}
|
|
|
|
const MIX_Decoder MIX_Decoder_OPUS = {
|
|
"OPUS",
|
|
OPUS_init,
|
|
OPUS_init_audio,
|
|
OPUS_init_track,
|
|
OPUS_decode,
|
|
OPUS_seek,
|
|
NULL, // jump_to_order
|
|
OPUS_quit_track,
|
|
OPUS_quit_audio,
|
|
OPUS_quit
|
|
};
|
|
|
|
#endif
|