audio: Ability to read comments/metadata from sound files (#1656)

This commit is contained in:
Wizzerinus | Alex K. 2026-01-16 23:16:01 +03:00 committed by GitHub
parent f025ba5da8
commit 2587247868
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 250 additions and 10 deletions

View File

@ -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];
}

View File

@ -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;

View File

@ -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);

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;

View File

@ -15,6 +15,7 @@
#include "ffmpegAudioCursor.h"
#include "ffmpegAudio.h"
#include "vector_string.h"
extern "C" {
#include <libavutil/dict.h>
#include <libavutil/opt.h>
@ -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;
}

View File

@ -21,6 +21,7 @@
#include "texture.h"
#include "pointerTo.h"
#include "ffmpegVirtualFile.h"
#include "vector_string.h"
extern "C" {
#include <libavcodec/avcodec.h>
@ -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);

View File

@ -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<FlacAudioCursor*>(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<std::istream*>(user);
drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin origin) {
std::istream* stream = static_cast<FlacAudioCursor*>(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<FlacAudioCursor*>(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<FlacAudioCursor*>(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;
}

View File

@ -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"

View File

@ -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();
}

View File

@ -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);

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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() {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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])