From 828f1c10cadc6e54bad5cac56bf60b71d11f2c47 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 24 May 2017 22:19:56 +0200 Subject: [PATCH] Support loading Opus audio files via libopusfile. --- dtool/src/parser-inc/opus/opus.h | 6 + dtool/src/parser-inc/opus/opus_types.h | 19 ++ dtool/src/parser-inc/opus/opusfile.h | 8 + makepanda/makepanda.py | 22 ++- panda/src/movies/config_movies.cxx | 16 ++ panda/src/movies/config_movies.h | 2 + panda/src/movies/opusAudio.I | 12 ++ panda/src/movies/opusAudio.cxx | 68 +++++++ panda/src/movies/opusAudio.h | 61 ++++++ panda/src/movies/opusAudioCursor.I | 12 ++ panda/src/movies/opusAudioCursor.cxx | 224 +++++++++++++++++++++++ panda/src/movies/opusAudioCursor.h | 78 ++++++++ panda/src/movies/p3movies_composite1.cxx | 2 + 13 files changed, 524 insertions(+), 6 deletions(-) create mode 100644 dtool/src/parser-inc/opus/opus.h create mode 100644 dtool/src/parser-inc/opus/opus_types.h create mode 100644 dtool/src/parser-inc/opus/opusfile.h create mode 100644 panda/src/movies/opusAudio.I create mode 100644 panda/src/movies/opusAudio.cxx create mode 100644 panda/src/movies/opusAudio.h create mode 100644 panda/src/movies/opusAudioCursor.I create mode 100644 panda/src/movies/opusAudioCursor.cxx create mode 100644 panda/src/movies/opusAudioCursor.h diff --git a/dtool/src/parser-inc/opus/opus.h b/dtool/src/parser-inc/opus/opus.h new file mode 100644 index 0000000000..8b5341cae7 --- /dev/null +++ b/dtool/src/parser-inc/opus/opus.h @@ -0,0 +1,6 @@ +#ifndef OPUS_H +#define OPUS_H + +#include "opus_types.h" + +#endif diff --git a/dtool/src/parser-inc/opus/opus_types.h b/dtool/src/parser-inc/opus/opus_types.h new file mode 100644 index 0000000000..ca01109eb7 --- /dev/null +++ b/dtool/src/parser-inc/opus/opus_types.h @@ -0,0 +1,19 @@ +#ifndef OPUS_TYPES_H +#define OPUS_TYPES_H + +#include + +typedef int16_t opus_int16; +typedef uint16_t opus_uint16; +typedef int32_t opus_int32; +typedef uint32_t opus_uint32; + +#define opus_int int +#define opus_int64 long long +#define opus_int8 signed char + +#define opus_uint unsigned int +#define opus_uint64 unsigned long long +#define opus_uint8 unsigned char + +#endif diff --git a/dtool/src/parser-inc/opus/opusfile.h b/dtool/src/parser-inc/opus/opusfile.h new file mode 100644 index 0000000000..1d9b656a05 --- /dev/null +++ b/dtool/src/parser-inc/opus/opusfile.h @@ -0,0 +1,8 @@ +#include "opus.h" + +typedef struct OpusHead OpusHead; +typedef struct OpusTags OpusTags; +typedef struct OpusPictureTag OpusPictureTag; +typedef struct OpusServerInfo OpusServerInfo; +typedef struct OpusFileCallbacks OpusFileCallbacks; +typedef struct OggOpusFile OggOpusFile; diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index d2a8127fc4..b5044b1761 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -77,7 +77,7 @@ PkgListSet(["PYTHON", "DIRECT", # Python support "EGL", # OpenGL (ES) integration "EIGEN", # Linear algebra acceleration "OPENAL", "FMODEX", # Audio playback - "VORBIS", "FFMPEG", "SWSCALE", "SWRESAMPLE", # Audio decoding + "VORBIS", "OPUS", "FFMPEG", "SWSCALE", "SWRESAMPLE", # Audio decoding "ODE", "PHYSX", "BULLET", "PANDAPHYSICS", # Physics "SPEEDTREE", # SpeedTree "ZLIB", "PNG", "JPEG", "TIFF", "OPENEXR", "SQUISH", # 2D Formats support @@ -700,6 +700,10 @@ if (COMPILER == "MSVC"): LibName("VORBIS", GetThirdpartyDir() + "vorbis/lib/libogg_static.lib") LibName("VORBIS", GetThirdpartyDir() + "vorbis/lib/libvorbis_static.lib") LibName("VORBIS", GetThirdpartyDir() + "vorbis/lib/libvorbisfile_static.lib") + if (PkgSkip("OPUS")==0): + LibName("OPUS", GetThirdpartyDir() + "opus/lib/libogg_static.lib") + LibName("OPUS", GetThirdpartyDir() + "opus/lib/libopus_static.lib") + LibName("OPUS", GetThirdpartyDir() + "opus/lib/libopusfile_static.lib") for pkg in MAYAVERSIONS: if (PkgSkip(pkg)==0): LibName(pkg, '"' + SDK[pkg] + '/lib/Foundation.lib"') @@ -819,6 +823,7 @@ if (COMPILER=="GCC"): SmartPkgEnable("VRPN", "", ("vrpn", "quat"), ("vrpn", "quat.h", "vrpn/vrpn_Types.h")) SmartPkgEnable("BULLET", "bullet", ("BulletSoftBody", "BulletDynamics", "BulletCollision", "LinearMath"), ("bullet", "bullet/btBulletDynamicsCommon.h")) SmartPkgEnable("VORBIS", "vorbisfile",("vorbisfile", "vorbis", "ogg"), ("ogg/ogg.h", "vorbis/vorbisfile.h")) + SmartPkgEnable("OPUS", "opusfile", ("opusfile", "opus", "ogg"), ("ogg/ogg.h", "opus/opusfile.h")) SmartPkgEnable("JPEG", "", ("jpeg"), "jpeglib.h") SmartPkgEnable("PNG", "libpng", ("png"), "png.h", tool = "libpng-config") @@ -2236,6 +2241,7 @@ DTOOL_CONFIG=[ ("HAVE_PNM", '1', '1'), ("HAVE_STB_IMAGE", '1', '1'), ("HAVE_VORBIS", 'UNDEF', 'UNDEF'), + ("HAVE_OPUS", 'UNDEF', 'UNDEF'), ("HAVE_FREETYPE", 'UNDEF', 'UNDEF'), ("HAVE_FFTW", 'UNDEF', 'UNDEF'), ("HAVE_OPENSSL", 'UNDEF', 'UNDEF'), @@ -3880,10 +3886,10 @@ if (not RUNTIME): # if (not RUNTIME): - OPTS=['DIR:panda/src/movies', 'BUILDING:PANDA', 'VORBIS'] + OPTS=['DIR:panda/src/movies', 'BUILDING:PANDA', 'VORBIS', 'OPUS'] TargetAdd('p3movies_composite1.obj', opts=OPTS, input='p3movies_composite1.cxx') - OPTS=['DIR:panda/src/movies', 'VORBIS', 'PYTHON'] + OPTS=['DIR:panda/src/movies', 'VORBIS', 'OPUS', 'PYTHON'] IGATEFILES=GetDirectoryContents('panda/src/movies', ["*.h", "*_composite*.cxx"]) TargetAdd('libp3movies.in', opts=OPTS, input=IGATEFILES) TargetAdd('libp3movies.in', opts=['IMOD:panda3d.core', 'ILIB:libp3movies', 'SRCDIR:panda/src/movies']) @@ -4017,7 +4023,7 @@ if (not RUNTIME): if (not RUNTIME): OPTS=['DIR:panda/metalibs/panda', 'BUILDING:PANDA', 'JPEG', 'PNG', 'HARFBUZZ', 'TIFF', 'OPENEXR', 'ZLIB', 'OPENSSL', 'FREETYPE', 'FFTW', 'ADVAPI', 'WINSOCK2', - 'SQUISH', 'NVIDIACG', 'VORBIS', 'WINUSER', 'WINMM', 'WINGDI', 'IPHLPAPI'] + 'SQUISH', 'NVIDIACG', 'VORBIS', 'OPUS', 'WINUSER', 'WINMM', 'WINGDI', 'IPHLPAPI'] TargetAdd('panda_panda.obj', opts=OPTS, input='panda.cxx') @@ -7226,10 +7232,14 @@ def MakeInstallerOSX(): if not PkgSkip("FFMPEG"): dist.write(' \n') - else: + elif PkgSkip("VORBIS"): + dist.write(' It is not required for loading .wav or .opus files, which Panda3D can read out of the box.">\n') + elif PkgSkip("OPUS"): dist.write(' It is not required for loading .wav or .ogg files, which Panda3D can read out of the box.">\n') + else: + dist.write(' It is not required for loading .wav, .ogg or .opus files, which Panda3D can read out of the box.">\n') dist.write(' \n') dist.write(' \n') diff --git a/panda/src/movies/config_movies.cxx b/panda/src/movies/config_movies.cxx index fbd80e275d..4884c15f5b 100644 --- a/panda/src/movies/config_movies.cxx +++ b/panda/src/movies/config_movies.cxx @@ -23,6 +23,8 @@ #include "movieTypeRegistry.h" #include "movieVideo.h" #include "movieVideoCursor.h" +#include "opusAudio.h" +#include "opusAudioCursor.h" #include "userDataAudio.h" #include "userDataAudioCursor.h" #include "vorbisAudio.h" @@ -51,6 +53,11 @@ ConfigVariableList load_video_type "either the name of a module, or a space-separate list of filename " "extensions, followed by the name of the module.")); +ConfigVariableBool opus_enable_seek +("opus-enable-seek", true, + PRC_DESC("Set this to false if you're having trouble with seeking while " + "using the Opus decoder.")); + ConfigVariableBool vorbis_enable_seek ("vorbis-enable-seek", true, PRC_DESC("Set this to false if you're having trouble with seeking while " @@ -91,6 +98,11 @@ init_libmovies() { WavAudio::init_type(); WavAudioCursor::init_type(); +#ifdef HAVE_OPUS + OpusAudio::init_type(); + OpusAudioCursor::init_type(); +#endif + #ifdef HAVE_VORBIS VorbisAudio::init_type(); VorbisAudioCursor::init_type(); @@ -100,6 +112,10 @@ init_libmovies() { reg->register_audio_type(&FlacAudio::make, "flac"); reg->register_audio_type(&WavAudio::make, "wav wave"); +#ifdef HAVE_OPUS + reg->register_audio_type(&OpusAudio::make, "opus"); +#endif + #ifdef HAVE_VORBIS reg->register_audio_type(&VorbisAudio::make, "ogg oga"); #endif diff --git a/panda/src/movies/config_movies.h b/panda/src/movies/config_movies.h index cbfd09a440..1d8d6ae93e 100644 --- a/panda/src/movies/config_movies.h +++ b/panda/src/movies/config_movies.h @@ -27,6 +27,8 @@ NotifyCategoryDecl(movies, EXPCL_PANDA_MOVIES, EXPTP_PANDA_MOVIES); extern ConfigVariableList load_audio_type; extern ConfigVariableList load_video_type; +extern ConfigVariableBool opus_enable_seek; + extern ConfigVariableBool vorbis_enable_seek; extern ConfigVariableBool vorbis_seek_lap; diff --git a/panda/src/movies/opusAudio.I b/panda/src/movies/opusAudio.I new file mode 100644 index 0000000000..23f144fe3a --- /dev/null +++ b/panda/src/movies/opusAudio.I @@ -0,0 +1,12 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file opusAudio.I + * @author rdb + * @date 2017-05-24 + */ diff --git a/panda/src/movies/opusAudio.cxx b/panda/src/movies/opusAudio.cxx new file mode 100644 index 0000000000..024560156a --- /dev/null +++ b/panda/src/movies/opusAudio.cxx @@ -0,0 +1,68 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file opusAudio.cxx + * @author rdb + * @date 2017-05-24 + */ + +#include "opusAudio.h" +#include "opusAudioCursor.h" +#include "virtualFileSystem.h" +#include "dcast.h" + +#ifdef HAVE_OPUS + +TypeHandle OpusAudio::_type_handle; + +/** + * xxx + */ +OpusAudio:: +OpusAudio(const Filename &name) : + MovieAudio(name) +{ + _filename = name; +} + +/** + * xxx + */ +OpusAudio:: +~OpusAudio() { +} + +/** + * Open this audio, returning a MovieAudioCursor + */ +PT(MovieAudioCursor) OpusAudio:: +open() { + VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr(); + istream *stream = vfs->open_read_file(_filename, true); + + if (stream == nullptr) { + return nullptr; + } else { + PT(OpusAudioCursor) cursor = new OpusAudioCursor(this, stream); + if (cursor == nullptr || !cursor->_is_valid) { + return nullptr; + } else { + return DCAST(MovieAudioCursor, cursor); + } + } +} + +/** + * Obtains a MovieAudio that references a file. + */ +PT(MovieAudio) OpusAudio:: +make(const Filename &name) { + return DCAST(MovieAudio, new OpusAudio(name)); +} + +#endif // HAVE_OPUS diff --git a/panda/src/movies/opusAudio.h b/panda/src/movies/opusAudio.h new file mode 100644 index 0000000000..813a4dbd58 --- /dev/null +++ b/panda/src/movies/opusAudio.h @@ -0,0 +1,61 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file opusAudio.h + * @author rdb + * @date 2017-05-24 + */ + +#ifndef OPUSAUDIO_H +#define OPUSAUDIO_H + +#include "pandabase.h" +#include "movieAudio.h" + +#ifdef HAVE_OPUS + +class OpusAudioCursor; + +/** + * Interfaces with the libopusfile library to implement decoding of Opus + * audio files. + */ +class EXPCL_PANDA_MOVIES OpusAudio : public MovieAudio { +PUBLISHED: + OpusAudio(const Filename &name); + virtual ~OpusAudio(); + virtual PT(MovieAudioCursor) open(); + + static PT(MovieAudio) make(const Filename &name); + +private: + friend class OpusAudioCursor; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + TypedWritableReferenceCount::init_type(); + register_type(_type_handle, "OpusAudio", + MovieAudio::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +#include "opusAudio.I" + +#endif // HAVE_OPUS + +#endif // OPUSAUDIO_H diff --git a/panda/src/movies/opusAudioCursor.I b/panda/src/movies/opusAudioCursor.I new file mode 100644 index 0000000000..1e923c3a80 --- /dev/null +++ b/panda/src/movies/opusAudioCursor.I @@ -0,0 +1,12 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file opusAudioCursor.I + * @author rdb + * @date 2017-05-24 + */ diff --git a/panda/src/movies/opusAudioCursor.cxx b/panda/src/movies/opusAudioCursor.cxx new file mode 100644 index 0000000000..698648c5cb --- /dev/null +++ b/panda/src/movies/opusAudioCursor.cxx @@ -0,0 +1,224 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file opusAudioCursor.cxx + * @author rdb + * @date 2017-05-24 + */ + +#include "opusAudioCursor.h" +#include "virtualFileSystem.h" + +#ifdef HAVE_OPUS + +#include + +/** + * Callbacks passed to libopusfile to implement file I/O via the + * VirtualFileSystem. + */ +int cb_read(void *stream, unsigned char *ptr, int nbytes) { + istream *in = (istream *)stream; + nassertr(in != nullptr, -1); + + in->read((char *)ptr, nbytes); + + if (in->eof()) { + // Gracefully handle EOF. + in->clear(); + } + + return in->gcount(); +} + +int cb_seek(void *stream, opus_int64 offset, int whence) { + if (!opus_enable_seek) { + return -1; + } + + istream *in = (istream *)stream; + nassertr(in != nullptr, -1); + + switch (whence) { + case SEEK_SET: + in->seekg(offset, ios::beg); + break; + + case SEEK_CUR: + in->seekg(offset, ios::cur); + break; + + case SEEK_END: + in->seekg(offset, ios::end); + break; + + default: + movies_cat.error() + << "Illegal parameter to seek in cb_seek\n"; + return -1; + } + + if (in->fail()) { + movies_cat.error() + << "Failure to seek to byte " << offset; + + switch (whence) { + case SEEK_CUR: + movies_cat.error(false) + << " from current location!\n"; + break; + + case SEEK_END: + movies_cat.error(false) + << " from end of file!\n"; + break; + + default: + movies_cat.error(false) << "!\n"; + } + + return -1; + } + + return 0; +} + +opus_int64 cb_tell(void *stream) { + istream *in = (istream *)stream; + nassertr(in != nullptr, -1); + + return in->tellg(); +} + +int cb_close(void *stream) { + istream *in = (istream *)stream; + nassertr(in != nullptr, EOF); + + VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr(); + vfs->close_read_file(in); + return 0; +} + +static const OpusFileCallbacks callbacks = {cb_read, cb_seek, cb_tell, cb_close}; + +TypeHandle OpusAudioCursor::_type_handle; + +/** + * Reads the .wav header from the indicated stream. This leaves the read + * pointer positioned at the start of the data. + */ +OpusAudioCursor:: +OpusAudioCursor(OpusAudio *src, istream *stream) : + MovieAudioCursor(src), + _is_valid(false), + _link(0) +{ + nassertv(stream != nullptr); + nassertv(stream->good()); + + int error = 0; + _op = op_open_callbacks((void *)stream, &callbacks, nullptr, 0, &error); + if (_op == nullptr) { + movies_cat.error() + << "Failed to read Opus file (error code " << error << ").\n"; + return; + } + + ogg_int64_t samples = op_pcm_total(_op, -1); + if (samples != OP_EINVAL) { + // Opus timestamps are fixed at 48 kHz. + _length = (double)samples / 48000.0; + } + + _audio_channels = op_channel_count(_op, -1); + _audio_rate = 48000; + + _can_seek = opus_enable_seek && op_seekable(_op); + _can_seek_fast = _can_seek; + + _is_valid = true; +} + +/** + * xxx + */ +OpusAudioCursor:: +~OpusAudioCursor() { + if (_op != nullptr) { + op_free(_op); + _op = nullptr; + } +} + +/** + * Seeks to a target location. Afterward, the packet_time is guaranteed to be + * less than or equal to the specified time. + */ +void OpusAudioCursor:: +seek(double t) { + if (!opus_enable_seek) { + return; + } + + t = max(t, 0.0); + + // Use op_time_seek_lap if cross-lapping is enabled. + int error = op_pcm_seek(_op, (ogg_int64_t)(t * 48000.0)); + if (error != 0) { + movies_cat.error() + << "Seek failed (error " << error << "). Opus stream may not be seekable.\n"; + return; + } + + _last_seek = op_pcm_tell(_op) / 48000.0; + _samples_read = 0; +} + +/** + * Read audio samples from the stream. N is the number of samples you wish to + * read. Your buffer must be equal in size to N * channels. Multiple-channel + * audio will be interleaved. + */ +void OpusAudioCursor:: +read_samples(int n, int16_t *data) { + int16_t *end = data + (n * _audio_channels); + + while (data < end) { + // op_read gives it to us in the exact format we need. Nifty! + int link; + int read_samples = op_read(_op, data, end - data, &link); + if (read_samples > 0) { + data += read_samples * _audio_channels; + _samples_read += read_samples; + } else { + break; + } + + if (_link != link) { + // It is technically possible for it to change parameters from one link + // to the next. However, we don't offer this flexibility. + int channels = op_channel_count(_op, link); + if (channels != _audio_channels) { + movies_cat.error() + << "Opus file has inconsistent channel count!\n"; + + // We'll change it anyway. Not sure what happens next. + _audio_channels = channels; + } + + _link = link; + } + } + + // Fill the rest of the buffer with silence. + if (data < end) { + memset(data, 0, (unsigned char *)end - (unsigned char *)data); + } +} + +#endif // HAVE_OPUS diff --git a/panda/src/movies/opusAudioCursor.h b/panda/src/movies/opusAudioCursor.h new file mode 100644 index 0000000000..55a717afcf --- /dev/null +++ b/panda/src/movies/opusAudioCursor.h @@ -0,0 +1,78 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file opusAudioCursor.h + * @author rdb + * @date 2017-05-24 + */ + +#ifndef OPUSAUDIOCURSOR_H +#define OPUSAUDIOCURSOR_H + +#include "pandabase.h" +#include "movieAudioCursor.h" + +#ifdef HAVE_OPUS + +#include + +typedef struct OggOpusFile OggOpusFile; + +class OpusAudio; + +/** + * Interfaces with the libopusfile library to implement decoding of Opus + * audio files. + */ +class EXPCL_PANDA_MOVIES OpusAudioCursor : public MovieAudioCursor { +PUBLISHED: + OpusAudioCursor(OpusAudio *src, istream *stream); + virtual ~OpusAudioCursor(); + virtual void seek(double offset); + +public: + virtual void read_samples(int n, int16_t *data); + + bool _is_valid; + +protected: + OggOpusFile *_op; + + int _link; + double _byte_rate; + int _block_align; + int _bytes_per_sample; + bool _is_float; + + streampos _data_start; + streampos _data_pos; + size_t _data_size; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + MovieAudioCursor::init_type(); + register_type(_type_handle, "OpusAudioCursor", + MovieAudioCursor::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +#include "opusAudioCursor.I" + +#endif // HAVE_OPUS + +#endif // OPUSAUDIOCURSOR_H diff --git a/panda/src/movies/p3movies_composite1.cxx b/panda/src/movies/p3movies_composite1.cxx index ea526c30b1..61954c669e 100644 --- a/panda/src/movies/p3movies_composite1.cxx +++ b/panda/src/movies/p3movies_composite1.cxx @@ -10,6 +10,8 @@ #include "movieTypeRegistry.cxx" #include "movieVideo.cxx" #include "movieVideoCursor.cxx" +#include "opusAudio.cxx" +#include "opusAudioCursor.cxx" #include "userDataAudio.cxx" #include "userDataAudioCursor.cxx" #include "vorbisAudio.cxx"