Files
Mikemon/libs/SDL3_mixer/src/decoder_vorbis.c
T
2026-04-02 16:52:07 +02:00

449 lines
16 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 Vorbis audio streams using libvorbisfile (or the integer-only implementation, "tremor").
#ifdef DECODER_OGGVORBIS_VORBISFILE
#include "SDL_mixer_internal.h"
#define OV_EXCLUDE_STATIC_CALLBACKS
#if defined(VORBIS_HEADER)
#include VORBIS_HEADER
#elif defined(VORBIS_USE_TREMOR)
#include <tremor/ivorbisfile.h>
#else
#include <vorbis/vorbisfile.h>
#endif
#if defined(MPG123_DYNAMIC) && defined(SDL_ELF_NOTE_DLOPEN)
SDL_ELF_NOTE_DLOPEN(
"vorbis",
"Support for VORBIS audio", // vorbisfile or tremor
SDL_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
VORBIS_DYNAMIC
)
#endif
#ifdef VORBIS_DYNAMIC
#define MIX_LOADER_DYNAMIC VORBIS_DYNAMIC
#endif
#define MIX_LOADER_FUNCTIONS_vorbisbase \
MIX_LOADER_FUNCTION(true,int,ov_clear,(OggVorbis_File *vf)) \
MIX_LOADER_FUNCTION(true,vorbis_info *,ov_info,(OggVorbis_File *vf,int link)) \
MIX_LOADER_FUNCTION(true,vorbis_comment *,ov_comment,(OggVorbis_File *vf,int link)) \
MIX_LOADER_FUNCTION(true,int,ov_test_callbacks,(void *datasource, OggVorbis_File *vf, const char *initial, long ibytes, ov_callbacks callbacks)) \
MIX_LOADER_FUNCTION(true,int,ov_open_callbacks,(void *datasource, OggVorbis_File *vf, const char *initial, long ibytes, ov_callbacks callbacks)) \
MIX_LOADER_FUNCTION(true,ogg_int64_t,ov_pcm_total,(OggVorbis_File *vf,int i)) \
MIX_LOADER_FUNCTION(true,int,ov_pcm_seek,(OggVorbis_File *vf, ogg_int64_t pos)) \
MIX_LOADER_FUNCTION(true,int,ov_raw_seek,(OggVorbis_File *vf, ogg_int64_t pos)) \
MIX_LOADER_FUNCTION(true,ogg_int64_t,ov_pcm_tell,(OggVorbis_File *vf)) \
#ifdef VORBIS_USE_TREMOR
#define MIX_LOADER_FUNCTIONS \
MIX_LOADER_FUNCTIONS_vorbisbase \
MIX_LOADER_FUNCTION(true,long,ov_read,(OggVorbis_File *vf,char *buffer,int length, int *bitstream))
#else
#define MIX_LOADER_FUNCTIONS \
MIX_LOADER_FUNCTIONS_vorbisbase \
MIX_LOADER_FUNCTION(true,long,ov_read_float,(OggVorbis_File *vf, float ***pcm_channels, int samples, int *bitstream))
#endif
#define MIX_LOADER_MODULE vorbis
#include "SDL_mixer_loader.h"
#ifdef VORBIS_USE_TREMOR
#define VORBIS_AUDIO_FORMAT SDL_AUDIO_S16
#else
#define VORBIS_AUDIO_FORMAT SDL_AUDIO_F32
#endif
typedef struct VORBIS_AudioData
{
size_t framesize;
MIX_OggLoop loop;
} VORBIS_AudioData;
typedef struct VORBIS_TrackData
{
const VORBIS_AudioData *adata;
OggVorbis_File vf;
int current_channels;
int current_freq;
int current_bitstream;
Sint64 current_iteration;
Sint64 current_iteration_frames;
} VORBIS_TrackData;
static bool SDLCALL VORBIS_init(void)
{
return LoadModule_vorbis();
}
static void SDLCALL VORBIS_quit(void)
{
UnloadModule_vorbis();
}
static bool SetOggVorbisError(const char *function, int error)
{
switch (error) {
#define HANDLE_ERROR_CASE(X) case X: return SDL_SetError("%s: %s", function, #X)
HANDLE_ERROR_CASE(OV_FALSE);
HANDLE_ERROR_CASE(OV_EOF);
HANDLE_ERROR_CASE(OV_HOLE);
HANDLE_ERROR_CASE(OV_EREAD);
HANDLE_ERROR_CASE(OV_EFAULT);
HANDLE_ERROR_CASE(OV_EIMPL);
HANDLE_ERROR_CASE(OV_EINVAL);
HANDLE_ERROR_CASE(OV_ENOTVORBIS);
HANDLE_ERROR_CASE(OV_EBADHEADER);
HANDLE_ERROR_CASE(OV_EVERSION);
HANDLE_ERROR_CASE(OV_ENOTAUDIO);
HANDLE_ERROR_CASE(OV_EBADPACKET);
HANDLE_ERROR_CASE(OV_EBADLINK);
HANDLE_ERROR_CASE(OV_ENOSEEK);
#undef HANDLE_ERROR_CASE
default: break;
}
return SDL_SetError("%s: unknown error %d", function, error);
}
static size_t VORBIS_IoRead(void *ptr, size_t size, size_t nmemb, void *datasource)
{
if (size > 0 && nmemb > 0) {
return SDL_ReadIO((SDL_IOStream*)datasource, ptr, size * nmemb) / size;
}
return 0;
}
static int VORBIS_IoSeek(void *datasource, ogg_int64_t offset, int whence)
{
return (SDL_SeekIO((SDL_IOStream*)datasource, offset, whence) < 0) ? -1 : 0;
}
static long VORBIS_IoTell(void *datasource)
{
return (long) SDL_TellIO((SDL_IOStream*)datasource);
}
static int VORBIS_IoClose(void *datasource)
{
(void)datasource;
return 0;
}
static const ov_callbacks VORBIS_IoCallbacks = { VORBIS_IoRead, VORBIS_IoSeek, VORBIS_IoClose, VORBIS_IoTell };
static bool SDLCALL VORBIS_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 Ogg Vorbis file.
// !!! FIXME: is ov_open_callbacks going to return more slowly if this isn't an Opus file? It's probably better to just do the full open.
OggVorbis_File vf;
if (vorbis.ov_test_callbacks(io, &vf, NULL, 0, VORBIS_IoCallbacks) < 0) {
return SDL_SetError("Not an Ogg Vorbis audio stream");
}
vorbis.ov_clear(&vf);
// Go back and do a proper load now to get metadata.
if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) < 0) {
return false;
}
VORBIS_AudioData *adata = (VORBIS_AudioData *) SDL_calloc(1, sizeof (*adata));
if (!adata) {
return false;
}
// now open the stream for serious processing.
const int rc = vorbis.ov_open_callbacks(io, &vf, NULL, 0, VORBIS_IoCallbacks);
if (rc < 0) {
SDL_free(adata);
return SetOggVorbisError("ov_open_callbacks", rc);
}
const vorbis_info *vi = vorbis.ov_info(&vf, -1);
if (!vi) {
vorbis.ov_clear(&vf);
SDL_free(adata);
return SDL_SetError("Couldn't get Ogg Vorbis info; corrupt data?");
}
spec->format = VORBIS_AUDIO_FORMAT;
spec->channels = vi->channels;
spec->freq = vi->rate;
adata->framesize = SDL_AUDIO_FRAMESIZE(*spec);
vorbis_comment *vc = vorbis.ov_comment(&vf, -1);
if (vc != NULL) {
MIX_ParseOggComments(props, spec->freq, vc->vendor, (const char * const *) vc->user_comments, vc->comments, &adata->loop);
}
vorbis.ov_raw_seek(&vf, 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) vorbis.ov_pcm_total(&vf, -1);
if (adata->loop.end > full_length) {
adata->loop.active = false;
}
vorbis.ov_clear(&vf); // done with this instance. Tracks will maintain their own OggVorbis_File 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 VORBIS_init_track(void *audio_userdata, SDL_IOStream *io, const SDL_AudioSpec *spec, SDL_PropertiesID props, void **track_userdata)
{
VORBIS_TrackData *tdata = (VORBIS_TrackData *) SDL_calloc(1, sizeof (*tdata));
if (!tdata) {
return false;
}
const VORBIS_AudioData *adata = (const VORBIS_AudioData *) audio_userdata;
// now open the stream for serious processing.
int rc = vorbis.ov_open_callbacks(io, &tdata->vf, NULL, 0, VORBIS_IoCallbacks);
if (rc < 0) {
SDL_free(tdata);
return SetOggVorbisError("ov_open_callbacks", rc);
}
tdata->current_channels = spec->channels;
tdata->current_freq = spec->freq;
tdata->current_bitstream = -1;
tdata->current_iteration = -1;
tdata->adata = adata;
*track_userdata = tdata;
return true;
}
static void UpdateVorbisStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *spec)
{
static const int vorbis_chmap_3[] = { 0, 2, 1, -1, -1, -1 }; // SDL=FL,FR,FC,LFE,RL,RR VORBIS=FL,FC,FR
static const int vorbis_chmap_5[] = { 0, 2, 1, 4, 5, -1}; // SDL=FL,FR,FC,LFE,RL,RR VORBIS=FL,FC,FR,RL,RR
static const int vorbis_chmap_6[] = { 0, 2, 1, 4, 5, 3 }; // SDL=FL,FR,FC,LFE,RL,RR VORBIS=FL,FC,FR,RL,RR,LFE
static const int vorbis_chmap_7[] = { 0, 2, 1, 5, 6, 4, 3 }; // SDL=FL,FR,FC,LFE,RC,SL,SR VORBIS=FL,FC,FR,SL,SR,RC,LFE
static const int vorbis_chmap_8[] = { 0, 2, 1, 6, 7, 4, 5, 3 }; // SDL=FL,FR,FC,LFE,RL,RR,SL,SR VORBIS=FL,FC,FR,SL,SR,RL,RR,LFE
SDL_AudioSpec bumped_spec;
const int *chmap = NULL;
switch (spec->channels) {
// 3 and 5 channel audio offers a Front Center channel, which SDL doesn't have until 5.1 (6 channels), so bump the spec
// to 5.1 and provide NULL planar channels where appropriate.
// !!! FIXME: Since Tremor doesn't provide planar data, we just pass this through in the wrong channel order there,
// !!! FIXME: which is dumb but who is using these formats in the first place, let alone with low-powered Tremor?
#ifndef VORBIS_USE_TREMOR
case 3:
SDL_copyp(&bumped_spec, spec);
bumped_spec.channels = 6;
spec = &bumped_spec;
chmap = vorbis_chmap_3;
break;
case 5:
SDL_copyp(&bumped_spec, spec);
bumped_spec.channels = 6;
spec = &bumped_spec;
chmap = vorbis_chmap_5;
break;
#endif
case 6: chmap = vorbis_chmap_6; break;
case 7: chmap = vorbis_chmap_7; break;
case 8: chmap = vorbis_chmap_8; break;
default:
// no special mapping, pass through as-is.
break;
}
SDL_SetAudioStreamFormat(stream, spec, NULL);
SDL_SetAudioStreamInputChannelMap(stream, chmap, spec->channels);
}
static bool SDLCALL VORBIS_seek(void *track_userdata, Uint64 frame);
static bool SDLCALL VORBIS_decode(void *track_userdata, SDL_AudioStream *stream)
{
VORBIS_TrackData *tdata = (VORBIS_TrackData *) track_userdata;
int bitstream = tdata->current_bitstream;
#ifdef VORBIS_USE_TREMOR
const size_t framesize = tdata->adata->framesize;
Uint8 samples[256];
int amount = (int)vorbis.ov_read(&tdata->vf, (char *) samples, sizeof (samples), &bitstream);
if (amount < 0) {
return SetOggVorbisError("ov_read", amount);
}
amount *= framesize;
#else
float **pcm_channels = NULL;
int amount = (int)vorbis.ov_read_float(&tdata->vf, &pcm_channels, 256, &bitstream);
if (amount < 0) {
return SetOggVorbisError("ov_read_float", amount);
}
#endif
if (bitstream != tdata->current_bitstream) {
const vorbis_info *vi = vorbis.ov_info(&tdata->vf, -1);
if (vi) { // 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 != vi->channels) || (tdata->current_freq != vi->rate)) {
const SDL_AudioSpec spec = { VORBIS_AUDIO_FORMAT, vi->channels, vi->rate };
tdata->current_channels = vi->channels;
tdata->current_freq = vi->rate;
UpdateVorbisStreamFormat(stream, &spec);
}
}
tdata->current_bitstream = bitstream;
}
if (amount == 0) {
return false; // EOF
}
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 (!VORBIS_seek(tdata, nextframe)) {
return false;
}
} else {
tdata->current_iteration = -1;
}
tdata->current_iteration_frames = 0;
}
}
if (amount > 0) {
#ifdef VORBIS_USE_TREMOR
SDL_PutAudioStreamData(stream, samples, amount / framesize);
#else
SDL_PutAudioStreamPlanarData(stream, (const void * const *) pcm_channels, tdata->current_channels, amount);
#endif
tdata->current_iteration_frames += amount;
}
return true; // had more data to decode.
}
static bool SDLCALL VORBIS_seek(void *track_userdata, Uint64 frame)
{
VORBIS_TrackData *tdata = (VORBIS_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 ov_raw_seek is faster if we're seeking to start, but I could be wrong.
const int rc = (frame == 0) ? vorbis.ov_raw_seek(&tdata->vf, 0) : vorbis.ov_pcm_seek(&tdata->vf, (ogg_int64_t) frame);
if (rc != 0) {
return SetOggVorbisError("ov_pcm_seek", rc);
}
tdata->current_iteration = final_iteration;
tdata->current_iteration_frames = final_iteration_frames;
return true;
}
static void SDLCALL VORBIS_quit_track(void *track_userdata)
{
VORBIS_TrackData *tdata = (VORBIS_TrackData *) track_userdata;
vorbis.ov_clear(&tdata->vf);
SDL_free(tdata);
}
static void SDLCALL VORBIS_quit_audio(void *audio_userdata)
{
SDL_free(audio_userdata);
}
const MIX_Decoder MIX_Decoder_VORBIS = {
"VORBIS",
VORBIS_init,
VORBIS_init_audio,
VORBIS_init_track,
VORBIS_decode,
VORBIS_seek,
VORBIS_quit_track,
VORBIS_quit_audio,
VORBIS_quit
};
#endif