Add SDL decoders for their corresponding codecs
This commit is contained in:
parent
85039a6033
commit
f39b51fa78
11 changed files with 1826 additions and 18 deletions
|
@ -23,7 +23,7 @@
|
|||
*
|
||||
* Documentation is in SDL_sound.h ... It's verbose, honest. :)
|
||||
*
|
||||
* Please see the file LICENSE.txt in the source's root directory.
|
||||
* Please see the file src/libs/decoders/docs/LICENSE.txt.
|
||||
*
|
||||
* This file written by Ryan C. Gordon. (icculus@icculus.org)
|
||||
*/
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
* - .OPUS (Ogg Opus support via the Opusfile and SpeexDSP libraries)
|
||||
* - .FLAC (Free Lossless Audio Codec support via the dr_flac single-header decoder)
|
||||
*
|
||||
* Please see the file LICENSE.txt in the source's root directory.
|
||||
* Please see the file src/libs/decoders/docs/LICENSE.txt.
|
||||
*
|
||||
* \author Ryan C. Gordon (icculus@icculus.org)
|
||||
* \author many others, please see CREDITS in the source's root directory.
|
||||
|
@ -722,21 +722,6 @@ SNDDECLSPEC int SDLCALL Sound_Seek(Sound_Sample *sample, Uint32 ms);
|
|||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
inline Sound_SampleFlags operator|(Sound_SampleFlags a, Sound_SampleFlags b)
|
||||
{return static_cast<Sound_SampleFlags>(static_cast<int>(a) | static_cast<int>(b));}
|
||||
|
||||
inline Sound_SampleFlags& operator|= (Sound_SampleFlags& a, Sound_SampleFlags b)
|
||||
{ return (Sound_SampleFlags&)((int&)a |= static_cast<int>(b)); }
|
||||
|
||||
inline Sound_SampleFlags operator& (Sound_SampleFlags a, Sound_SampleFlags b)
|
||||
{ return (Sound_SampleFlags)((int)a & (int)b); }
|
||||
|
||||
inline Sound_SampleFlags& operator&= (Sound_SampleFlags& a, Sound_SampleFlags b)
|
||||
{ return (Sound_SampleFlags&)((int&)a &= (int)b); }
|
||||
#endif
|
||||
*/
|
||||
|
||||
#endif /* !defined _INCLUDE_SDL_SOUND_H_ */
|
||||
|
||||
/* end of SDL_sound.h ... */
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
* Internal function/structure declaration. Do NOT include in your
|
||||
* application.
|
||||
*
|
||||
* Please see the file LICENSE.txt in the source's root directory.
|
||||
* Please see the file src/libs/decoders/docs/LICENSE.txt.
|
||||
*
|
||||
* This file written by Ryan C. Gordon. (icculus@icculus.org)
|
||||
*/
|
||||
|
|
181
src/libs/decoders/flac.c
Normal file
181
src/libs/decoders/flac.c
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* DOSBox FLAC decoder is maintained by Kevin R. Croft (krcroft@gmail.com)
|
||||
* This decoder makes use of the excellent dr_flac library by David Reid (mackron@gmail.com)
|
||||
*
|
||||
* Source links
|
||||
* - dr_libs: https://github.com/mackron/dr_libs (source)
|
||||
* - dr_flac: http://mackron.github.io/dr_flac.html (website)
|
||||
*
|
||||
* The upstream SDL2 Sound 1.9.x FLAC decoder is written and copyright by Ryan C. Gordon. (icculus@icculus.org)
|
||||
*
|
||||
* Please see the file src/libs/decoders/docs/LICENSE.txt.
|
||||
*
|
||||
* This file is part of the SDL Sound Library.
|
||||
*
|
||||
* This SDL_sound FLAC decoder backend is free software: you can redistribute
|
||||
* it and/or modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This SDL_sound Ogg Opus decoder backend is distributed in the hope that it
|
||||
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with the SDL Sound Library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#if HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <math.h> /* for llroundf */
|
||||
|
||||
#include "SDL_sound.h"
|
||||
#define __SDL_SOUND_INTERNAL__
|
||||
#include "SDL_sound_internal.h"
|
||||
|
||||
#define DR_FLAC_IMPLEMENTATION
|
||||
#define DR_FLAC_NO_STDIO 1
|
||||
#define DR_FLAC_NO_WIN32_IO 1
|
||||
#define DRFLAC_MALLOC(sz) SDL_malloc((sz))
|
||||
#define DRFLAC_REALLOC(p, sz) SDL_realloc((p), (sz))
|
||||
#define DRFLAC_FREE(p) SDL_free((p))
|
||||
#define DRFLAC_COPY_MEMORY(dst, src, sz) SDL_memcpy((dst), (src), (sz))
|
||||
#define DRFLAC_ZERO_MEMORY(p, sz) SDL_memset((p), 0, (sz))
|
||||
#include "dr_flac.h"
|
||||
|
||||
static size_t flac_read(void* pUserData, void* pBufferOut, size_t bytesToRead)
|
||||
{
|
||||
Uint8 *ptr = (Uint8 *) pBufferOut;
|
||||
Sound_Sample *sample = (Sound_Sample *) pUserData;
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
SDL_RWops *rwops = internal->rw;
|
||||
size_t retval = 0;
|
||||
|
||||
while (retval < bytesToRead)
|
||||
{
|
||||
const size_t rc = SDL_RWread(rwops, ptr, 1, bytesToRead);
|
||||
if (rc == 0) {
|
||||
sample->flags |= SOUND_SAMPLEFLAG_EOF;
|
||||
break;
|
||||
} /* if */
|
||||
retval += rc;
|
||||
ptr += rc;
|
||||
} /* while */
|
||||
|
||||
return retval;
|
||||
} /* flac_read */
|
||||
|
||||
static drflac_bool32 flac_seek(void* pUserData, int offset, drflac_seek_origin origin)
|
||||
{
|
||||
const int whence = (origin == drflac_seek_origin_start) ? RW_SEEK_SET : RW_SEEK_CUR;
|
||||
Sound_Sample *sample = (Sound_Sample *) pUserData;
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
return (SDL_RWseek(internal->rw, offset, whence) != -1) ? DRFLAC_TRUE : DRFLAC_FALSE;
|
||||
} /* flac_seek */
|
||||
|
||||
|
||||
static int FLAC_init(void)
|
||||
{
|
||||
return 1; /* always succeeds. */
|
||||
} /* FLAC_init */
|
||||
|
||||
|
||||
static void FLAC_quit(void)
|
||||
{
|
||||
/* it's a no-op. */
|
||||
} /* FLAC_quit */
|
||||
|
||||
static int FLAC_open(Sound_Sample *sample, const char *ext)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drflac *dr = drflac_open(flac_read, flac_seek, sample, NULL);
|
||||
|
||||
if (!dr) {
|
||||
BAIL_IF_MACRO(sample->flags & SOUND_SAMPLEFLAG_ERROR, ERR_IO_ERROR, 0);
|
||||
BAIL_MACRO("FLAC: Not a FLAC stream.", 0);
|
||||
} /* if */
|
||||
|
||||
SNDDBG(("FLAC: Accepting data stream.\n"));
|
||||
sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
|
||||
|
||||
sample->actual.channels = dr->channels;
|
||||
sample->actual.rate = dr->sampleRate;
|
||||
sample->actual.format = AUDIO_S16SYS; /* returns native byte-order based on architecture */
|
||||
|
||||
const Uint64 frames = (Uint64) dr->totalPCMFrameCount;
|
||||
if (frames == 0) {
|
||||
internal->total_time = -1;
|
||||
}
|
||||
else {
|
||||
const Uint32 rate = (Uint32) dr->sampleRate;
|
||||
internal->total_time = ( (Sint32)frames / rate) * 1000;
|
||||
internal->total_time += ((frames % rate) * 1000) / rate;
|
||||
} /* else */
|
||||
|
||||
internal->decoder_private = dr;
|
||||
|
||||
return 1;
|
||||
} /* FLAC_open */
|
||||
|
||||
|
||||
|
||||
static void FLAC_close(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drflac *dr = (drflac *) internal->decoder_private;
|
||||
drflac_close(dr);
|
||||
} /* FLAC_close */
|
||||
|
||||
|
||||
static Uint32 FLAC_read(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drflac *dr = (drflac *) internal->decoder_private;
|
||||
const drflac_uint64 rc = drflac_read_pcm_frames_s16(dr,
|
||||
internal->buffer_size / (dr->channels * sizeof(drflac_int16)),
|
||||
(drflac_int16 *) internal->buffer);
|
||||
return (Uint32) rc * dr->channels * sizeof (drflac_int16);
|
||||
} /* FLAC_read */
|
||||
|
||||
|
||||
static int FLAC_rewind(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drflac *dr = (drflac *) internal->decoder_private;
|
||||
return (drflac_seek_to_pcm_frame(dr, 0) == DRFLAC_TRUE);
|
||||
} /* FLAC_rewind */
|
||||
|
||||
static int FLAC_seek(Sound_Sample *sample, Uint32 ms)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drflac *dr = (drflac *) internal->decoder_private;
|
||||
const float frames_per_ms = ((float) sample->actual.rate) / 1000.0f;
|
||||
const drflac_uint64 frame_offset = llroundf(frames_per_ms * ms);
|
||||
return (drflac_seek_to_pcm_frame(dr, frame_offset) == DRFLAC_TRUE);
|
||||
} /* FLAC_seek */
|
||||
|
||||
|
||||
static const char *extensions_flac[] = { "FLAC", "FLA", NULL };
|
||||
const Sound_DecoderFunctions __Sound_DecoderFunctions_FLAC =
|
||||
{
|
||||
{
|
||||
extensions_flac,
|
||||
"Free Lossless Audio Codec",
|
||||
"Ryan C. Gordon <icculus@icculus.org>",
|
||||
"https://icculus.org/SDL_sound/"
|
||||
},
|
||||
|
||||
FLAC_init, /* init() method */
|
||||
FLAC_quit, /* quit() method */
|
||||
FLAC_open, /* open() method */
|
||||
FLAC_close, /* close() method */
|
||||
FLAC_read, /* read() method */
|
||||
FLAC_rewind, /* rewind() method */
|
||||
FLAC_seek /* seek() method */
|
||||
};
|
||||
|
||||
/* end of flac.c ... */
|
220
src/libs/decoders/mp3.cpp
Normal file
220
src/libs/decoders/mp3.cpp
Normal file
|
@ -0,0 +1,220 @@
|
|||
/**
|
||||
* This DOSBox mp3 decooder backend is maintained by Kevin R. Croft (krcroft@gmail.com)
|
||||
* This decoder makes use of the following single-header public libraries:
|
||||
* - dr_mp3: http://mackron.github.io/dr_mp3.html, by David Reid
|
||||
*
|
||||
* The upstream SDL2 Sound 1.9.x mp3 decoder is written and copyright by Ryan C. Gordon. (icculus@icculus.org)
|
||||
*
|
||||
* This SDL_sound MP3 decoder backend is free software: you can redistribute
|
||||
* it and/or modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This MP3 decoder backend is distributed in the hope that it
|
||||
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with the SDL Sound Library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
#if HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <SDL.h> // provides: SDL_malloc, SDL_realloc, SDL_free, SDL_memcpy, and SDL_memset
|
||||
#define DR_MP3_IMPLEMENTATION
|
||||
#define DR_MP3_NO_STDIO 1
|
||||
#define DRMP3_ASSERT(x) assert((x))
|
||||
#define DRMP3_MALLOC(sz) SDL_malloc((sz))
|
||||
#define DRMP3_REALLOC(p, sz) SDL_realloc((p), (sz))
|
||||
#define DRMP3_FREE(p) SDL_free((p))
|
||||
#define DRMP3_COPY_MEMORY(dst, src, sz) SDL_memcpy((dst), (src), (sz))
|
||||
#define DRMP3_ZERO_MEMORY(p, sz) SDL_memset((p), 0, (sz))
|
||||
#include "dr_mp3.h" // provides: drmp3
|
||||
|
||||
#include "mp3_seek_table.h" // provides: populate_seek_table and SDL_Sound headers
|
||||
|
||||
#include "SDL_sound.h"
|
||||
#define __SDL_SOUND_INTERNAL__
|
||||
#include "SDL_sound_internal.h" // provides: Sound_SampleInternal
|
||||
|
||||
#define MP3_FAST_SEEK_FILENAME "fastseek.lut"
|
||||
|
||||
static size_t mp3_read(void* const pUserData, void* const pBufferOut, const size_t bytesToRead)
|
||||
{
|
||||
Uint8* ptr = static_cast<Uint8*>(pBufferOut);
|
||||
Sound_Sample* const sample = static_cast<Sound_Sample* const>(pUserData);
|
||||
const Sound_SampleInternal* const internal = static_cast<const Sound_SampleInternal*>(sample->opaque);
|
||||
SDL_RWops* rwops = internal->rw;
|
||||
size_t retval = 0;
|
||||
|
||||
while (retval < bytesToRead)
|
||||
{
|
||||
const size_t rc = SDL_RWread(rwops, ptr, 1, bytesToRead);
|
||||
if (rc == 0) {
|
||||
sample->flags |= SOUND_SAMPLEFLAG_EOF;
|
||||
break;
|
||||
} /* if */
|
||||
retval += rc;
|
||||
ptr += rc;
|
||||
} /* while */
|
||||
|
||||
return retval;
|
||||
} /* mp3_read */
|
||||
|
||||
static drmp3_bool32 mp3_seek(void* const pUserData, const Sint32 offset, const drmp3_seek_origin origin)
|
||||
{
|
||||
const Sint32 whence = (origin == drmp3_seek_origin_start) ? RW_SEEK_SET : RW_SEEK_CUR;
|
||||
Sound_Sample* const sample = static_cast<Sound_Sample*>(pUserData);
|
||||
Sound_SampleInternal* const internal = static_cast<Sound_SampleInternal*>(sample->opaque);
|
||||
return (SDL_RWseek(internal->rw, offset, whence) != -1) ? DRMP3_TRUE : DRMP3_FALSE;
|
||||
} /* mp3_seek */
|
||||
|
||||
|
||||
static Sint32 MP3_init(void)
|
||||
{
|
||||
return 1; /* always succeeds. */
|
||||
} /* MP3_init */
|
||||
|
||||
|
||||
static void MP3_quit(void)
|
||||
{
|
||||
/* it's a no-op. */
|
||||
} /* MP3_quit */
|
||||
|
||||
static void MP3_close(Sound_Sample* const sample)
|
||||
{
|
||||
Sound_SampleInternal* const internal = static_cast<Sound_SampleInternal* const>(sample->opaque);
|
||||
mp3_t* p_mp3 = static_cast<mp3_t*>(internal->decoder_private);
|
||||
if (p_mp3 != NULL) {
|
||||
if (p_mp3->p_dr != NULL) {
|
||||
drmp3_uninit(p_mp3->p_dr);
|
||||
SDL_free(p_mp3->p_dr);
|
||||
}
|
||||
// maps and vector destructors free their memory
|
||||
SDL_free(p_mp3);
|
||||
internal->decoder_private = NULL;
|
||||
}
|
||||
} /* MP3_close */
|
||||
|
||||
static Uint32 MP3_read(Sound_Sample* const sample)
|
||||
{
|
||||
Sound_SampleInternal* const internal = static_cast<Sound_SampleInternal* const>(sample->opaque);
|
||||
const Sint32 channels = (Sint32) sample->actual.channels;
|
||||
mp3_t* p_mp3 = static_cast<mp3_t*>(internal->decoder_private);
|
||||
|
||||
// setup our 32-bit input buffer
|
||||
float in_buffer[4096];
|
||||
const drmp3_uint16 in_buffer_frame_capacity = 4096 / channels;
|
||||
|
||||
// setup our 16-bit output buffer
|
||||
drmp3_int16* out_buffer = static_cast<drmp3_int16*>(internal->buffer);
|
||||
drmp3_uint16 remaining_frames = (internal->buffer_size / sizeof(drmp3_int16)) / channels;
|
||||
|
||||
// LOG_MSG("read: remaining_frames: %u", remaining_frames);
|
||||
drmp3_uint16 total_samples_read = 0;
|
||||
while (remaining_frames > 0) {
|
||||
const drmp3_uint16 num_frames = (remaining_frames > in_buffer_frame_capacity) ? in_buffer_frame_capacity : remaining_frames;
|
||||
|
||||
// LOG_MSG("read-while: num_frames: %u", num_frames);
|
||||
const drmp3_uint16 frames_just_read = static_cast<drmp3_uint16>(drmp3_read_pcm_frames_f32(p_mp3->p_dr, num_frames, in_buffer));
|
||||
|
||||
// LOG_MSG("read-while: frames_just_read: %u", frames_just_read);
|
||||
if (frames_just_read == 0) {
|
||||
break; // Reached the end.
|
||||
}
|
||||
|
||||
const drmp3_uint16 samples_just_read = frames_just_read * channels;
|
||||
|
||||
// f32 -> s16
|
||||
drmp3dec_f32_to_s16(in_buffer, out_buffer, samples_just_read);
|
||||
|
||||
remaining_frames -= frames_just_read;
|
||||
out_buffer += samples_just_read;
|
||||
total_samples_read += samples_just_read;
|
||||
}
|
||||
// SNDDBG(("encoded stream offset: %d", SDL_RWtell(internal->rw) ));
|
||||
|
||||
return total_samples_read * sizeof(drmp3_int16);
|
||||
} /* MP3_read */
|
||||
|
||||
static Sint32 MP3_open(Sound_Sample* const sample, const char* const ext)
|
||||
{
|
||||
Sound_SampleInternal* const internal = static_cast<Sound_SampleInternal*>(sample->opaque);
|
||||
Sint32 result(0); // assume failure until proven otherwise
|
||||
mp3_t* p_mp3 = (mp3_t*) SDL_calloc(1, sizeof (mp3_t));
|
||||
if (p_mp3 != NULL) {
|
||||
p_mp3->p_dr = (drmp3*) SDL_calloc(1, sizeof (drmp3));
|
||||
if (p_mp3->p_dr != NULL) {
|
||||
result = drmp3_init(p_mp3->p_dr, mp3_read, mp3_seek, sample, NULL, NULL);
|
||||
if (result == DRMP3_TRUE) {
|
||||
SNDDBG(("MP3: Accepting data stream.\n"));
|
||||
sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
|
||||
sample->actual.channels = p_mp3->p_dr->channels;
|
||||
sample->actual.rate = p_mp3->p_dr->sampleRate;
|
||||
sample->actual.format = AUDIO_S16SYS; // returns native byte-order based on architecture
|
||||
const Uint64 num_frames = populate_seek_points(internal->rw, p_mp3, MP3_FAST_SEEK_FILENAME); // status will be 0 or pcm_frame_count
|
||||
if (num_frames != 0) {
|
||||
const unsigned int rate = p_mp3->p_dr->sampleRate;
|
||||
internal->total_time = ( static_cast<Sint32>(num_frames) / rate) * 1000;
|
||||
internal->total_time += (num_frames % rate) * 1000 / rate;
|
||||
result = 1;
|
||||
} else {
|
||||
internal->total_time = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign our internal decoder to the mp3 object we've just populated
|
||||
internal->decoder_private = p_mp3;
|
||||
|
||||
// if anything went wrong then tear down our private structure
|
||||
if (result == 0) {
|
||||
MP3_close(sample);
|
||||
}
|
||||
|
||||
return static_cast<Sint32>(result);
|
||||
} /* MP3_open */
|
||||
|
||||
static Sint32 MP3_rewind(Sound_Sample* const sample)
|
||||
{
|
||||
Sound_SampleInternal* const internal = static_cast<Sound_SampleInternal*>(sample->opaque);
|
||||
mp3_t* p_mp3 = static_cast<mp3_t*>(internal->decoder_private);
|
||||
return (drmp3_seek_to_start_of_stream(p_mp3->p_dr) == DRMP3_TRUE);
|
||||
} /* MP3_rewind */
|
||||
|
||||
static Sint32 MP3_seek(Sound_Sample* const sample, const Uint32 ms)
|
||||
{
|
||||
Sound_SampleInternal* const internal = static_cast<Sound_SampleInternal* const>(sample->opaque);
|
||||
mp3_t* p_mp3 = static_cast<mp3_t*>(internal->decoder_private);
|
||||
const float frames_per_ms = sample->actual.rate / 1000.0f;
|
||||
const drmp3_uint64 frame_offset = static_cast<drmp3_uint64>(frames_per_ms) * ms;
|
||||
const Sint32 result = drmp3_seek_to_pcm_frame(p_mp3->p_dr, frame_offset);
|
||||
return (result == DRMP3_TRUE);
|
||||
} /* MP3_seek */
|
||||
|
||||
/* dr_mp3 will play layer 1 and 2 files, too */
|
||||
static const char* extensions_mp3[] = { "MP3", "MP2", "MP1", NULL };
|
||||
|
||||
extern const Sound_DecoderFunctions __Sound_DecoderFunctions_MP3 = {
|
||||
{
|
||||
extensions_mp3,
|
||||
"MPEG-1 Audio Layer I-III",
|
||||
"Ryan C. Gordon <icculus@icculus.org>",
|
||||
"https://icculus.org/SDL_sound/"
|
||||
},
|
||||
|
||||
MP3_init, /* init() method */
|
||||
MP3_quit, /* quit() method */
|
||||
MP3_open, /* open() method */
|
||||
MP3_close, /* close() method */
|
||||
MP3_read, /* read() method */
|
||||
MP3_rewind, /* rewind() method */
|
||||
MP3_seek /* seek() method */
|
||||
}; }
|
||||
/* end of SDL_sound_mp3.c ... */
|
346
src/libs/decoders/mp3_seek_table.cpp
Normal file
346
src/libs/decoders/mp3_seek_table.cpp
Normal file
|
@ -0,0 +1,346 @@
|
|||
/**
|
||||
* DOSBox MP3 Seek Table handler, Copyright 2018 Kevin R. Croft (krcroft@gmail.com)
|
||||
*
|
||||
* Problem:
|
||||
* Seeking within an MP3 file to an exact time-offset, such as is expected
|
||||
* within DOS games, is extremely difficult because the MP3 format doesn't
|
||||
* provide a defined relationship between the compressed data stream positions
|
||||
* versus decompressed PCM times.
|
||||
*
|
||||
* Solution:
|
||||
* To solve this, we step through each compressed MP3 frames in
|
||||
* the MP3 file (without decoding the actual audio) and keep a record of the
|
||||
* decompressed "PCM" times for each frame. We save this relationship to
|
||||
* to a local fie, called a fast-seek look-up table, which we can quickly
|
||||
* reuse every subsequent time we need to seek within the MP3 file. This allows
|
||||
* seeks to be performed extremely fast while being PCM-exact.
|
||||
*
|
||||
* This "fast-seek" file can hold data for multiple MP3s to avoid
|
||||
* creating an excessive number of files in the local working directory.
|
||||
*
|
||||
* Challenges:
|
||||
* 1. What happens if an MP3 file is changed but the MP3's filename remains the same?
|
||||
*
|
||||
* The lookup table is indexed based on a checksum instead of filename.
|
||||
* The checksum is calculated based on a subset of the MP3's content in
|
||||
* addition to being seeded based on the MP3's size in bytes.
|
||||
* This makes it very sensitive to changes in MP3 content; if a change
|
||||
* is detected a new lookup table is generated.
|
||||
*
|
||||
* 2. Checksums can be weak, what if a collision happens?
|
||||
*
|
||||
* To avoid the risk of collision, we use the current best-of-breed
|
||||
* xxHash algorithm that has a quality-score of 10, the highest rating
|
||||
* from the SMHasher test set. See https://github.com/Cyan4973/xxHash
|
||||
* for more details.
|
||||
*
|
||||
* 3. What happens if fast-seek file is brought from a little-endian
|
||||
* machine to a big-endian machine (x86 or ARM to a PowerPC or Sun
|
||||
* Sparc machine)?
|
||||
*
|
||||
* The lookup table is serialized and multi-byte types are byte-swapped
|
||||
* at runtime according to the architecture. This makes fast-seek files
|
||||
* cross-compatible regardless of where they were written to or read from.
|
||||
*
|
||||
* 4. What happens if this code is updated to use a new fast-seek file
|
||||
* format, but an old fast-seek file exists?
|
||||
*
|
||||
* The seek-table file is versioned (see SEEK_TABLE_IDENTIFIER befow),
|
||||
* therefore, if the format and version is updated, then the seek-table
|
||||
* will be regenerated.
|
||||
|
||||
* The seek table handler makes use of the following single-header public libraries:
|
||||
* - dr_mp3: http://mackron.github.io/dr_mp3.html, by David Reid
|
||||
* - archive: https://github.com/voidah/archive, by Arthur Ouellet
|
||||
* - xxHash: http://cyan4973.github.io/xxHash, by Yann Collet
|
||||
*
|
||||
* This seek table handler is free software: you can redistribute
|
||||
* it and/or modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DOSBox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#if HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
// System headers
|
||||
#include <sys/stat.h>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
// Local headers
|
||||
#include "xxhash.h"
|
||||
// #include "../../../include/logging.h"
|
||||
#include "mp3_seek_table.h"
|
||||
|
||||
// C++ scope modifiers
|
||||
using std::map;
|
||||
using std::vector;
|
||||
using std::string;
|
||||
using std::ios_base;
|
||||
using std::ifstream;
|
||||
using std::ofstream;
|
||||
|
||||
// Identifies a valid versioned seek-table
|
||||
#define SEEK_TABLE_IDENTIFIER "st-v3"
|
||||
|
||||
// How many compressed MP3 frames should we skip between each recorded
|
||||
// time point. The trade-off is as follows:
|
||||
// - a large number means slower in-game seeking but a smaller fast-seek file.
|
||||
// - a smaller numbers (below 10) results in fast seeks on slow hardware.
|
||||
#define FRAMES_PER_SEEK_POINT 7
|
||||
|
||||
// Returns the size of a file in bytes (if valid), otherwise 0
|
||||
const size_t get_file_size(const char* filename) {
|
||||
struct stat stat_buf;
|
||||
int rc = stat(filename, &stat_buf);
|
||||
return rc == 0 ? stat_buf.st_size : -1;
|
||||
}
|
||||
|
||||
|
||||
// Calculates a unique 64-bit hash (integer) from the provided file.
|
||||
// This function should not cause side-effects; ie, the current
|
||||
// read-position within the file should not be altered.
|
||||
//
|
||||
// This function tries to files as-close to the middle of the MP3 file as possible,
|
||||
// and use that feed the hash function in hopes of the most uniqueness.
|
||||
// We're trying to avoid content that might be duplicated across MP3s, like:
|
||||
// 1. ID3 tag filler content, which might be boiler plate or all empty
|
||||
// 2. Trailing silence or similar zero-PCM content
|
||||
//
|
||||
const Uint64 calculate_stream_hash(struct SDL_RWops* const context) {
|
||||
|
||||
// Save the current stream position, so we can restore it at the end of the function.
|
||||
const Sint64 original_pos = SDL_RWtell(context);
|
||||
|
||||
// Seek to the end of the file so we can calculate the stream size.
|
||||
SDL_RWseek(context, 0, RW_SEEK_END);
|
||||
|
||||
const Sint32 stream_size = (Sint32) SDL_RWtell(context);
|
||||
if (stream_size <= 0) {
|
||||
// LOG_MSG("MP3: get_stream_size returned %d, but should be positive", stream_size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Seek to the middle of the file while taking into account version small files.
|
||||
const Uint32 tail_size = (stream_size > 32768) ? 32768 : stream_size;
|
||||
const Sint64 mid_pos = static_cast<Sint64>(stream_size/2.0) - tail_size;
|
||||
SDL_RWseek(context, mid_pos >= 0 ? static_cast<int>(mid_pos) : 0, RW_SEEK_SET);
|
||||
|
||||
// Prepare our read buffer and counter:
|
||||
vector<char> buffer(1024, 0);
|
||||
Uint32 total_bytes_read = 0;
|
||||
|
||||
// Initialize xxHash's state using the stream_size as our seed.
|
||||
// Seeding with the stream_size provide a second level of uniqueness
|
||||
// in the unlikely scenario that two files of different length happen to
|
||||
// have the same trailing 32KB of content. The different seeds will produce
|
||||
// unique hashes.
|
||||
XXH64_state_t* const state = XXH64_createState();
|
||||
const Uint64 seed = stream_size;
|
||||
XXH64_reset(state, seed);
|
||||
|
||||
while (total_bytes_read < tail_size) {
|
||||
// Read a chunk of data.
|
||||
const size_t bytes_read = SDL_RWread(context, buffer.data(), 1, buffer.size());
|
||||
|
||||
if (bytes_read != 0) {
|
||||
// Update our hash if we read data.
|
||||
XXH64_update(state, buffer.data(), bytes_read);
|
||||
total_bytes_read += bytes_read;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// restore the stream position
|
||||
SDL_RWseek(context, static_cast<int>(original_pos), RW_SEEK_SET);
|
||||
|
||||
const Uint64 hash = XXH64_digest(state);
|
||||
XXH64_freeState(state);
|
||||
return hash;
|
||||
}
|
||||
|
||||
// This function generates a new seek-table for a given mp3 stream and writes
|
||||
// the data to the fast-seek file.
|
||||
//
|
||||
const Uint64 generate_new_seek_points(const char* filename,
|
||||
const Uint64& stream_hash,
|
||||
drmp3* const p_dr,
|
||||
map<Uint64, vector<drmp3_seek_point_serial> >& seek_points_table,
|
||||
map<Uint64, drmp3_uint64>& pcm_frame_count_table,
|
||||
vector<drmp3_seek_point_serial>& seek_points_vector) {
|
||||
|
||||
// Initialize our frame counters with zeros.
|
||||
drmp3_uint64 mp3_frame_count(0);
|
||||
drmp3_uint64 pcm_frame_count(0);
|
||||
|
||||
// Get the number of compressed MP3 frames and the number of uncompressed PCM frames.
|
||||
drmp3_bool8 result = drmp3_get_mp3_and_pcm_frame_count(p_dr,
|
||||
&mp3_frame_count,
|
||||
&pcm_frame_count);
|
||||
|
||||
if ( result != DRMP3_TRUE
|
||||
|| mp3_frame_count < FRAMES_PER_SEEK_POINT
|
||||
|| pcm_frame_count < FRAMES_PER_SEEK_POINT) {
|
||||
// LOG_MSG("MP3: failed to determine or find sufficient mp3 and pcm frames");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Based on the number of frames found in the file, we size our seek-point
|
||||
// vector accordingly. We then pass our sized vector into dr_mp3 which populates
|
||||
// the decoded PCM times.
|
||||
// We also take into account the desired number of "FRAMES_PER_SEEK_POINT",
|
||||
// which is defined above.
|
||||
drmp3_uint32 num_seek_points = static_cast<drmp3_uint32>(mp3_frame_count)/FRAMES_PER_SEEK_POINT + 1;
|
||||
seek_points_vector.resize(num_seek_points);
|
||||
result = drmp3_calculate_seek_points(p_dr,
|
||||
&num_seek_points,
|
||||
reinterpret_cast<drmp3_seek_point*>(seek_points_vector.data()));
|
||||
|
||||
if (result != DRMP3_TRUE || num_seek_points == 0) {
|
||||
// LOG_MSG("MP3: failed to calculate sufficient seek points for stream");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The calculate function provides us with the actual number of generated seek
|
||||
// points in the num_seek_points variable; so if this differs from expected then we
|
||||
// need to resize (ie: shrink) our vector.
|
||||
if (num_seek_points != seek_points_vector.size()) {
|
||||
seek_points_vector.resize(num_seek_points);
|
||||
}
|
||||
|
||||
// Update our lookup table file with the new seek points and pcm_frame_count.
|
||||
// Note: the serializer elegantly handles C++ STL objects and is endian-safe.
|
||||
seek_points_table[stream_hash] = seek_points_vector;
|
||||
pcm_frame_count_table[stream_hash] = pcm_frame_count;
|
||||
ofstream outfile(filename, ios_base::trunc | ios_base::binary);
|
||||
|
||||
// Caching our seek table to file is optional. If the user is blocked due to
|
||||
// security or write-access issues, then this write-phase is skipped. In this
|
||||
// scenario the seek table will be generated on-the-fly on every start of DOSBox.
|
||||
if (outfile.is_open()) {
|
||||
Archive<ofstream> serialize(outfile);
|
||||
serialize << SEEK_TABLE_IDENTIFIER << seek_points_table << pcm_frame_count_table;
|
||||
outfile.close();
|
||||
}
|
||||
|
||||
// Finally, we return the number of decoded PCM frames for this given file, which
|
||||
// doubles as a success-code.
|
||||
return pcm_frame_count;
|
||||
}
|
||||
|
||||
// This function attempts to fetch a seek-table for a given mp3 stream from the fast-seek file.
|
||||
// If anything is amiss then this function fails.
|
||||
//
|
||||
const Uint64 load_existing_seek_points(const char* filename,
|
||||
const Uint64& stream_hash,
|
||||
map<Uint64, vector<drmp3_seek_point_serial> >& seek_points_table,
|
||||
map<Uint64, drmp3_uint64>& pcm_frame_count_table,
|
||||
vector<drmp3_seek_point_serial>& seek_points) {
|
||||
|
||||
// The below sentinals sanity check and read the incoming
|
||||
// file one-by-one until all the data can be trusted.
|
||||
|
||||
// Sentinal 1: bail if we got a zero-byte file.
|
||||
struct stat buffer;
|
||||
if (stat(filename, &buffer) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sentinal 2: Bail if the file isn't even big enough to hold our 4-byte header string.
|
||||
const string expected_identifier(SEEK_TABLE_IDENTIFIER);
|
||||
if (get_file_size(filename) < 4 + expected_identifier.length()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sentinal 3: Bail if we don't get a match on our ID string.
|
||||
string fetched_identifier;
|
||||
ifstream infile(filename, ios_base::binary);
|
||||
Archive<ifstream> deserialize(infile);
|
||||
deserialize >> fetched_identifier;
|
||||
if (fetched_identifier != expected_identifier) {
|
||||
infile.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// De-serialize the seek point and pcm_count tables.
|
||||
deserialize >> seek_points_table >> pcm_frame_count_table;
|
||||
infile.close();
|
||||
|
||||
// Sentinal 4: does the seek_points table have our stream's hash?
|
||||
const auto p_seek_points = seek_points_table.find(stream_hash);
|
||||
if (p_seek_points == seek_points_table.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sentinal 5: does the pcm_frame_count table have our stream's hash?
|
||||
const auto p_pcm_frame_count = pcm_frame_count_table.find(stream_hash);
|
||||
if (p_pcm_frame_count == pcm_frame_count_table.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If we made it here, the file was valid and has lookup-data for our
|
||||
// our desired stream
|
||||
seek_points = p_seek_points->second;
|
||||
return p_pcm_frame_count->second;
|
||||
}
|
||||
|
||||
// This function attempts to populate our seek table for the given mp3 stream, first
|
||||
// attempting to read it from the fast-seek file and (if it can't be read for any reason), it
|
||||
// calculates new data. It makes use of the above two functions.
|
||||
//
|
||||
const Uint64 populate_seek_points(struct SDL_RWops* const context, mp3_t* p_mp3, const char* seektable_filename) {
|
||||
|
||||
// Calculate the stream's xxHash value.
|
||||
Uint64 stream_hash = calculate_stream_hash(context);
|
||||
if (stream_hash == 0) {
|
||||
// LOG_MSG("MP3: could not compute the hash of the stream");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Attempt to fetch the seek points and pcm count from an existing look up table file.
|
||||
map<Uint64, vector<drmp3_seek_point_serial> > seek_points_table;
|
||||
map<Uint64, drmp3_uint64> pcm_frame_count_table;
|
||||
drmp3_uint64 pcm_frame_count = load_existing_seek_points(seektable_filename,
|
||||
stream_hash,
|
||||
seek_points_table,
|
||||
pcm_frame_count_table,
|
||||
p_mp3->seek_points_vector);
|
||||
|
||||
// Otherwise calculate new seek points and save them to the fast-seek file.
|
||||
if (pcm_frame_count == 0) {
|
||||
pcm_frame_count = generate_new_seek_points(seektable_filename,
|
||||
stream_hash,
|
||||
p_mp3->p_dr,
|
||||
seek_points_table,
|
||||
pcm_frame_count_table,
|
||||
p_mp3->seek_points_vector);
|
||||
if (pcm_frame_count == 0) {
|
||||
// LOG_MSG("MP3: could not load existing or generate new seek points for the stream");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, regardless of which scenario succeeded above, we now have our seek points!
|
||||
// We bind our seek points to the dr_mp3 object which will be used for fast seeking.
|
||||
drmp3_bool8 result = drmp3_bind_seek_table(p_mp3->p_dr,
|
||||
p_mp3->seek_points_vector.size(),
|
||||
reinterpret_cast<drmp3_seek_point*>(p_mp3->seek_points_vector.data()));
|
||||
if (result != DRMP3_TRUE) {
|
||||
// LOG_MSG("MP3: could not bind the seek points to the dr_mp3 object");
|
||||
return 0;
|
||||
}
|
||||
return pcm_frame_count;
|
||||
}
|
57
src/libs/decoders/mp3_seek_table.h
Normal file
57
src/libs/decoders/mp3_seek_table.h
Normal file
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* DOSBox MP3 Seek Table handler, Copyright 2018-2019 Kevin R. Croft (krcroft@gmail.com)
|
||||
* See mp3_seek_table.cpp for more documentation.
|
||||
*
|
||||
* The seek table handler makes use of the following single-header public libraries:
|
||||
* - dr_mp3: http://mackron.github.io/dr_mp3.html, by David Reid
|
||||
* - archive: https://github.com/voidah/archive, by Arthur Ouellet
|
||||
* - xxHash: http://cyan4973.github.io/xxHash, by Yann Collet
|
||||
*
|
||||
* This seek table handler is free software: you can redistribute
|
||||
* it and/or modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with DOSBox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <vector> // provides: vector
|
||||
#include <SDL.h> // provides: SDL_RWops
|
||||
#include "archive.h" // provides: archive
|
||||
|
||||
// Ensure we only get the API
|
||||
#ifdef DR_MP3_IMPLEMENTATION
|
||||
# undef DR_MP3_IMPLEMENTATION
|
||||
#endif
|
||||
#include "dr_mp3.h" // provides: drmp3
|
||||
|
||||
// Note: this C++ struct must match (in binary-form) the "drmp3_seek_point" struct
|
||||
// defined in dr_mp3.h. If that changes, then update this to match, along
|
||||
// with adjusting the Serialize() template function that union's the values.
|
||||
//
|
||||
struct drmp3_seek_point_serial {
|
||||
drmp3_uint64 seekPosInBytes; // Points to the first byte of an MP3 frame.
|
||||
drmp3_uint64 pcmFrameIndex; // The index of the PCM frame this seek point targets.
|
||||
drmp3_uint16 mp3FramesToDiscard; // The number of whole MP3 frames to be discarded before pcmFramesToDiscard.
|
||||
drmp3_uint16 pcmFramesToDiscard;
|
||||
template <class T> void Serialize(T& archive) {
|
||||
archive & seekPosInBytes & pcmFrameIndex & mp3FramesToDiscard & pcmFramesToDiscard;
|
||||
}
|
||||
};
|
||||
|
||||
// Our private-decoder structure where we hold:
|
||||
// - a pointer to the working dr_mp3 instance
|
||||
// - a template vector of seek_points (the serializeable form)
|
||||
struct mp3_t {
|
||||
drmp3* p_dr; // the actual drmp3 instance we open, read, and seek within
|
||||
std::vector<drmp3_seek_point_serial> seek_points_vector;
|
||||
};
|
||||
|
||||
const Uint64 populate_seek_points(struct SDL_RWops* const context, mp3_t* p_mp3, const char* seektable_filename);
|
619
src/libs/decoders/opus.c
Normal file
619
src/libs/decoders/opus.c
Normal file
|
@ -0,0 +1,619 @@
|
|||
/*
|
||||
* This DOSBox Ogg Opus decoder backend is written and copyright 2019 Kevin R Croft (krcroft@gmail.com)
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*
|
||||
* This decoders makes use of:
|
||||
* - libopusfile, for .opus file handing and frame decoding
|
||||
* - speexdsp, for resampling to the original input rate, if needed
|
||||
*
|
||||
* Source links
|
||||
* - libogg: https://github.com/xiph/ogg
|
||||
* - libopus: https://github.com/xiph/opus
|
||||
* - opusfile: https://github.com/xiph/opusfile
|
||||
* - speexdsp: https://github.com/xiph/speexdsp
|
||||
* - opus-tools: https://github.com/xiph/opus-tools
|
||||
|
||||
* Documentation references
|
||||
* - Ogg Opus: https://www.opus-codec.org/docs
|
||||
* - OpusFile: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/index.html
|
||||
* - Resampler: https://www.speex.org/docs/manual/speex-manual/node7.html
|
||||
*
|
||||
*/
|
||||
|
||||
#if HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
// Avoid warning about getenv()
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
|
||||
#include <stdlib.h> // getenv
|
||||
#include <math.h> // ceilf
|
||||
|
||||
// On macOS with GCC, pkg-config only include the opus/ subdirectory
|
||||
// itself instead of the parent, so we take this into account:
|
||||
#if defined(MACOSX) && ! defined(__clang__) && defined(__GNUC__)
|
||||
#include <opusfile.h>
|
||||
#else
|
||||
#include <opus/opusfile.h>
|
||||
#endif
|
||||
#include <speex/speex_resampler.h>
|
||||
|
||||
#include "SDL_sound.h"
|
||||
#define __SDL_SOUND_INTERNAL__
|
||||
#include "SDL_sound_internal.h"
|
||||
|
||||
// The minimum buffer samples per channel: 120 ms @ 48 samples/ms, defined by opus
|
||||
#define OPUS_MIN_BUFFER_SAMPLES_PER_CHANNEL 5760
|
||||
|
||||
// Opus's internal sample rates, to which all encoded streams get resampled
|
||||
#define OPUS_SAMPLE_RATE 48000
|
||||
#define OPUS_SAMPLE_RATE_PER_MS 48
|
||||
|
||||
static Sint32 opus_init (void);
|
||||
static void opus_quit (void);
|
||||
static Sint32 opus_open (Sound_Sample* sample, const char* ext);
|
||||
static void opus_close (Sound_Sample* sample);
|
||||
static Uint32 opus_read (Sound_Sample* sample);
|
||||
static Sint32 opus_rewind (Sound_Sample* sample);
|
||||
static Sint32 opus_seek (Sound_Sample* sample, const Uint32 ms);
|
||||
|
||||
static const char* extensions_opus[] = { "OPUS", NULL };
|
||||
|
||||
const Sound_DecoderFunctions __Sound_DecoderFunctions_OPUS =
|
||||
{
|
||||
{
|
||||
extensions_opus,
|
||||
"Ogg Opus audio using libopusfile",
|
||||
"Kevin R Croft <krcroft@gmail.com>",
|
||||
"https://www.opus-codec.org/"
|
||||
},
|
||||
|
||||
opus_init, /* init() method */
|
||||
opus_quit, /* quit() method */
|
||||
opus_open, /* open() method */
|
||||
opus_close, /* close() method */
|
||||
opus_read, /* read() method */
|
||||
opus_rewind, /* rewind() method */
|
||||
opus_seek /* seek() method */
|
||||
};
|
||||
|
||||
|
||||
// Our private-decoder structure where we hold the opusfile, resampler,
|
||||
// circular buffer, and buffer tracking variables.
|
||||
typedef struct
|
||||
{
|
||||
Uint64 of_pcm; // absolute position in consumed Opus samples
|
||||
OggOpusFile* of; // the actual opusfile we open/read/seek within
|
||||
opus_int16* buffer; // pointer to the start of our circular buffer
|
||||
SpeexResamplerState* resampler; // pointer to an instantiated resampler
|
||||
float rate_ratio; // OPUS_RATE (48KHz) divided by desired sample rate
|
||||
Uint16 buffer_size; // maximum number of samples we can hold in our buffer
|
||||
Uint16 decoded; // number of samples decoded in our buffer
|
||||
Uint16 consumed; // number of samples consumed in our buffer
|
||||
Uint16 frame_size; // number of samples decoded in one opus frame
|
||||
SDL_bool eof; // indicates if we've hit end-of-file decoding
|
||||
} opus_t;
|
||||
|
||||
|
||||
static Sint32 opus_init(void)
|
||||
{
|
||||
SNDDBG(("Opus init: done\n"));
|
||||
return 1; /* always succeeds. */
|
||||
} /* opus_init */
|
||||
|
||||
|
||||
static void opus_quit(void){
|
||||
SNDDBG(("Opus quit: done\n"));
|
||||
} // no-op
|
||||
|
||||
|
||||
/*
|
||||
* Read-Write Ops Read Callback Wrapper
|
||||
* ------------------------------------
|
||||
* OPUS: typedef int(*op_read_func)
|
||||
* void* _stream --> The stream to read from
|
||||
* unsigned char* _ptr --> The buffer to store the data in
|
||||
* int _nbytes --> The maximum number of bytes to read.
|
||||
* Returns: The number of bytes successfully read, or a negative value on error.
|
||||
*
|
||||
* SDL: size_t SDL_RWread
|
||||
* struct SDL_RWops* context --> a pointer to an SDL_RWops structure
|
||||
* void* ptr --> a pointer to a buffer to read data into
|
||||
* size_t size --> the size of each object to read, in bytes
|
||||
* size_t maxnum --> the maximum number of objects to be read
|
||||
*/
|
||||
static Sint32 RWops_opus_read(void* stream, unsigned char* ptr, Sint32 nbytes)
|
||||
{
|
||||
const Sint32 bytes_read = SDL_RWread((SDL_RWops*)stream,
|
||||
(void*)ptr,
|
||||
sizeof(unsigned char),
|
||||
(size_t)nbytes);
|
||||
SNDDBG(("Opus ops read: "
|
||||
"{wanted: %d, returned: %ld}\n", nbytes, bytes_read));
|
||||
|
||||
return bytes_read;
|
||||
} /* RWops_opus_read */
|
||||
|
||||
|
||||
/*
|
||||
* Read-Write Ops Seek Callback Wrapper
|
||||
* ------------------------------------
|
||||
*
|
||||
* OPUS: typedef int(* op_seek_func)
|
||||
* void* _stream, --> The stream to seek in
|
||||
* opus_int64 _offset, --> Sets the position indicator for _stream in bytes
|
||||
* int _whence --> If whence is set to SEEK_SET, SEEK_CUR, or SEEK_END,
|
||||
* the offset is relative to the start of the stream,
|
||||
* the current position indicator, or end-of-file,
|
||||
* respectively
|
||||
* Returns: 0 Success, or -1 if seeking is not supported or an error occurred.
|
||||
* define SEEK_SET 0
|
||||
* define SEEK_CUR 1
|
||||
* define SEEK_END 2
|
||||
*
|
||||
* SDL: Sint64 SDL_RWseek
|
||||
* SDL_RWops* context --> a pointer to an SDL_RWops structure
|
||||
* Sint64 offset, --> offset, in bytes
|
||||
* Sint32 whence --> an offset in bytes, relative to whence location; can be negative
|
||||
* Returns the final offset in the data stream after the seek or -1 on error.
|
||||
* RW_SEEK_SET 0
|
||||
* RW_SEEK_CUR 1
|
||||
* RW_SEEK_END 2
|
||||
*/
|
||||
static Sint32 RWops_opus_seek(void* stream, const opus_int64 offset, const Sint32 whence)
|
||||
{
|
||||
const Sint64 offset_after_seek = SDL_RWseek((SDL_RWops*)stream, (int)offset, whence);
|
||||
|
||||
SNDDBG(("Opus ops seek: "
|
||||
"{requested offset: %ld, seeked offset: %ld}\n",
|
||||
offset, offset_after_seek));
|
||||
|
||||
return (offset_after_seek != -1 ? 0 : -1);
|
||||
} /* RWops_opus_seek */
|
||||
|
||||
|
||||
/*
|
||||
* Read-Write Ops Close Callback Wrapper
|
||||
* -------------------------------------
|
||||
* OPUS: typedef int(* op_close_func)(void *_stream)
|
||||
* SDL: Sint32 SDL_RWclose(struct SDL_RWops* context)
|
||||
*/
|
||||
static Sint32 RWops_opus_close(void* stream)
|
||||
{
|
||||
/* SDL closes this for us */
|
||||
// return SDL_RWclose((SDL_RWops*)stream);
|
||||
return 0;
|
||||
} /* RWops_opus_close */
|
||||
|
||||
|
||||
/*
|
||||
* Read-Write Ops Tell Callback Wrapper
|
||||
* ------------------------------------
|
||||
* OPUS: typedef opus_int64(* op_tell_func)(void *_stream)
|
||||
* SDL: Sint64 SDL_RWtell(struct SDL_RWops* context)
|
||||
*/
|
||||
static opus_int64 RWops_opus_tell(void* stream)
|
||||
{
|
||||
const Sint64 current_offset = SDL_RWtell((SDL_RWops*)stream);
|
||||
|
||||
SNDDBG(("Opus ops tell: "
|
||||
"%ld\n", current_offset));
|
||||
|
||||
return current_offset;
|
||||
} /* RWops_opus_tell */
|
||||
|
||||
|
||||
// Populate the opus callback object (in perscribed order), with our callback functions.
|
||||
static const OpusFileCallbacks RWops_opus_callbacks =
|
||||
{
|
||||
.read = RWops_opus_read,
|
||||
.seek = RWops_opus_seek,
|
||||
.tell = RWops_opus_tell,
|
||||
.close = RWops_opus_close
|
||||
};
|
||||
|
||||
static __inline__ void output_opus_info(const OggOpusFile* of, const OpusHead* oh)
|
||||
{
|
||||
#if (defined DEBUG_CHATTER)
|
||||
const OpusTags* ot = op_tags(of, -1);
|
||||
|
||||
// Guard
|
||||
if ( of == NULL
|
||||
|| oh == NULL
|
||||
|| ot == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump info
|
||||
SNDDBG(("Opus serial number: %u\n", op_serialno(of, -1)));
|
||||
SNDDBG(("Opus format version: %d\n", oh->version));
|
||||
SNDDBG(("Opus channel count: %d\n", oh->channel_count ));
|
||||
SNDDBG(("Opus seekable: %s\n", op_seekable(of) ? "True" : "False"));
|
||||
SNDDBG(("Opus pre-skip samples: %u\n", oh->pre_skip));
|
||||
SNDDBG(("Opus input sample rate: %u\n", oh->input_sample_rate));
|
||||
SNDDBG(("Opus logical streams: %d\n", oh->stream_count));
|
||||
SNDDBG(("Opus vendor: %s\n", ot->vendor));
|
||||
for (int i = 0; i < ot->comments; i++) {
|
||||
SNDDBG(("Opus: user comment: '%s'\n", ot->user_comments[i]));
|
||||
}
|
||||
|
||||
#endif
|
||||
} /* output_opus_comments */
|
||||
|
||||
/*
|
||||
* Opus Open
|
||||
* ---------
|
||||
* - Creates a new opus file object by using our our callback structure for all IO operations.
|
||||
* - We also intialize and allocate memory for fields in the opus_t decode structure.
|
||||
* - SDL expects a returns of 1 on success
|
||||
*/
|
||||
static Sint32 opus_open(Sound_Sample* sample, const char* ext)
|
||||
{
|
||||
Sint32 rcode;
|
||||
Sound_SampleInternal* internal = (Sound_SampleInternal*)sample->opaque;
|
||||
|
||||
// Open the Opus File and print some info
|
||||
OggOpusFile* of = op_open_callbacks(internal->rw, &RWops_opus_callbacks, NULL, 0, &rcode);
|
||||
if (rcode != 0) {
|
||||
op_free(of);
|
||||
of = NULL;
|
||||
SNDDBG(("Opus open error: "
|
||||
"'Could not open opus file: %s'\n", opus_strerror(rcode)));
|
||||
BAIL_MACRO("Opus open fatal: 'Not a valid Ogg Opus file'", 0);
|
||||
}
|
||||
const OpusHead* oh = op_head(of, -1);
|
||||
output_opus_info(of, oh);
|
||||
|
||||
// Initialize our decoder struct elements
|
||||
opus_t* decoder = SDL_malloc(sizeof(opus_t));
|
||||
decoder->of = of;
|
||||
decoder->of_pcm = 0;
|
||||
decoder->decoded = 0;
|
||||
decoder->consumed = 0;
|
||||
decoder->frame_size = 0;
|
||||
decoder->eof = SDL_FALSE;
|
||||
decoder->buffer = NULL;
|
||||
|
||||
// Connect our long-lived internal decoder to the one we're building here
|
||||
internal->decoder_private = decoder;
|
||||
|
||||
if ( sample->desired.rate != 0
|
||||
&& sample->desired.rate != OPUS_SAMPLE_RATE
|
||||
&& getenv("SDL_DONT_RESAMPLE") == NULL) {
|
||||
|
||||
// Opus resamples all inputs to 48kHz. By default (if env-var SDL_DONT_RESAMPLE doesn't exist)
|
||||
// we resample to the desired rate so the recieving SDL_sound application doesn't have to.
|
||||
// This avoids breaking applications that don't expect 48kHz audio and also gives us
|
||||
// quality-control by using the speex resampler, which has a noise floor of -140 dB, which
|
||||
// is ~40dB lower than the -96dB offered by 16-bit CD-quality audio.
|
||||
//
|
||||
sample->actual.rate = sample->desired.rate;
|
||||
decoder->rate_ratio = OPUS_SAMPLE_RATE / (float)(sample->desired.rate);
|
||||
decoder->resampler = speex_resampler_init(oh->channel_count,
|
||||
OPUS_SAMPLE_RATE,
|
||||
sample->desired.rate,
|
||||
// SPEEX_RESAMPLER_QUALITY_VOIP, // consumes ~20 Mhz
|
||||
SPEEX_RESAMPLER_QUALITY_DEFAULT, // consumes ~40 Mhz
|
||||
// SPEEX_RESAMPLER_QUALITY_DESKTOP, // consumes ~80 Mhz
|
||||
&rcode);
|
||||
|
||||
// If we failed to initialize the resampler, then tear down
|
||||
if (rcode < 0) {
|
||||
opus_close(sample);
|
||||
BAIL_MACRO("Opus: failed initializing the resampler", 0);
|
||||
}
|
||||
|
||||
// Otherwise use native sampling
|
||||
} else {
|
||||
sample->actual.rate = OPUS_SAMPLE_RATE;
|
||||
decoder->rate_ratio = 1.0;
|
||||
decoder->resampler = NULL;
|
||||
}
|
||||
|
||||
// Allocate our buffer to hold PCM samples from the Opus decoder
|
||||
decoder->buffer_size = (Uint16) (oh->channel_count * OPUS_MIN_BUFFER_SAMPLES_PER_CHANNEL * 1.5);
|
||||
decoder->buffer = SDL_malloc(decoder->buffer_size * sizeof(opus_int16));
|
||||
|
||||
// Gather static properties about our stream (channels, seek-ability, format, and duration)
|
||||
sample->actual.channels = (Uint8)(oh->channel_count);
|
||||
sample->flags = op_seekable(of) ? SOUND_SAMPLEFLAG_CANSEEK: 0;
|
||||
sample->actual.format = AUDIO_S16LSB; // returns least-significant-byte order regardless of architecture
|
||||
|
||||
ogg_int64_t total_time = op_pcm_total(of, -1); // total PCM samples in the stream
|
||||
internal->total_time = total_time == OP_EINVAL ? -1 : // total milliseconds in the stream
|
||||
(Sint32)( (double)total_time / OPUS_SAMPLE_RATE_PER_MS);
|
||||
|
||||
return 1;
|
||||
} /* opus_open */
|
||||
|
||||
|
||||
/*
|
||||
* Opus Close
|
||||
* ----------
|
||||
* Free and NULL all allocated memory pointers.
|
||||
*/
|
||||
static void opus_close(Sound_Sample* sample)
|
||||
{
|
||||
/* From the Opus docs: if opening a stream/file/or using op_test_callbacks() fails
|
||||
* then we are still responsible for freeing the OggOpusFile with op_free().
|
||||
*/
|
||||
Sound_SampleInternal* internal = (Sound_SampleInternal*) sample->opaque;
|
||||
|
||||
opus_t* d = internal->decoder_private;
|
||||
if (d != NULL) {
|
||||
if (d->of != NULL) {
|
||||
op_free(d->of);
|
||||
d->of = NULL;
|
||||
}
|
||||
|
||||
if (d->resampler != NULL) {
|
||||
speex_resampler_destroy(d->resampler);
|
||||
d->resampler = NULL;
|
||||
}
|
||||
|
||||
if (d->buffer != NULL) {
|
||||
SDL_free(d->buffer);
|
||||
d->buffer = NULL;
|
||||
}
|
||||
|
||||
SDL_free(d);
|
||||
d = NULL;
|
||||
}
|
||||
return;
|
||||
|
||||
} /* opus_close */
|
||||
|
||||
|
||||
/*
|
||||
* Opus Read
|
||||
* ---------
|
||||
* Decode, resample (if needed), and write the output to the
|
||||
* requested buffer.
|
||||
*/
|
||||
static Uint32 opus_read(Sound_Sample* sample)
|
||||
{
|
||||
Sound_SampleInternal* internal = (Sound_SampleInternal*) sample->opaque;
|
||||
opus_t* d = internal->decoder_private;
|
||||
|
||||
opus_int16* output_buffer = internal->buffer;
|
||||
const Uint16 requested_output_size = internal->buffer_size / sizeof(opus_int16);
|
||||
const Uint16 derived_consumption_size = (Uint16) ceilf(requested_output_size * d->rate_ratio);
|
||||
|
||||
// Three scenarios in order of probabilty:
|
||||
//
|
||||
// 1. consume: resample (if needed) a chunk from our decoded queue
|
||||
// sufficient to fill the requested buffer.
|
||||
//
|
||||
// If the decoder has hit the end-of-file, drain any
|
||||
// remaining decoded data before setting the EOF flag.
|
||||
//
|
||||
// 2. decode: decode chunks unil our buffer is full or we hit EOF.
|
||||
//
|
||||
// 3. wrap: we've decoded and consumed to edge of our buffer
|
||||
// so wrap any remaining decoded samples back around.
|
||||
|
||||
Sint32 rcode = 1;
|
||||
SDL_bool have_consumed = SDL_FALSE;
|
||||
while (! have_consumed){
|
||||
|
||||
// consume ...
|
||||
const Uint16 unconsumed_size = d->decoded - d->consumed;
|
||||
if (unconsumed_size >= derived_consumption_size || d->eof) {
|
||||
|
||||
// If we're at the start of the stream, ignore 'pre-skip' samples
|
||||
// per-channel. Pre-skip describes how much data must be decoded
|
||||
// before valid output is obtained.
|
||||
//
|
||||
const OpusHead* oh = op_head(d->of, -1);
|
||||
if (d->of_pcm == 0) {
|
||||
d->consumed += oh->pre_skip * oh->channel_count;
|
||||
}
|
||||
|
||||
// We use these to record the actual consumed and output sizes
|
||||
Uint32 actual_consumed_size = unconsumed_size;
|
||||
Uint32 actual_output_size = requested_output_size;
|
||||
|
||||
// If we need to resample
|
||||
if (d->resampler) {
|
||||
(void) speex_resampler_process_int(d->resampler, 0,
|
||||
d->buffer + d->consumed,
|
||||
&actual_consumed_size,
|
||||
output_buffer,
|
||||
&actual_output_size);
|
||||
}
|
||||
// Otherwise copy the bytes
|
||||
else {
|
||||
if (unconsumed_size < requested_output_size) {
|
||||
actual_output_size = unconsumed_size;
|
||||
}
|
||||
actual_consumed_size = actual_output_size;
|
||||
SDL_memcpy(output_buffer, d->buffer + d->consumed, actual_output_size * sizeof(opus_int16));
|
||||
}
|
||||
|
||||
// bump our comsumption count and absolute pcm position
|
||||
d->consumed += actual_consumed_size;
|
||||
d->of_pcm += actual_consumed_size;
|
||||
|
||||
SNDDBG(("Opus read consuming: "
|
||||
"{output: %u, so_far: %u, remaining_buffer: %u}\n",
|
||||
actual_output_size, d->consumed, d->decoded - d->consumed));
|
||||
|
||||
// if we wrote less than requested then we're at the end-of-file
|
||||
if (actual_output_size < requested_output_size) {
|
||||
sample->flags |= SOUND_SAMPLEFLAG_EOF;
|
||||
SNDDBG(("Opus read consuming: "
|
||||
"{end_of_buffer: True, requested: %u, resampled_output: %u}\n",
|
||||
requested_output_size, actual_output_size));
|
||||
}
|
||||
|
||||
rcode = actual_output_size * sizeof(opus_int16); // covert from samples to bytes
|
||||
have_consumed = SDL_TRUE;
|
||||
}
|
||||
|
||||
else {
|
||||
// wrap ...
|
||||
if (d->frame_size > 0) {
|
||||
SDL_memcpy(d->buffer,
|
||||
d->buffer + d->consumed,
|
||||
(d->decoded - d->consumed)*sizeof(opus_int16));
|
||||
|
||||
d->decoded -= d->consumed;
|
||||
d->consumed = 0;
|
||||
|
||||
SNDDBG(("Opus read wrapping: "
|
||||
"{wrapped: %u}\n", d->decoded));
|
||||
}
|
||||
|
||||
// decode ...
|
||||
while (rcode > 0 && d->buffer_size - d->decoded >= d->frame_size) {
|
||||
|
||||
rcode = sample->actual.channels * op_read(d->of,
|
||||
d->buffer + d->decoded,
|
||||
d->buffer_size - d->decoded, NULL);
|
||||
// Use the largest decoded frame to know when
|
||||
// our buffer is too small to hold a frame, to
|
||||
// avoid constraining the decoder to fill sizes
|
||||
// smaller than the stream's frame-size
|
||||
if (rcode > d->frame_size) {
|
||||
|
||||
SNDDBG(("Opus read decoding: "
|
||||
"{frame_previous: %u, frame_new: %u}\n",
|
||||
d->frame_size, rcode));
|
||||
|
||||
d->frame_size = rcode;
|
||||
}
|
||||
|
||||
// assess the validity of the return code
|
||||
if (rcode > 0) { d->decoded += rcode;} // reading
|
||||
else if (rcode == 0) { d->eof = SDL_TRUE;} // done
|
||||
else if (rcode == OP_HOLE) { rcode = 1;} // hole in the data, carry on
|
||||
else { // (rcode < 0) // error
|
||||
sample->flags |= SOUND_SAMPLEFLAG_ERROR;
|
||||
}
|
||||
|
||||
SNDDBG(("Opus read decoding: "
|
||||
"{decoded: %u, remaining buffer: %u, end_of_file: %s}\n",
|
||||
rcode, d->buffer_size - d->decoded, d->eof ? "True" : "False"));
|
||||
}
|
||||
}
|
||||
} // end while.
|
||||
return rcode;
|
||||
} /* opus_read */
|
||||
|
||||
|
||||
/*
|
||||
* Opus Rewind
|
||||
* -----------
|
||||
* Sets the current position of the stream to 0.
|
||||
*/
|
||||
static Sint32 opus_rewind(Sound_Sample* sample)
|
||||
{
|
||||
const Sint32 rcode = opus_seek(sample, 0);
|
||||
BAIL_IF_MACRO(rcode < 0, ERR_IO_ERROR, 0);
|
||||
return rcode;
|
||||
} /* opus_rewind */
|
||||
|
||||
|
||||
/*
|
||||
* Opus Seek
|
||||
* ---------
|
||||
* Set the current position of the stream to the indicated
|
||||
* integer offset in milliseconds.
|
||||
*/
|
||||
static Sint32 opus_seek(Sound_Sample* sample, const Uint32 ms)
|
||||
{
|
||||
Sound_SampleInternal* internal = (Sound_SampleInternal*) sample->opaque;
|
||||
opus_t* d = internal->decoder_private;
|
||||
int rcode = -1;
|
||||
|
||||
#if (defined DEBUG_CHATTER)
|
||||
const float total_seconds = (float)ms/1000;
|
||||
uint8_t minutes = total_seconds / 60;
|
||||
const float seconds = ((int)total_seconds % 60) + (total_seconds - (int)total_seconds);
|
||||
const uint8_t hours = minutes / 60;
|
||||
minutes = minutes % 60;
|
||||
#endif
|
||||
|
||||
// convert the desired ms offset into OPUS PCM samples
|
||||
const ogg_int64_t desired_pcm = ms * OPUS_SAMPLE_RATE_PER_MS;
|
||||
|
||||
// Is our stream already positioned at the requested offset?
|
||||
if (d->of_pcm == desired_pcm) {
|
||||
|
||||
SNDDBG(("Opus seek avoided: "
|
||||
"{requested_time: '%02d:%02d:%.2f', becomes_opus_pcm: %ld, actual_pcm_pos: %ld}\n",
|
||||
hours, minutes, seconds, desired_pcm, d->of_pcm));
|
||||
|
||||
rcode = 1;
|
||||
}
|
||||
|
||||
// If not, check if we can jump within our circular buffer (and not actually seek!)
|
||||
// In this scenario, we don't have to waste our currently decoded samples
|
||||
// or incur the cost of 80ms of pre-roll decoding behind the scene in libopus.
|
||||
else {
|
||||
Uint64 pcm_start = d->of_pcm - d->consumed;
|
||||
Uint64 pcm_end = pcm_start + d->decoded;
|
||||
|
||||
// In both scenarios below we're going to seek, in which case
|
||||
// our sample flags should be reset and let the read function
|
||||
// re-assess the flag.
|
||||
//
|
||||
|
||||
// Is the requested pcm offset within our decoded range?
|
||||
if ( (Uint64) desired_pcm >= pcm_start && (Uint64) desired_pcm <= pcm_end) {
|
||||
|
||||
SNDDBG(("Opus seek avoided: "
|
||||
"{requested_time: '%02d:%02d:%.2f', becomes_opus_pcm: %ld, buffer_start: %ld, buffer_end: %ld}\n",
|
||||
hours, minutes, seconds, desired_pcm, pcm_start, pcm_end));
|
||||
|
||||
// Yes, so simply adjust our existing pcm offset and consumption position
|
||||
// No seeks or pre-roll needed!
|
||||
d->consumed = (Uint16)(desired_pcm - pcm_start);
|
||||
d->of_pcm = desired_pcm;
|
||||
|
||||
// reset our sample flags and let our consumption state re-apply
|
||||
// the flags per its own rules
|
||||
if (op_seekable(d->of)) {
|
||||
sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
|
||||
}
|
||||
|
||||
// note, we don't reset d->eof because our decode state is unchanged
|
||||
rcode = 1;
|
||||
// rcode is 1, confirming we successfully seeked
|
||||
}
|
||||
|
||||
// No; the requested pcm offset is outside our circular decode buffer,
|
||||
// so actually seek and reset our decode and consumption counters.
|
||||
else {
|
||||
rcode = op_pcm_seek(d->of, desired_pcm) + 1;
|
||||
|
||||
// op_pcm_seek(..) returns 0, to which we add 1, on success
|
||||
// ... or a negative value on error.
|
||||
if (rcode > 0) {
|
||||
d->of_pcm = desired_pcm;
|
||||
d->consumed = 0;
|
||||
d->decoded = 0;
|
||||
d->eof = SDL_FALSE;
|
||||
SNDDBG(("Opus seek in file: "
|
||||
"{requested_time: '%02d:%02d:%.2f', becomes_opus_pcm: %ld}\n",
|
||||
hours, minutes, seconds, desired_pcm));
|
||||
|
||||
// reset our sample flags and let the read function re-apply
|
||||
// sample flags as it hits them from our our offset
|
||||
if (op_seekable(d->of)) {
|
||||
sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
|
||||
}
|
||||
|
||||
}
|
||||
// otherwise we failed to seek.. so leave everything as-is.
|
||||
}
|
||||
}
|
||||
|
||||
BAIL_IF_MACRO(rcode < 0, ERR_IO_ERROR, 0);
|
||||
return rcode;
|
||||
} /* opus_seek */
|
||||
|
||||
/* end of ogg_opus.c ... */
|
231
src/libs/decoders/vorbis.c
Normal file
231
src/libs/decoders/vorbis.c
Normal file
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* This DOSBox Vorbis decoder backend maintained by Kevin R. Croft (krcroft@gmail.com)
|
||||
* This decoder makes use of the excellent STB Vorbis decoder by Sean Barrett
|
||||
*
|
||||
* Source links
|
||||
* - STB: https://github.com/nothings/stb (source)
|
||||
* - STB: https://twitter.com/nothings (website/author info)
|
||||
*
|
||||
* The upstream SDL2 Sound 1.9.x Vorbis decoder is written and copyright by Ryan C. Gordon. (icculus@icculus.org)
|
||||
*
|
||||
* Please see the file src/libs/decoders/docs/LICENSE.txt.
|
||||
*
|
||||
* This file is part of the SDL Sound Library.
|
||||
*
|
||||
* This Vorbis decoder backend is free software: you can redistribute
|
||||
* it and/or modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This SDL_sound Ogg Opus decoder backend is distributed in the hope that it
|
||||
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with the SDL Sound Library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#if HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#ifdef memcpy
|
||||
# undef memcpy
|
||||
#endif
|
||||
|
||||
#include <string.h> /* memcpy */
|
||||
#include <math.h> /* lroundf */
|
||||
|
||||
#include "SDL_sound.h"
|
||||
#define __SDL_SOUND_INTERNAL__
|
||||
#include "SDL_sound_internal.h"
|
||||
|
||||
#ifdef asset
|
||||
# undef assert
|
||||
# define assert SDL_assert
|
||||
#endif
|
||||
|
||||
#ifdef memset
|
||||
# undef memset
|
||||
# define memset SDL_memset
|
||||
#endif
|
||||
|
||||
#define memcmp SDL_memcmp
|
||||
#define qsort SDL_qsort
|
||||
#define malloc SDL_malloc
|
||||
#define realloc SDL_realloc
|
||||
#define free SDL_free
|
||||
#define dealloca(x) SDL_stack_free((x))
|
||||
|
||||
/* Configure and include stb_vorbis for compiling... */
|
||||
#define STB_VORBIS_NO_STDIO 1
|
||||
#define STB_VORBIS_NO_CRT 1
|
||||
#define STB_VORBIS_NO_PUSHDATA_API 1
|
||||
#define STB_VORBIS_MAX_CHANNELS 2
|
||||
// #define STBV_CDECL
|
||||
// #define STB_FORCEINLINE SDL_FORCE_INLINE
|
||||
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
|
||||
#define STB_VORBIS_BIG_ENDIAN 1
|
||||
#endif
|
||||
|
||||
#include "stb_vorbis.h"
|
||||
|
||||
#ifdef DEBUG_CHATTER
|
||||
static const char *vorbis_error_string(const int err)
|
||||
{
|
||||
switch (err)
|
||||
{
|
||||
case VORBIS__no_error: return NULL;
|
||||
case VORBIS_need_more_data: return "VORBIS: need more data";
|
||||
case VORBIS_invalid_api_mixing: return "VORBIS: can't mix API modes";
|
||||
case VORBIS_outofmem: return "VORBIS: out of memory";
|
||||
case VORBIS_feature_not_supported: return "VORBIS: feature not supported";
|
||||
case VORBIS_too_many_channels: return "VORBIS: too many channels";
|
||||
case VORBIS_file_open_failure: return "VORBIS: failed opening the file";
|
||||
case VORBIS_seek_without_length: return "VORBIS: can't seek in unknown length stream";
|
||||
case VORBIS_unexpected_eof: return "VORBIS: unexpected eof";
|
||||
case VORBIS_seek_invalid: return "VORBIS: invalid seek";
|
||||
case VORBIS_invalid_setup: return "VORBIS: invalid setup";
|
||||
case VORBIS_invalid_stream: return "VORBIS: invalid stream";
|
||||
case VORBIS_missing_capture_pattern: return "VORBIS: missing capture pattern";
|
||||
case VORBIS_invalid_stream_structure_version: return "VORBIS: invalid stream structure version";
|
||||
case VORBIS_continued_packet_flag_invalid: return "VORBIS: continued packet flag invalid";
|
||||
case VORBIS_incorrect_stream_serial_number: return "VORBIS: incorrect stream serial number";
|
||||
case VORBIS_invalid_first_page: return "VORBIS: invalid first page";
|
||||
case VORBIS_bad_packet_type: return "VORBIS: bad packet type";
|
||||
case VORBIS_cant_find_last_page: return "VORBIS: can't find last page";
|
||||
case VORBIS_seek_failed: return "VORBIS: seek failed";
|
||||
case VORBIS_ogg_skeleton_not_supported: return "VORBIS: multi-track streams are not supported; "
|
||||
"consider re-encoding without the Ogg Skeleton bitstream";
|
||||
default: break;
|
||||
} /* switch */
|
||||
|
||||
return "VORBIS: unknown error";
|
||||
} /* vorbis_error_string */
|
||||
#endif
|
||||
|
||||
static int VORBIS_init(void)
|
||||
{
|
||||
return 1; /* always succeeds. */
|
||||
} /* VORBIS_init */
|
||||
|
||||
static void VORBIS_quit(void)
|
||||
{
|
||||
/* it's a no-op. */
|
||||
} /* VORBIS_quit */
|
||||
|
||||
static int VORBIS_open(Sound_Sample *sample, const char *ext)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
SDL_RWops *rw = internal->rw;
|
||||
int err = 0;
|
||||
stb_vorbis *stb = stb_vorbis_open_rwops(rw, 0, &err, NULL);
|
||||
|
||||
if (stb == NULL) {
|
||||
SNDDBG(("%s (error code: %d)\n", vorbis_error_string(err), err));
|
||||
return 0;
|
||||
}
|
||||
internal->decoder_private = stb;
|
||||
sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
|
||||
sample->actual.format = AUDIO_S16SYS; // returns byte-order native to the running architecture
|
||||
sample->actual.channels = stb->channels;
|
||||
sample->actual.rate = stb->sample_rate;
|
||||
const unsigned int num_frames = stb_vorbis_stream_length_in_samples(stb);
|
||||
if (!num_frames) {
|
||||
internal->total_time = -1;
|
||||
}
|
||||
else {
|
||||
const unsigned int rate = stb->sample_rate;
|
||||
internal->total_time = (num_frames / rate) * 1000;
|
||||
internal->total_time += (num_frames % rate) * 1000 / rate;
|
||||
} /* else */
|
||||
|
||||
return 1; /* we'll handle this data. */
|
||||
} /* VORBIS_open */
|
||||
|
||||
|
||||
static void VORBIS_close(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
stb_vorbis *stb = (stb_vorbis *) internal->decoder_private;
|
||||
stb_vorbis_close(stb);
|
||||
} /* VORBIS_close */
|
||||
|
||||
|
||||
static Uint32 VORBIS_read(Sound_Sample *sample)
|
||||
{
|
||||
Uint32 retval;
|
||||
int rc;
|
||||
int err;
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
stb_vorbis *stb = (stb_vorbis *) internal->decoder_private;
|
||||
const int channels = (int) sample->actual.channels;
|
||||
const int want_samples = (int) (internal->buffer_size / sizeof (int16_t));
|
||||
|
||||
stb_vorbis_get_error(stb); /* clear any error state */
|
||||
rc = stb_vorbis_get_samples_short_interleaved(stb, channels, (int16_t *) internal->buffer, want_samples);
|
||||
retval = (Uint32) (rc * channels * sizeof (int16_t)); /* rc == number of sample frames read */
|
||||
err = stb_vorbis_get_error(stb);
|
||||
|
||||
if (retval == 0) {
|
||||
sample->flags |= (err ? SOUND_SAMPLEFLAG_ERROR : SOUND_SAMPLEFLAG_EOF);
|
||||
}
|
||||
else if (retval < internal->buffer_size) {
|
||||
sample->flags |= SOUND_SAMPLEFLAG_EAGAIN;
|
||||
}
|
||||
return retval;
|
||||
} /* VORBIS_read */
|
||||
|
||||
|
||||
static int VORBIS_rewind(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
stb_vorbis *stb = (stb_vorbis *) internal->decoder_private;
|
||||
|
||||
if (!stb_vorbis_seek_start(stb)) {
|
||||
SNDDBG(("%s\n", vorbis_error_string(stb_vorbis_get_error(stb))));
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
} /* VORBIS_rewind */
|
||||
|
||||
|
||||
static int VORBIS_seek(Sound_Sample *sample, Uint32 ms)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
stb_vorbis *stb = (stb_vorbis *) internal->decoder_private;
|
||||
const float frames_per_ms = ((float) sample->actual.rate) / 1000.0f;
|
||||
const Uint32 frame_offset = lroundf(frames_per_ms * ms);
|
||||
const unsigned int sampnum = (unsigned int) frame_offset;
|
||||
|
||||
if (!stb_vorbis_seek(stb, sampnum)) {
|
||||
SNDDBG(("%s\n", vorbis_error_string(stb_vorbis_get_error(stb))));
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
} /* VORBIS_seek */
|
||||
|
||||
|
||||
static const char *extensions_vorbis[] = { "OGG", "OGA", "VORBIS", NULL };
|
||||
const Sound_DecoderFunctions __Sound_DecoderFunctions_VORBIS =
|
||||
{
|
||||
{
|
||||
extensions_vorbis,
|
||||
"Ogg Vorbis audio",
|
||||
"Ryan C. Gordon <icculus@icculus.org>",
|
||||
"https://icculus.org/SDL_sound/"
|
||||
},
|
||||
|
||||
VORBIS_init, /* init() method */
|
||||
VORBIS_quit, /* quit() method */
|
||||
VORBIS_open, /* open() method */
|
||||
VORBIS_close, /* close() method */
|
||||
VORBIS_read, /* read() method */
|
||||
VORBIS_rewind, /* rewind() method */
|
||||
VORBIS_seek /* seek() method */
|
||||
};
|
||||
|
||||
/* end of SDL_sound_vorbis.c ... */
|
169
src/libs/decoders/wav.c
Normal file
169
src/libs/decoders/wav.c
Normal file
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* DOSBox WAV decoder is maintained by Kevin R. Croft (krcroft@gmail.com)
|
||||
* This decoder makes use of the excellent dr_wav library by David Reid (mackron@gmail.com)
|
||||
*
|
||||
* Source links
|
||||
* - dr_libs: https://github.com/mackron/dr_libs (source)
|
||||
* - dr_wav: http://mackron.github.io/dr_wav.html (website)
|
||||
*
|
||||
* Please see the file src/libs/decoders/docs/LICENSE.txt.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with the SDL Sound Library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#if HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <math.h> /* llroundf */
|
||||
|
||||
#include "SDL_sound.h"
|
||||
#define __SDL_SOUND_INTERNAL__
|
||||
#include "SDL_sound_internal.h"
|
||||
|
||||
/* Map dr_wav's memory routines to SDL's */
|
||||
#define DRWAV_FREE(p) SDL_free((p))
|
||||
#define DRWAV_MALLOC(sz) SDL_malloc((sz))
|
||||
#define DRWAV_REALLOC(p, sz) SDL_realloc((p), (sz))
|
||||
#define DRWAV_ZERO_MEMORY(p, sz) SDL_memset((p), 0, (sz))
|
||||
#define DRWAV_COPY_MEMORY(dst, src, sz) SDL_memcpy((dst), (src), (sz))
|
||||
|
||||
#define DR_WAV_NO_STDIO
|
||||
#define DR_WAV_IMPLEMENTATION
|
||||
#include "dr_wav.h"
|
||||
|
||||
static size_t wav_read(void* pUserData, void* pBufferOut, size_t bytesToRead)
|
||||
{
|
||||
Uint8 *ptr = (Uint8 *) pBufferOut;
|
||||
Sound_Sample *sample = (Sound_Sample *) pUserData;
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
SDL_RWops *rwops = internal->rw;
|
||||
size_t retval = 0;
|
||||
|
||||
while (retval < bytesToRead) {
|
||||
const size_t rc = SDL_RWread(rwops, ptr, 1, bytesToRead);
|
||||
if (rc == 0) {
|
||||
sample->flags |= SOUND_SAMPLEFLAG_EOF;
|
||||
break;
|
||||
} /* if */
|
||||
retval += rc;
|
||||
ptr += rc;
|
||||
} /* while */
|
||||
|
||||
return retval;
|
||||
} /* wav_read */
|
||||
|
||||
static drwav_bool32 wav_seek(void* pUserData, int offset, drwav_seek_origin origin)
|
||||
{
|
||||
const int whence = (origin == drwav_seek_origin_start) ? RW_SEEK_SET : RW_SEEK_CUR;
|
||||
Sound_Sample *sample = (Sound_Sample *) pUserData;
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
return (SDL_RWseek(internal->rw, offset, whence) != -1) ? DRWAV_TRUE : DRWAV_FALSE;
|
||||
} /* wav_seek */
|
||||
|
||||
|
||||
static int WAV_init(void)
|
||||
{
|
||||
return 1; /* always succeeds. */
|
||||
} /* WAV_init */
|
||||
|
||||
|
||||
static void WAV_quit(void)
|
||||
{
|
||||
/* it's a no-op. */
|
||||
} /* WAV_quit */
|
||||
|
||||
static void WAV_close(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drwav *dr = (drwav *) internal->decoder_private;
|
||||
if (dr != NULL) {
|
||||
(void) drwav_uninit(dr);
|
||||
SDL_free(dr);
|
||||
internal->decoder_private = NULL;
|
||||
}
|
||||
return;
|
||||
} /* WAV_close */
|
||||
|
||||
static int WAV_open(Sound_Sample *sample, const char *ext)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drwav* dr = SDL_malloc(sizeof(drwav));
|
||||
drwav_result result = drwav_init_ex(dr, wav_read, wav_seek, NULL, sample, NULL, 0, NULL);
|
||||
internal->decoder_private = dr;
|
||||
|
||||
if (result == DRWAV_TRUE) {
|
||||
SNDDBG(("WAV: Codec accepted the data stream.\n"));
|
||||
sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
|
||||
sample->actual.rate = dr->sampleRate;
|
||||
sample->actual.format = AUDIO_S16SYS;
|
||||
sample->actual.channels = (Uint8)(dr->channels);
|
||||
|
||||
const Uint64 frames = (Uint64) dr->totalPCMFrameCount;
|
||||
if (frames == 0) {
|
||||
internal->total_time = -1;
|
||||
}
|
||||
else {
|
||||
const Uint32 rate = (Uint32) dr->sampleRate;
|
||||
internal->total_time = ( (Sint32)frames / rate) * 1000;
|
||||
internal->total_time += ((frames % rate) * 1000) / rate;
|
||||
} /* else */
|
||||
|
||||
} /* if result != DRWAV_TRUE */
|
||||
else {
|
||||
SNDDBG(("WAV: Codec could not parse the data stream.\n"));
|
||||
WAV_close(sample);
|
||||
}
|
||||
return result;
|
||||
} /* WAV_open */
|
||||
|
||||
|
||||
static Uint32 WAV_read(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drwav *dr = (drwav *) internal->decoder_private;
|
||||
const drwav_uint64 frames_read = drwav_read_pcm_frames_s16(dr,
|
||||
internal->buffer_size / (dr->channels * sizeof(drwav_int16)),
|
||||
(drwav_int16 *) internal->buffer);
|
||||
return (Uint32)frames_read * dr->channels * sizeof (drwav_int16);
|
||||
} /* WAV_read */
|
||||
|
||||
|
||||
static int WAV_rewind(Sound_Sample *sample)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drwav *dr = (drwav *) internal->decoder_private;
|
||||
return (drwav_seek_to_pcm_frame(dr, 0) == DRWAV_TRUE);
|
||||
} /* WAV_rewind */
|
||||
|
||||
static int WAV_seek(Sound_Sample *sample, Uint32 ms)
|
||||
{
|
||||
Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
|
||||
drwav *dr = (drwav *) internal->decoder_private;
|
||||
const float frames_per_ms = ((float) sample->actual.rate) / 1000.0f;
|
||||
const drwav_uint64 frame_offset = llroundf(frames_per_ms * ms);
|
||||
return (drwav_seek_to_pcm_frame(dr, frame_offset) == DRWAV_TRUE);
|
||||
} /* WAV_seek */
|
||||
|
||||
static const char *extensions_wav[] = { "WAV", "W64", NULL };
|
||||
|
||||
const Sound_DecoderFunctions __Sound_DecoderFunctions_WAV =
|
||||
{
|
||||
{
|
||||
extensions_wav,
|
||||
"WAV Audio Codec",
|
||||
"Kevin R. Croft <krcroft@gmail.com>",
|
||||
"github.com/mackron/dr_libs/blob/master/dr_wav.h"
|
||||
},
|
||||
|
||||
WAV_init, /* init() method */
|
||||
WAV_quit, /* quit() method */
|
||||
WAV_open, /* open() method */
|
||||
WAV_close, /* close() method */
|
||||
WAV_read, /* read() method */
|
||||
WAV_rewind, /* rewind() method */
|
||||
WAV_seek /* seek() method */
|
||||
};
|
||||
/* end of wav.c ... */
|
Loading…
Add table
Reference in a new issue