DOSBox-X
|
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 ... */