audio: Ability to read comments/metadata from sound files (#1656)
This commit is contained in:
parent
f025ba5da8
commit
2587247868
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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])
|
||||
|
|
|
|||
Loading…
Reference in New Issue