diff --git a/panda/src/audio/audioSound.cxx b/panda/src/audio/audioSound.cxx index 946d1bd9f0..e3c1a11c39 100644 --- a/panda/src/audio/audioSound.cxx +++ b/panda/src/audio/audioSound.cxx @@ -13,6 +13,7 @@ */ #include "audioSound.h" +#include "vector_string.h" using std::ostream; @@ -196,3 +197,60 @@ operator << (ostream &out, AudioSound::SoundStatus status) { return out << "**invalid AudioSound::SoundStatus(" << (int)status << ")**"; } + +static const vector_string empty; + +/** + * Get the comment attached to this AudioSound as a list of strings. + */ +const vector_string& AudioSound:: +get_raw_comment() const { + return empty; +} + +/** + * Returns true if this AudioSound has a comment with the given key, + * i.e. "author". Case-sensitive. + */ +bool AudioSound:: +has_comment(const std::string &key) const { + for (const std::string &st : get_raw_comment()) { + if (st.rfind(key + "=", 0) == 0) { + return true; + } + } + return false; +} + +/** + * Returns the value for a given key in the comment. If the key is not present, + * returns an empty string. + */ +std::string AudioSound:: +get_comment(const std::string &key) const { + for (const std::string &st : get_raw_comment()) { + if (st.rfind(key + "=", 0) == 0) { + return st.substr(key.length() + 1); + } + } + return ""; +} + +/** + * Returns the number of comments this sound has. + */ +int AudioSound:: +get_num_raw_comments() const { + return get_raw_comment().size(); +} + +/** + * Returns the comment at a given index. + */ +std::string AudioSound:: +get_raw_comment(int index) const { + if (index >= get_num_raw_comments() || index < 0) { + return ""; + } + return get_raw_comment()[index]; +} diff --git a/panda/src/audio/audioSound.h b/panda/src/audio/audioSound.h index c1aa059c35..a40bbdb31d 100644 --- a/panda/src/audio/audioSound.h +++ b/panda/src/audio/audioSound.h @@ -20,6 +20,7 @@ #include "pointerTo.h" #include "filterProperties.h" #include "luse.h" +#include "vector_string.h" class AudioManager; @@ -141,6 +142,16 @@ PUBLISHED: enum SoundStatus { BAD, READY, PLAYING }; virtual SoundStatus status() const = 0; + bool has_comment(const std::string &key) const; + std::string get_comment(const std::string &key) const; + MAKE_MAP_PROPERTY(comments, has_comment, get_comment); + + virtual const vector_string& get_raw_comment() const; + + int get_num_raw_comments() const; + std::string get_raw_comment(int index) const; + MAKE_SEQ_PROPERTY(raw_comments, get_num_raw_comments, get_raw_comment); + virtual void output(std::ostream &out) const; virtual void write(std::ostream &out) const; diff --git a/panda/src/audiotraits/openalAudioManager.cxx b/panda/src/audiotraits/openalAudioManager.cxx index d45060c33c..451cd172f4 100644 --- a/panda/src/audiotraits/openalAudioManager.cxx +++ b/panda/src/audiotraits/openalAudioManager.cxx @@ -440,6 +440,7 @@ get_sound_data(MovieAudio *movie, int mode) { sd->_rate = stream->audio_rate(); sd->_channels = stream->audio_channels(); sd->_length = stream->length(); + sd->_comment = stream->get_raw_comment(); audio_debug("Creating: " << sd->_movie->get_filename().get_basename()); audio_debug(" - Rate: " << sd->_rate); audio_debug(" - Channels: " << sd->_channels); diff --git a/panda/src/audiotraits/openalAudioManager.h b/panda/src/audiotraits/openalAudioManager.h index 56b01afc6c..236dc92f23 100644 --- a/panda/src/audiotraits/openalAudioManager.h +++ b/panda/src/audiotraits/openalAudioManager.h @@ -21,6 +21,7 @@ #include "pset.h" #include "movieAudioCursor.h" #include "reMutex.h" +#include "vector_string.h" // OSX uses the OpenAL framework #ifdef HAVE_OPENAL_FRAMEWORK @@ -173,6 +174,8 @@ private: int _channels; int _client_count; ExpirationQueue::iterator _expire; + + vector_string _comment; }; diff --git a/panda/src/audiotraits/openalAudioSound.cxx b/panda/src/audiotraits/openalAudioSound.cxx index d334a4cd3d..8b404f1cba 100644 --- a/panda/src/audiotraits/openalAudioSound.cxx +++ b/panda/src/audiotraits/openalAudioSound.cxx @@ -17,6 +17,7 @@ #include "coordinateSystem.h" #include "openalAudioSound.h" #include "openalAudioManager.h" +#include "vector_string.h" TypeHandle OpenALAudioSound::_type_handle; @@ -88,6 +89,8 @@ OpenALAudioSound(OpenALAudioManager *manager, audio_warning("stereo sound " << movie->get_filename() << " will not be spatialized"); } } + + _comment = std::move(_sd->_comment); release_sound_data(false); } @@ -1092,3 +1095,11 @@ status() const { return AudioSound::PLAYING; } } + +/** + * Returns the comments attached to this audio file. + */ +const vector_string& OpenALAudioSound:: +get_raw_comment() const { + return _comment; +} diff --git a/panda/src/audiotraits/openalAudioSound.h b/panda/src/audiotraits/openalAudioSound.h index 6dc30a936c..48394bd175 100644 --- a/panda/src/audiotraits/openalAudioSound.h +++ b/panda/src/audiotraits/openalAudioSound.h @@ -19,6 +19,7 @@ #include "movieAudioCursor.h" #include "trueClock.h" #include "openalAudioManager.h" +#include "vector_string.h" // OSX uses the OpenAL framework #ifdef HAVE_OPENAL_FRAMEWORK @@ -117,6 +118,8 @@ public: void finished(); + const vector_string& get_raw_comment() const; + private: OpenALAudioSound(OpenALAudioManager* manager, MovieAudio *movie, @@ -214,6 +217,8 @@ private: PN_stdfloat _cone_outer_angle; PN_stdfloat _cone_outer_gain; + vector_string _comment; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/ffmpeg/ffmpegAudioCursor.cxx b/panda/src/ffmpeg/ffmpegAudioCursor.cxx index 69635549a6..89301ffa17 100644 --- a/panda/src/ffmpeg/ffmpegAudioCursor.cxx +++ b/panda/src/ffmpeg/ffmpegAudioCursor.cxx @@ -15,6 +15,7 @@ #include "ffmpegAudioCursor.h" #include "ffmpegAudio.h" +#include "vector_string.h" extern "C" { #include #include @@ -508,3 +509,19 @@ read_samples(int n, int16_t *data) { _samples_read += n; return n; } + +/** + * + */ +vector_string FfmpegAudioCursor:: +get_raw_comment() const { + AVDictionaryEntry *tag = nullptr; + vector_string comment_strings; + while ((tag = av_dict_get(_format_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { + std::string comment(tag->key); + comment.append("="); + comment.append(tag->value); + comment_strings.push_back(std::move(comment)); + } + return comment_strings; +} diff --git a/panda/src/ffmpeg/ffmpegAudioCursor.h b/panda/src/ffmpeg/ffmpegAudioCursor.h index 77e8890d6c..02c53df700 100644 --- a/panda/src/ffmpeg/ffmpegAudioCursor.h +++ b/panda/src/ffmpeg/ffmpegAudioCursor.h @@ -21,6 +21,7 @@ #include "texture.h" #include "pointerTo.h" #include "ffmpegVirtualFile.h" +#include "vector_string.h" extern "C" { #include @@ -43,6 +44,7 @@ PUBLISHED: FfmpegAudioCursor(FfmpegAudio *src); virtual ~FfmpegAudioCursor(); virtual void seek(double offset); + vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); diff --git a/panda/src/movies/flacAudioCursor.cxx b/panda/src/movies/flacAudioCursor.cxx index 0927f54b5d..1af4121146 100644 --- a/panda/src/movies/flacAudioCursor.cxx +++ b/panda/src/movies/flacAudioCursor.cxx @@ -24,8 +24,8 @@ extern "C" { /** * Callback passed to dr_flac to implement file I/O via the VirtualFileSystem. */ -static size_t cb_read_proc(void *user, void *buffer, size_t size) { - std::istream *stream = (std::istream *)user; +size_t cb_read_proc(void *user, void *buffer, size_t size) { + std::istream *stream = static_cast(user)->_stream; nassertr(stream != nullptr, 0); stream->read((char *)buffer, size); @@ -41,8 +41,8 @@ static size_t cb_read_proc(void *user, void *buffer, size_t size) { /** * Callback passed to dr_flac to implement file seeking via the VirtualFileSystem. */ -static drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin origin) { - std::istream* stream = static_cast(user); +drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin origin) { + std::istream* stream = static_cast(user)->_stream; nassertr(stream != nullptr, DRFLAC_FALSE); std::ios_base::seekdir dir; @@ -64,14 +64,30 @@ static drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin ori /** * Callback passed to dr_flac to report the current stream position. */ -static drflac_bool32 cb_tell_proc(void *user, drflac_int64 *pCursor) { - std::istream *stream = (std::istream *)user; +drflac_bool32 cb_tell_proc(void *user, drflac_int64 *pCursor) { + std::istream *stream = static_cast(user)->_stream; nassertr(stream != nullptr, DRFLAC_FALSE); *pCursor = (drflac_int64)stream->tellg(); return !stream->fail() ? DRFLAC_TRUE : DRFLAC_FALSE; } +/** + * Callback passed to dr_flac to process metadata found inside flac files. + */ +void cb_meta_proc(void* pUserData, drflac_metadata* pMetadata) { + FlacAudioCursor* cursor = static_cast(pUserData); + if (pMetadata->type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) { + drflac_vorbis_comment_iterator iter; + drflac_init_vorbis_comment_iterator(&iter, pMetadata->data.vorbis_comment.commentCount, pMetadata->data.vorbis_comment.pComments); + const char* next_comment; + drflac_uint32 comment_length; + while ((next_comment = drflac_next_vorbis_comment(&iter, &comment_length)) != nullptr) { + cursor->raw_comment.push_back({ next_comment, comment_length }); + } + } +} + TypeHandle FlacAudioCursor::_type_handle; /** @@ -83,12 +99,13 @@ FlacAudioCursor(FlacAudio *src, std::istream *stream) : MovieAudioCursor(src), _is_valid(false), _drflac(nullptr), - _stream(stream) + _stream(stream), + raw_comment() { nassertv(stream != nullptr); nassertv(stream->good()); - _drflac = drflac_open(&cb_read_proc, &cb_seek_proc, &cb_tell_proc, (void *)stream, nullptr); + _drflac = drflac_open_with_metadata(&cb_read_proc, &cb_seek_proc, &cb_tell_proc, &cb_meta_proc, (void *)this, nullptr); if (_drflac == nullptr) { movies_cat.error() @@ -147,3 +164,11 @@ read_samples(int n, int16_t *data) { _samples_read += n; return n; } + +/** + * + */ +vector_string FlacAudioCursor:: +get_raw_comment() const { + return raw_comment; +} diff --git a/panda/src/movies/flacAudioCursor.h b/panda/src/movies/flacAudioCursor.h index 93a48e5f32..349b004724 100644 --- a/panda/src/movies/flacAudioCursor.h +++ b/panda/src/movies/flacAudioCursor.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "movieAudioCursor.h" +#include "vector_string.h" #define DR_FLAC_NO_STDIO extern "C" { @@ -31,6 +32,11 @@ class FlacAudio; * @since 1.10.0 */ class EXPCL_PANDA_MOVIES FlacAudioCursor : public MovieAudioCursor { + friend drflac_bool32 cb_tell_proc(void* pUserData, drflac_int64 *pCursor); + friend drflac_bool32 cb_seek_proc(void* pUserData, int offset, drflac_seek_origin origin); + friend size_t cb_read_proc(void* pUserData, void *buffer, size_t size); + friend void cb_meta_proc(void* pUserData, drflac_metadata* pMetadata); + PUBLISHED: explicit FlacAudioCursor(FlacAudio *src, std::istream *stream); virtual ~FlacAudioCursor(); @@ -58,9 +64,11 @@ public: return get_class_type(); } virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + vector_string get_raw_comment() const; private: static TypeHandle _type_handle; + vector_string raw_comment; }; #include "flacAudioCursor.I" diff --git a/panda/src/movies/movieAudioCursor.cxx b/panda/src/movies/movieAudioCursor.cxx index a836937455..374b7a9f93 100644 --- a/panda/src/movies/movieAudioCursor.cxx +++ b/panda/src/movies/movieAudioCursor.cxx @@ -12,6 +12,7 @@ */ #include "movieAudioCursor.h" +#include "vector_string.h" TypeHandle MovieAudioCursor::_type_handle; @@ -167,3 +168,11 @@ int MovieAudioCursor:: ready() const { return 0x40000000; } + +/** + * + */ +vector_string MovieAudioCursor:: +get_raw_comment() const { + return vector_string(); +} diff --git a/panda/src/movies/movieAudioCursor.h b/panda/src/movies/movieAudioCursor.h index e7c9668713..b810b04a59 100644 --- a/panda/src/movies/movieAudioCursor.h +++ b/panda/src/movies/movieAudioCursor.h @@ -18,6 +18,7 @@ #include "namable.h" #include "texture.h" #include "pointerTo.h" +#include "vector_string.h" class MovieAudio; /** @@ -49,6 +50,7 @@ PUBLISHED: virtual void seek(double offset); void read_samples(int n, Datagram *dg); vector_uchar read_samples(int n); + virtual vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); diff --git a/panda/src/movies/opusAudioCursor.cxx b/panda/src/movies/opusAudioCursor.cxx index ab81068742..afdf738f26 100644 --- a/panda/src/movies/opusAudioCursor.cxx +++ b/panda/src/movies/opusAudioCursor.cxx @@ -17,6 +17,7 @@ #include "opusAudio.h" #include "virtualFileSystem.h" +#include "vector_string.h" #ifdef HAVE_OPUS @@ -121,6 +122,20 @@ static const OpusFileCallbacks callbacks = {cb_read, cb_seek, cb_tell, nullptr}; TypeHandle OpusAudioCursor::_type_handle; +/** + * + */ +vector_string +parse_opus_comments(const OpusTags *com) { + // Blatant copy from vorbis code because they're the same struct + vector_string comments; + for (int cnum = 0; cnum < com->comments; ++cnum) { + std::string tag(com->user_comments[cnum], com->comment_lengths[cnum]); + comments.push_back(std::move(tag)); + } + return comments; +} + /** * Reads the .wav header from the indicated stream. This leaves the read * pointer positioned at the start of the data. @@ -156,6 +171,9 @@ OpusAudioCursor(OpusAudio *src, istream *stream) : _can_seek_fast = _can_seek; _is_valid = true; + + const OpusTags *tag = op_tags(_op, -1); + _comment = parse_opus_comments(tag); } /** @@ -270,4 +288,12 @@ read_samples(int n, int16_t *data) { return n; } +/** + * + */ +vector_string OpusAudioCursor:: +get_raw_comment() const { + return _comment; +} + #endif // HAVE_OPUS diff --git a/panda/src/movies/opusAudioCursor.h b/panda/src/movies/opusAudioCursor.h index 3e2137de5e..26dea22a63 100644 --- a/panda/src/movies/opusAudioCursor.h +++ b/panda/src/movies/opusAudioCursor.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "movieAudioCursor.h" +#include "vector_string.h" #ifdef HAVE_OPUS @@ -37,6 +38,7 @@ PUBLISHED: explicit OpusAudioCursor(OpusAudio *src, std::istream *stream); virtual ~OpusAudioCursor(); virtual void seek(double offset); + vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); @@ -47,6 +49,7 @@ protected: OggOpusFile *_op; std::istream *_stream; int _link; + vector_string _comment; public: static TypeHandle get_class_type() { diff --git a/panda/src/movies/vorbisAudioCursor.cxx b/panda/src/movies/vorbisAudioCursor.cxx index 181b9454c4..e7954d2bc5 100644 --- a/panda/src/movies/vorbisAudioCursor.cxx +++ b/panda/src/movies/vorbisAudioCursor.cxx @@ -17,6 +17,7 @@ #include "vorbisAudio.h" #include "virtualFileSystem.h" +#include "vector_string.h" #ifdef HAVE_VORBIS @@ -24,6 +25,19 @@ using std::istream; TypeHandle VorbisAudioCursor::_type_handle; +/** + * + */ +vector_string +parse_vorbis_comments(vorbis_comment *com) { + vector_string comments; + for (int cnum = 0; cnum < com->comments; ++cnum) { + std::string tag(com->user_comments[cnum], com->comment_lengths[cnum]); + comments.push_back(std::move(tag)); + } + return comments; +} + /** * Reads the .wav header from the indicated stream. This leaves the read * pointer positioned at the start of the data. @@ -67,6 +81,9 @@ VorbisAudioCursor(VorbisAudio *src, istream *stream) : _can_seek = vorbis_enable_seek && (ov_seekable(&_ov) != 0); _can_seek_fast = _can_seek; + vorbis_comment *com = ov_comment(&_ov, -1); + _comment = parse_vorbis_comments(com); + _is_valid = true; } @@ -313,4 +330,12 @@ cb_tell_func(void *datasource) { return stream->tellg(); } +/** + * + */ +vector_string VorbisAudioCursor:: +get_raw_comment() const { + return _comment; +} + #endif // HAVE_VORBIS diff --git a/panda/src/movies/vorbisAudioCursor.h b/panda/src/movies/vorbisAudioCursor.h index cd74acc750..2bb48dfa3c 100644 --- a/panda/src/movies/vorbisAudioCursor.h +++ b/panda/src/movies/vorbisAudioCursor.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "movieAudioCursor.h" +#include "vector_string.h" #ifdef HAVE_VORBIS @@ -33,6 +34,7 @@ PUBLISHED: explicit VorbisAudioCursor(VorbisAudio *src, std::istream *stream); virtual ~VorbisAudioCursor(); virtual void seek(double offset); + vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); @@ -51,6 +53,7 @@ protected: OggVorbis_File _ov; #endif int _bitstream; + vector_string _comment; public: static TypeHandle get_class_type() { diff --git a/tests/audio/openclose_with_comments.flac b/tests/audio/openclose_with_comments.flac new file mode 100644 index 0000000000..a32b3836e8 Binary files /dev/null and b/tests/audio/openclose_with_comments.flac differ diff --git a/tests/audio/openclose_with_comments.mp3 b/tests/audio/openclose_with_comments.mp3 new file mode 100644 index 0000000000..f594029c9b Binary files /dev/null and b/tests/audio/openclose_with_comments.mp3 differ diff --git a/tests/audio/openclose_with_comments.ogg b/tests/audio/openclose_with_comments.ogg new file mode 100644 index 0000000000..479d94d7db Binary files /dev/null and b/tests/audio/openclose_with_comments.ogg differ diff --git a/tests/audio/openclose_with_comments.opus b/tests/audio/openclose_with_comments.opus new file mode 100644 index 0000000000..ec3bd78b48 Binary files /dev/null and b/tests/audio/openclose_with_comments.opus differ diff --git a/tests/audio/test_loading.py b/tests/audio/test_loading.py index ea3bdb2cd9..46032e7351 100644 --- a/tests/audio/test_loading.py +++ b/tests/audio/test_loading.py @@ -1,5 +1,36 @@ +import os + import pytest +from panda3d.core import Filename + def test_missing_file(audiomgr): - sound = audiomgr.get_sound('/not/a/valid/file.ogg') - assert str(sound).startswith('NullAudioSound') + sound = audiomgr.get_sound("/not/a/valid/file.ogg") + assert str(sound).startswith("NullAudioSound") + + +@pytest.mark.parametrize("extension", ["ogg", "opus", "mp3", "flac"]) +def test_comments(audiomgr, extension): + if "openal" not in str(audiomgr).lower(): + # NULL audio manager (as well as fmod) don't support comment reading + pytest.skip("Comment reading is only supported on OpenAL") + # ogg should be loaded with libvorbis, opus with libopus, mp3 with ffmpeg + # we cannot test this though because after loading the loader information is gone + sound_path = os.path.join( + os.path.dirname(__file__), f"openclose_with_comments.{extension}" + ) + sound_path = Filename.from_os_specific(sound_path) + sound = audiomgr.get_sound(sound_path) + if extension == "mp3": # FFMPEG encodes/decodes tags differently + tags = ["artist", "comment", "genre"] + else: + tags = ["ARTIST", "COMMENTS", "GENRE"] + assert sorted(sound.raw_comments) == [ + f"{tags[0]}=Example Artist", + f"{tags[1]}=This is an example OGG comment", + f"{tags[2]}=Blues", + ] + assert sound.comments[tags[-1]] == "Blues" + assert sound.get_comment(tags[0]) == "Example Artist" + assert sound.get_comment("DOES NOT EXIST") == "" + assert sound.has_comment(tags[0])