1
0
Fork 0

Add SDL decoders for their corresponding codecs

This commit is contained in:
krcroft 2019-10-29 09:02:28 -07:00 committed by Patryk Obara
parent 85039a6033
commit f39b51fa78
11 changed files with 1826 additions and 18 deletions

View file

@ -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)
*/

View file

@ -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 ... */

View file

@ -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
View 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
View 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 ... */

View 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;
}

View 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
View 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
View 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
View 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 ... */