diff --git a/src/libs/decoders/SDL_sound.c b/src/libs/decoders/SDL_sound.c index e331c38e..49ee3f87 100644 --- a/src/libs/decoders/SDL_sound.c +++ b/src/libs/decoders/SDL_sound.c @@ -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) */ diff --git a/src/libs/decoders/SDL_sound.h b/src/libs/decoders/SDL_sound.h index 0bb6f1e9..8c9cf94e 100644 --- a/src/libs/decoders/SDL_sound.h +++ b/src/libs/decoders/SDL_sound.h @@ -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(static_cast(a) | static_cast(b));} - -inline Sound_SampleFlags& operator|= (Sound_SampleFlags& a, Sound_SampleFlags b) -{ return (Sound_SampleFlags&)((int&)a |= static_cast(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 ... */ diff --git a/src/libs/decoders/SDL_sound_internal.h b/src/libs/decoders/SDL_sound_internal.h index 7d3eaad3..d3d981d7 100644 --- a/src/libs/decoders/SDL_sound_internal.h +++ b/src/libs/decoders/SDL_sound_internal.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) */ diff --git a/src/libs/decoders/flac.c b/src/libs/decoders/flac.c new file mode 100644 index 00000000..eba6f47f --- /dev/null +++ b/src/libs/decoders/flac.c @@ -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 . + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +#include /* 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 ", + "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 ... */ diff --git a/src/libs/decoders/mp3.cpp b/src/libs/decoders/mp3.cpp new file mode 100644 index 00000000..28ea00e3 --- /dev/null +++ b/src/libs/decoders/mp3.cpp @@ -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 . + * + */ +#if HAVE_CONFIG_H +# include +#endif + +#include + +#include // 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(pBufferOut); + Sound_Sample* const sample = static_cast(pUserData); + const Sound_SampleInternal* const internal = static_cast(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(pUserData); + Sound_SampleInternal* const internal = static_cast(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(sample->opaque); + mp3_t* p_mp3 = static_cast(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(sample->opaque); + const Sint32 channels = (Sint32) sample->actual.channels; + mp3_t* p_mp3 = static_cast(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(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_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(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(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(result); +} /* MP3_open */ + +static Sint32 MP3_rewind(Sound_Sample* const sample) +{ + Sound_SampleInternal* const internal = static_cast(sample->opaque); + mp3_t* p_mp3 = static_cast(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(sample->opaque); + mp3_t* p_mp3 = static_cast(internal->decoder_private); + const float frames_per_ms = sample->actual.rate / 1000.0f; + const drmp3_uint64 frame_offset = static_cast(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 ", + "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 ... */ diff --git a/src/libs/decoders/mp3_seek_table.cpp b/src/libs/decoders/mp3_seek_table.cpp new file mode 100644 index 00000000..84fa7191 --- /dev/null +++ b/src/libs/decoders/mp3_seek_table.cpp @@ -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 . + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +// System headers +#include +#include +#include +#include + +// 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(stream_size/2.0) - tail_size; + SDL_RWseek(context, mid_pos >= 0 ? static_cast(mid_pos) : 0, RW_SEEK_SET); + + // Prepare our read buffer and counter: + vector 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(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 >& seek_points_table, + map& pcm_frame_count_table, + vector& 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(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(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 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 >& seek_points_table, + map& pcm_frame_count_table, + vector& 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 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 > seek_points_table; + map 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(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; +} diff --git a/src/libs/decoders/mp3_seek_table.h b/src/libs/decoders/mp3_seek_table.h new file mode 100644 index 00000000..52f5e3a4 --- /dev/null +++ b/src/libs/decoders/mp3_seek_table.h @@ -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 . + * + */ + +#include // provides: vector +#include // 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 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 seek_points_vector; +}; + +const Uint64 populate_seek_points(struct SDL_RWops* const context, mp3_t* p_mp3, const char* seektable_filename); diff --git a/src/libs/decoders/opus.c b/src/libs/decoders/opus.c new file mode 100644 index 00000000..7aed5379 --- /dev/null +++ b/src/libs/decoders/opus.c @@ -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 +#endif + +#ifdef _MSC_VER +// Avoid warning about getenv() +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include // getenv +#include // 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 +#else +#include +#endif +#include + +#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 ", + "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 ... */ diff --git a/src/libs/decoders/stb_vorbis.c b/src/libs/decoders/stb_vorbis.h similarity index 100% rename from src/libs/decoders/stb_vorbis.c rename to src/libs/decoders/stb_vorbis.h diff --git a/src/libs/decoders/vorbis.c b/src/libs/decoders/vorbis.c new file mode 100644 index 00000000..228a6471 --- /dev/null +++ b/src/libs/decoders/vorbis.c @@ -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 . + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +#ifdef memcpy +# undef memcpy +#endif + +#include /* memcpy */ +#include /* 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 ", + "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 ... */ diff --git a/src/libs/decoders/wav.c b/src/libs/decoders/wav.c new file mode 100644 index 00000000..80ed0f28 --- /dev/null +++ b/src/libs/decoders/wav.c @@ -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 . + * + */ + +#if HAVE_CONFIG_H +# include +#endif + +#include /* 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 ", + "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 ... */