DOSBox-X
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Defines
src/libs/decoders/opus.cpp
00001 /*
00002  *  DOSBox-X Opus decoder API implementation
00003  *  ----------------------------------------
00004  *  This decoders makes use of:
00005  *    - libopusfile, for .opus file handing and frame decoding
00006  *
00007  *  Source links
00008  *    - opusfile:   https://github.com/xiph/opusfile
00009  *    - opus-tools: https://github.com/xiph/opus-tools
00010  *
00011  * Documentation references
00012  *    - Ogg Opus:  https://www.opus-codec.org/docs
00013  *    - OpusFile:  https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/index.html
00014  *
00015  *  Copyright (C) 2020       The DOSBox Team
00016  *  Copyright (C) 2018-2019  Kevin R. Croft <krcroft@gmail.com>
00017  *
00018  *  This program is free software; you can redistribute it and/or modify
00019  *  it under the terms of the GNU General Public License as published by
00020  *  the Free Software Foundation; either version 2 of the License, or
00021  *  (at your option) any later version.
00022  *
00023  *  This program is distributed in the hope that it will be useful,
00024  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00025  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00026  *  GNU General Public License for more details.
00027  *
00028  *  You should have received a copy of the GNU General Public License along
00029  *  with this program; if not, write to the Free Software Foundation, Inc.,
00030  *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
00031  */
00032 
00033 // #define DEBUG_CHATTER 1
00034 
00035 #if HAVE_CONFIG_H
00036 #  include <config.h>
00037 #endif
00038 
00039 #include <cassert>
00040 #include <opusfile.h>
00041 #include <SDL.h>
00042 
00043 #include "support.h"
00044 
00045 #include "SDL_sound.h"
00046 #define __SDL_SOUND_INTERNAL__
00047 #include "SDL_sound_internal.h"
00048 
00049 // Opus's internal sampling rates to which all encoded streams get resampled
00050 #define OPUS_SAMPLE_RATE        48000u
00051 #define OPUS_SAMPLE_RATE_PER_MS    48u
00052 
00053 static int32_t opus_init(void)
00054 {
00055     SNDDBG(("Opus init:              done\n"));
00056     return 1; /* always succeeds. */
00057 } /* opus_init */
00058 
00059 
00060 static void opus_quit(void)
00061 {
00062     SNDDBG(("Opus quit:              done\n"));
00063 } // no-op
00064 
00065 
00066 /*
00067  * Read-Write Ops Read Callback Wrapper
00068  * ------------------------------------
00069  * OPUS: typedef int(*op_read_func)
00070  *       void*       _stream  --> The stream to read from
00071  *       unsigned char* _ptr  --> The buffer to store the data in
00072  *       int         _nbytes  --> The maximum number of bytes to read.
00073  *    Returns: The number of bytes successfully read, or a negative value on error.
00074  *
00075  * SDL: size_t SDL_RWread
00076  *      struct SDL_RWops* context --> a pointer to an SDL_RWops structure
00077  *      void*             ptr     --> a pointer to a buffer to read data into
00078  *      size_t            size    --> the size of each object to read, in bytes
00079  *      size_t            maxnum  --> the maximum number of objects to be read
00080  */
00081 static int RWops_opus_read(void * stream, uint8_t * buffer, int32_t requested_bytes)
00082 {
00083     // Guard against invalid inputs and the no-op scenario
00084     assertm(stream && buffer, "OPUS: Inputs are not initialized");
00085     if (requested_bytes <= 0)
00086         return 0;
00087     
00088     uint8_t *buf_pos = buffer;
00089     int32_t bytes_read = 0;
00090     auto *sample = static_cast<Sound_Sample*>(stream);
00091     while (bytes_read < requested_bytes) {
00092         const size_t rc = SDL_RWread(static_cast<SDL_RWops*>(stream),
00093                                      static_cast<void*>(buf_pos),
00094                                      1,
00095                                      static_cast<size_t>(requested_bytes - bytes_read));
00096 
00097         if (rc == 0) {
00098             sample->flags |= SOUND_SAMPLEFLAG_EOF;
00099             break;
00100         }
00101         buf_pos += rc;
00102         bytes_read += rc;
00103     } /* while */
00104 
00105     return bytes_read;
00106 } /* RWops_opus_read */
00107 
00108 
00109 /*
00110  * Read-Write Ops Seek Callback Wrapper
00111  * ------------------------------------
00112  *
00113  * OPUS: typedef int(* op_seek_func)
00114  *       void*      _stream, --> The stream to seek in
00115  *       opus_int64 _offset, --> Sets the position indicator for _stream in bytes
00116  *       int _whence         --> If whence is set to SEEK_SET, SEEK_CUR, or SEEK_END,
00117  *                               the offset is relative to the start of the stream,
00118  *                               the current position indicator, or end-of-file,
00119  *                               respectively
00120  *    Returns: 0  Success, or -1 if seeking is not supported or an error occurred.
00121  *      define  SEEK_SET    0
00122  *      define  SEEK_CUR    1
00123  *      define  SEEK_END    2
00124  *
00125  * SDL: Sint64 SDL_RWseek
00126  *      SDL_RWops* context --> a pointer to an SDL_RWops structure
00127  *      Sint64     offset, --> offset, in bytes
00128  *      Sint32     whence  --> an offset in bytes, relative to whence location; can be negative
00129  *   Returns the final offset in the data stream after the seek or -1 on error.
00130  *      RW_SEEK_SET   0
00131  *      RW_SEEK_CUR   1
00132  *      RW_SEEK_END   2
00133  */
00134 static int32_t RWops_opus_seek(void * stream, const opus_int64 offset, const int32_t whence)
00135 {
00136     // Guard against invalid inputs
00137     assertm(stream, "OPUS: Input is not initialized");
00138     assertm(whence == SEEK_SET || whence == SEEK_CUR || whence == SEEK_END,
00139             "OPUS: The position from where to seek is invalid");
00140 
00141     const int64_t offset_after_seek = SDL_RWseek(static_cast<SDL_RWops*>(stream),
00142                                                  static_cast<int32_t>(offset),
00143                                                  whence);
00144     SNDDBG(("Opus ops seek:          "
00145             "{requested offset: %ld, seeked offset: %ld}\n",
00146             offset, offset_after_seek));
00147     return (offset_after_seek != -1 ? 0 : -1);
00148 } /* RWops_opus_seek */
00149 
00150 
00151 /*
00152  * Read-Write Ops Close Callback Wrapper
00153  * -------------------------------------
00154  * OPUS: typedef int(* op_close_func)(void *_stream)
00155  * SDL:  Sint32 SDL_RWclose(struct SDL_RWops* context)
00156  */
00157 static int32_t RWops_opus_close(void * stream)
00158 {
00159     (void) stream; // deliberately unused, but present for API compliance
00160     /* SDL closes this for us */
00161     // return SDL_RWclose((SDL_RWops*)stream);
00162     return 0;
00163 } /* RWops_opus_close */
00164 
00165 
00166 /*
00167  * Read-Write Ops Tell Callback Wrapper
00168  * ------------------------------------
00169  * OPUS: typedef opus_int64(* op_tell_func)(void *_stream)
00170  * SDL:  Sint64 SDL_RWtell(struct SDL_RWops* context)
00171  */
00172 static opus_int64 RWops_opus_tell(void * stream)
00173 {
00174     // Guard against invalid input
00175     assertm(stream, "OPUS: Input is not initialized");
00176 
00177     const int64_t current_offset = SDL_RWtell(static_cast<SDL_RWops*>(stream));
00178     SNDDBG(("Opus ops tell:          "
00179             "%ld\n", current_offset));
00180     return current_offset;
00181 } /* RWops_opus_tell */
00182 
00183 
00184 // Populate the opus IO callback object (in prescribed order)
00185 static const OpusFileCallbacks RWops_opus_callbacks =
00186 {
00187     RWops_opus_read, // .read member
00188     RWops_opus_seek, // .seek member
00189     RWops_opus_tell, // .tell member
00190     RWops_opus_close // .close member
00191 };
00192 
00193 static __inline__ void output_opus_info(const OggOpusFile * of, const OpusHead * oh)
00194 {
00195 #if (defined DEBUG_CHATTER)
00196     // Guard against invalid input
00197     assertm(of && oh, "OPUS: Inputs are not initialized");
00198 
00199     const OpusTags* ot = op_tags(of, -1);
00200     if (ot != nullptr) {
00201         SNDDBG(("Opus serial number:     %u\n", op_serialno(of, -1)));
00202         SNDDBG(("Opus format version:    %d\n", oh->version));
00203         SNDDBG(("Opus channel count:     %d\n", oh->channel_count ));
00204         SNDDBG(("Opus seekable:          %s\n", op_seekable(of) ? "True" : "False"));
00205         SNDDBG(("Opus pre-skip samples:  %u\n", oh->pre_skip));
00206         SNDDBG(("Opus input sample rate: %u\n", oh->input_sample_rate));
00207         SNDDBG(("Opus logical streams:   %d\n", oh->stream_count));
00208         SNDDBG(("Opus vendor:            %s\n", ot->vendor));
00209         for (int i = 0; i < ot->comments; i++) {
00210             SNDDBG(("Opus: user comment:     '%s'\n", ot->user_comments[i]));
00211         }
00212     }
00213 #else
00214     (void) of; // unused if DEBUG_CHATTER not defined
00215     (void) oh; // unused if DEBUG_CHATTER not defined
00216 #endif
00217 } /* output_opus_comments */
00218 
00219 /*
00220  * Opus Close
00221  * ----------
00222  * Free and nullptr all heap-allocated codec objects.
00223  */
00224 static void opus_close(Sound_Sample * sample)
00225 {
00226     // Guard against the no-op case
00227     if (!sample
00228        || !sample->opaque
00229        || !static_cast<Sound_SampleInternal*>(sample->opaque)->decoder_private)
00230         return;
00231 
00232     /* From the Opus docs: if opening a stream/file/or using op_test_callbacks() fails
00233      * then we are still responsible for freeing the OggOpusFile with op_free().
00234      */
00235     auto *internal = static_cast<Sound_SampleInternal*>(sample->opaque);
00236     auto *of = static_cast<OggOpusFile*>(internal->decoder_private);
00237     if (of != nullptr) {
00238         op_free(of);
00239         internal->decoder_private = nullptr;
00240     }
00241     return;
00242 
00243 } /* opus_close */
00244 
00245 /*
00246  * Opus Open
00247  * ---------
00248  *  - Creates a new opus file object by using our our callback structure for all IO operations.
00249  *  - SDL expects a returns of 1 on success
00250  */
00251 static int32_t opus_open(Sound_Sample * sample, const char * ext)
00252 {
00253     // Guard against invalid input
00254     assertm(sample, "OPUS: Input is not initialized");
00255     (void) ext; // deliberately unused, but present for API compliance
00256 
00257     int32_t rcode = 0; // assume failure until determined otherwise
00258     auto *internal = static_cast<Sound_SampleInternal*>(sample->opaque);
00259     OggOpusFile *of = op_open_callbacks(internal->rw, &RWops_opus_callbacks, nullptr, 0, &rcode);
00260     internal->decoder_private = of;
00261 
00262     // Had a problem during the open
00263     if (rcode != 0) { // op_open will set rcode to non-zero
00264         opus_close(sample);
00265         SNDDBG(("OPUS: open error:        "
00266                 "'Could not open file due errno: %d'\n", rcode));
00267         return 0; // We return 0 to indicate failure from opus_open
00268     } else
00269         rcode = 1; // Otherwise open succeeded, so set rcode to 1
00270 
00271     const OpusHead* oh = op_head(of, -1);
00272     output_opus_info(of, oh);
00273 
00274     // Populate track properties
00275     sample->actual.rate = OPUS_SAMPLE_RATE;
00276     sample->actual.channels = static_cast<Uint8>(oh->channel_count);
00277     sample->flags = op_seekable(of) ? SOUND_SAMPLEFLAG_CANSEEK: 0;
00278     sample->actual.format = AUDIO_S16SYS;
00279 
00280     // Populate the track's duration in milliseconds (or -1 if bad)
00281     const auto pcm_result = static_cast<int32_t>(op_pcm_total(of, -1)); 
00282     if (pcm_result == OP_EINVAL)
00283         internal->total_time = -1;
00284     else {
00285         constexpr auto frames_per_ms = static_cast<int32_t>(OPUS_SAMPLE_RATE_PER_MS);
00286         internal->total_time = ceil_sdivide(pcm_result, frames_per_ms);
00287     }
00288     return rcode;
00289 } /* opus_open */
00290 
00291 /*
00292  * Opus Read
00293  * ---------
00294  */
00295 static uint32_t opus_read(Sound_Sample * sample, void * buffer, uint32_t requested_frames)
00296 {
00297     // Guard against invalid inputs and the no-op case
00298     assertm(sample && buffer, "OPUS: Inputs are not initialized");
00299     if (requested_frames == 0)
00300         return 0u;
00301 
00302     auto *internal = static_cast<Sound_SampleInternal*>(sample->opaque);
00303     auto *of = static_cast<OggOpusFile*>(internal->decoder_private);
00304     const uint32_t channels = sample->actual.channels;
00305 
00306     // Initial state-tracking variables
00307     uint32_t total_decoded_samples = 0;
00308     auto *buf_pos = static_cast<opus_int16*>(buffer);
00309     int32_t remaining_samples = static_cast<int32_t>(requested_frames * channels);
00310 
00311     // Start the decode loop, incrementing as we go
00312     while(remaining_samples > 0) {
00313         const int result = op_read(of, buf_pos, remaining_samples, nullptr);
00314         if (result == 0) {
00315             sample->flags |= SOUND_SAMPLEFLAG_EOF;
00316             break;
00317         }
00318         if (result == OP_HOLE)
00319             continue;  // hole in the data; keeping going!
00320 
00321         if (result  < 0) {
00322             sample->flags |= SOUND_SAMPLEFLAG_ERROR;
00323             break;
00324         }
00325         // If good, the result contains the number samples decoded per channel (ie: frames)
00326         const uint32_t decoded_samples = static_cast<uint32_t>(result) * channels;
00327         buf_pos               += decoded_samples;
00328         remaining_samples     -= decoded_samples;
00329         total_decoded_samples += decoded_samples;
00330     }
00331 
00332     // Finally, we return the number of frames decoded 
00333     const uint32_t decoded_frames = ceil_udivide(total_decoded_samples, channels);
00334     return decoded_frames;
00335 } /* opus_read */
00336 
00337 /*
00338  * Opus Seek
00339  * ---------
00340  * Set the current position of the stream to the indicated
00341  * integer offset in milliseconds.
00342  */
00343 static int32_t opus_seek(Sound_Sample * sample, const uint32_t ms)
00344 {
00345     // Guard against invalid input
00346     assertm(sample, "OPUS: Input is not initialized");
00347 
00348     int rcode = -1;
00349 
00350     auto *internal = static_cast<Sound_SampleInternal*>(sample->opaque);
00351     auto *of = static_cast<OggOpusFile*>(internal->decoder_private);
00352 
00353 #if (defined DEBUG_CHATTER)
00354     const float total_seconds = ms / 1000.0;
00355     uint8_t minutes = total_seconds / 60;
00356     const double seconds =
00357         static_cast<int>(total_seconds) % 60
00358         + total_seconds
00359         - static_cast<int>(total_seconds);
00360     const uint8_t hours = minutes / 60;
00361     minutes = minutes % 60;
00362 #endif
00363 
00364     // convert the desired ms offset into OPUS PCM samples
00365     const ogg_int64_t desired_pcm = ms * OPUS_SAMPLE_RATE_PER_MS;
00366     rcode = op_pcm_seek(of, desired_pcm);
00367 
00368     if (rcode != 0) {
00369         SNDDBG(("Opus seek problem, see errno:        %d\n", rcode));
00370         sample->flags |= SOUND_SAMPLEFLAG_ERROR;
00371     } else {
00372         SNDDBG(("Opus seek in file:      "
00373                 "{requested_time: '%02d:%02d:%.2f', becomes_opus_pcm: %ld}\n",
00374                 hours, minutes, seconds, desired_pcm));
00375     }
00376     return (rcode == 0);
00377 } /* opus_seek */
00378 
00379 /*
00380  * Opus Rewind
00381  * -----------
00382  * Sets the current position of the stream to 0.
00383  */
00384 static int32_t opus_rewind(Sound_Sample* sample)
00385 {
00386     // Guard against invalid input
00387     assertm(sample, "OPUS: Input is not initialized");
00388     return opus_seek(sample, 0);
00389 } /* opus_rewind */
00390 
00391 
00392 
00393 static const char* extensions_opus[] = { "OPUS", nullptr };
00394 
00395 extern const Sound_DecoderFunctions __Sound_DecoderFunctions_OPUS =
00396 {
00397     {
00398         extensions_opus,
00399         "Ogg Opus audio using libopusfile",
00400         "The DOSBox-X project"
00401     },
00402 
00403     opus_init,   /*   init() method */
00404     opus_quit,   /*   quit() method */
00405     opus_open,   /*   open() method */
00406     opus_close,  /*  close() method */
00407     opus_read,   /*   read() method */
00408     opus_rewind, /* rewind() method */
00409     opus_seek    /*   seek() method */
00410 }; }
00411 /* end of opus.cpp ... */