diff --git a/doc/classes/AudioStream.xml b/doc/classes/AudioStream.xml index 52e99512813..fe5274f42a7 100644 --- a/doc/classes/AudioStream.xml +++ b/doc/classes/AudioStream.xml @@ -51,6 +51,13 @@ Override this method to customize the name assigned to this audio stream. Unused by the engine. + + + + Override this method to customize the tags for this audio stream. Should return a [Dictionary] of strings with the tag as the key and its content as the value. + Commonly used tags include [code]title[/code], [code]artist[/code], [code]album[/code], [code]tracknumber[/code], and [code]date[/code]. + + diff --git a/doc/classes/AudioStreamWAV.xml b/doc/classes/AudioStreamWAV.xml index 7440b558c1c..e4ee60dfbb3 100644 --- a/doc/classes/AudioStreamWAV.xml +++ b/doc/classes/AudioStreamWAV.xml @@ -79,6 +79,12 @@ If [code]true[/code], audio is stereo. + + Contains user-defined tags if found in the WAV data. + Commonly used tags include [code]title[/code], [code]artist[/code], [code]album[/code], [code]tracknumber[/code], and [code]date[/code] ([code]date[/code] does not have a standard date format). + [b]Note:[/b] No tag is [i]guaranteed[/i] to be present in every file, so make sure to account for the keys not always existing. + [b]Note:[/b] Only WAV files using a [code]LIST[/code] chunk with an identifier of [code]INFO[/code] to encode the tags are currently supported. + diff --git a/modules/vorbis/audio_stream_ogg_vorbis.cpp b/modules/vorbis/audio_stream_ogg_vorbis.cpp index c3e7746b8b5..d948d0cd3ba 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.cpp +++ b/modules/vorbis/audio_stream_ogg_vorbis.cpp @@ -456,6 +456,23 @@ void AudioStreamOggVorbis::maybe_update_info() { ERR_FAIL_COND_MSG(err != 0, "Error parsing header packet " + itos(i) + ": " + itos(err)); } + Dictionary dictionary; + for (int i = 0; i < comment.comments; i++) { + String c = String::utf8(comment.user_comments[i]); + int equals = c.find_char('='); + + if (equals == -1) { + WARN_PRINT("Invalid comment in Ogg Vorbis file."); + continue; + } + + String tag = c.substr(0, equals); + String tag_value = c.substr(equals + 1); + + dictionary[tag.to_lower()] = tag_value; + } + tags = dictionary; + packet_sequence->set_sampling_rate(info.rate); vorbis_comment_clear(&comment); @@ -524,6 +541,14 @@ int AudioStreamOggVorbis::get_bar_beats() const { return bar_beats; } +void AudioStreamOggVorbis::set_tags(const Dictionary &p_tags) { + tags = p_tags; +} + +Dictionary AudioStreamOggVorbis::get_tags() const { + return tags; +} + bool AudioStreamOggVorbis::is_monophonic() const { return false; } @@ -692,10 +717,14 @@ void AudioStreamOggVorbis::_bind_methods() { ClassDB::bind_method(D_METHOD("set_bar_beats", "count"), &AudioStreamOggVorbis::set_bar_beats); ClassDB::bind_method(D_METHOD("get_bar_beats"), &AudioStreamOggVorbis::get_bar_beats); + ClassDB::bind_method(D_METHOD("set_tags", "tags"), &AudioStreamOggVorbis::set_tags); + ClassDB::bind_method(D_METHOD("get_tags"), &AudioStreamOggVorbis::get_tags); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "packet_sequence", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_packet_sequence", "get_packet_sequence"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bpm", PROPERTY_HINT_RANGE, "0,400,0.01,or_greater"), "set_bpm", "get_bpm"); ADD_PROPERTY(PropertyInfo(Variant::INT, "beat_count", PROPERTY_HINT_RANGE, "0,512,1,or_greater"), "set_beat_count", "get_beat_count"); ADD_PROPERTY(PropertyInfo(Variant::INT, "bar_beats", PROPERTY_HINT_RANGE, "2,32,1,or_greater"), "set_bar_beats", "get_bar_beats"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "tags", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_tags", "get_tags"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "loop_offset"), "set_loop_offset", "get_loop_offset"); } diff --git a/modules/vorbis/audio_stream_ogg_vorbis.h b/modules/vorbis/audio_stream_ogg_vorbis.h index 6c6f3435a63..e14df33490a 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.h +++ b/modules/vorbis/audio_stream_ogg_vorbis.h @@ -133,6 +133,7 @@ class AudioStreamOggVorbis : public AudioStream { double bpm = 0; int beat_count = 0; int bar_beats = 4; + Dictionary tags; protected: static void _bind_methods(); @@ -156,6 +157,9 @@ public: void set_bar_beats(int p_bar_beats); virtual int get_bar_beats() const override; + void set_tags(const Dictionary &p_tags); + virtual Dictionary get_tags() const override; + virtual Ref instantiate_playback() override; virtual String get_stream_name() const override; diff --git a/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml b/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml index fa5abe4b86e..5f144baa319 100644 --- a/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml +++ b/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml @@ -41,5 +41,10 @@ Contains the raw Ogg data for this stream. + + Contains user-defined tags if found in the Ogg Vorbis data. + Commonly used tags include [code]title[/code], [code]artist[/code], [code]album[/code], [code]tracknumber[/code], and [code]date[/code] ([code]date[/code] does not have a standard date format). + [b]Note:[/b] No tag is [i]guaranteed[/i] to be present in every file, so make sure to account for the keys not always existing. + diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index 45d3317be92..273bf211adb 100644 --- a/scene/resources/audio_stream_wav.cpp +++ b/scene/resources/audio_stream_wav.cpp @@ -477,6 +477,18 @@ bool AudioStreamWAV::is_stereo() const { return stereo; } +void AudioStreamWAV::set_tags(const Dictionary &p_tags) { + tags = p_tags; +} + +Dictionary AudioStreamWAV::get_tags() const { + return tags; +} + +HashMap::ConstIterator AudioStreamWAV::remap_tag_id(const String &p_tag_id) { + return tag_id_remaps.find(p_tag_id); +} + double AudioStreamWAV::get_length() const { int len = data_bytes; switch (format) { @@ -704,6 +716,8 @@ Ref AudioStreamWAV::load_from_buffer(const Vector &p_st Vector data; + HashMap tag_map; + while (!file->eof_reached()) { /* chunk */ char chunk_id[4]; @@ -859,6 +873,40 @@ Ref AudioStreamWAV::load_from_buffer(const Vector &p_st loop_end = file->get_32(); } } + + if (chunk_id[0] == 'L' && chunk_id[1] == 'I' && chunk_id[2] == 'S' && chunk_id[3] == 'T') { + // RIFF 'LIST' chunk. + // See https://www.recordingblogs.com/wiki/list-chunk-of-a-wave-file + + char list_id[4]; + file->get_buffer((uint8_t *)&list_id, 4); + + if (list_id[0] == 'I' && list_id[1] == 'N' && list_id[2] == 'F' && list_id[3] == 'O') { + // 'INFO' list type. + // The size of an entry can be arbitrary. + uint32_t end_of_chunk = file_pos + chunksize - 4; + while (file->get_position() < end_of_chunk) { + char info_id[4]; + file->get_buffer((uint8_t *)&info_id, 4); + + uint32_t text_size = file->get_32(); + + Vector text; + text.resize(text_size); + file->get_buffer((uint8_t *)&text[0], text_size); + + // The data is always an ASCII string. ASCII is a subset of UTF-8. + String tag; + tag.append_utf8(&info_id[0], 4); + + String tag_value; + tag_value.append_utf8(&text[0], text_size); + + tag_map[tag] = tag_value; + } + } + } + // Move to the start of the next chunk. Note that RIFF requires a padding byte for odd // chunk sizes. file->seek(file_pos + chunksize + (chunksize & 1)); @@ -1098,6 +1146,18 @@ Ref AudioStreamWAV::load_from_buffer(const Vector &p_st sample->set_loop_begin(loop_begin); sample->set_loop_end(loop_end); sample->set_stereo(format_channels == 2); + + Dictionary tag_dictionary; + for (const KeyValue &E : tag_map) { + HashMap::ConstIterator remap = sample->remap_tag_id(E.key); + if (remap) { + tag_map.replace_key(E.key, remap->value); + } + + tag_dictionary[E.key] = E.value; + } + sample->set_tags(tag_dictionary); + return sample; } @@ -1132,6 +1192,9 @@ void AudioStreamWAV::_bind_methods() { ClassDB::bind_method(D_METHOD("set_stereo", "stereo"), &AudioStreamWAV::set_stereo); ClassDB::bind_method(D_METHOD("is_stereo"), &AudioStreamWAV::is_stereo); + ClassDB::bind_method(D_METHOD("set_tags", "tags"), &AudioStreamWAV::set_tags); + ClassDB::bind_method(D_METHOD("get_tags"), &AudioStreamWAV::get_tags); + ClassDB::bind_method(D_METHOD("save_to_wav", "path"), &AudioStreamWAV::save_to_wav); ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data"); @@ -1141,6 +1204,7 @@ void AudioStreamWAV::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_end"), "set_loop_end", "get_loop_end"); ADD_PROPERTY(PropertyInfo(Variant::INT, "mix_rate"), "set_mix_rate", "get_mix_rate"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "stereo"), "set_stereo", "is_stereo"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "tags", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_tags", "get_tags"); BIND_ENUM_CONSTANT(FORMAT_8_BITS); BIND_ENUM_CONSTANT(FORMAT_16_BITS); @@ -1152,3 +1216,22 @@ void AudioStreamWAV::_bind_methods() { BIND_ENUM_CONSTANT(LOOP_PINGPONG); BIND_ENUM_CONSTANT(LOOP_BACKWARD); } + +AudioStreamWAV::AudioStreamWAV() { + // Used to make the metadata tags more unified across different AudioStreams. + // See https://www.recordingblogs.com/wiki/list-chunk-of-a-wave-file + tag_id_remaps["IARL"] = "location"; + tag_id_remaps["IART"] = "artist"; + tag_id_remaps["ICMS"] = "organization"; + tag_id_remaps["ICMT"] = "comments"; + tag_id_remaps["ICOP"] = "copyright"; + tag_id_remaps["ICRD"] = "date"; + tag_id_remaps["IGNR"] = "genre"; + tag_id_remaps["IKEY"] = "keywords"; + tag_id_remaps["IMED"] = "medium"; + tag_id_remaps["INAM"] = "title"; + tag_id_remaps["IPRD"] = "album"; + tag_id_remaps["ISBJ"] = "description"; + tag_id_remaps["ISFT"] = "software"; + tag_id_remaps["ITRK"] = "tracknumber"; +} diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h index 2a00afd6f86..f8aceeecdb1 100644 --- a/scene/resources/audio_stream_wav.h +++ b/scene/resources/audio_stream_wav.h @@ -124,6 +124,9 @@ private: LocalVector data; uint32_t data_bytes = 0; + HashMap tag_id_remaps; + Dictionary tags; + protected: static void _bind_methods(); @@ -149,6 +152,11 @@ public: void set_stereo(bool p_enable); bool is_stereo() const; + void set_tags(const Dictionary &p_tags); + virtual Dictionary get_tags() const override; + + HashMap::ConstIterator remap_tag_id(const String &p_tag_id); + virtual double get_length() const override; //if supported, otherwise return 0 virtual bool is_monophonic() const override; @@ -284,6 +292,8 @@ public: dst_ptr += qoa_encode_frame(data16.ptr(), p_desc, frame_len, dst_ptr); } } + + AudioStreamWAV(); }; VARIANT_ENUM_CAST(AudioStreamWAV::Format) diff --git a/servers/audio/audio_stream.cpp b/servers/audio/audio_stream.cpp index b0559fe2f4c..bae177af0fd 100644 --- a/servers/audio/audio_stream.cpp +++ b/servers/audio/audio_stream.cpp @@ -297,6 +297,12 @@ int AudioStream::get_beat_count() const { return ret; } +Dictionary AudioStream::get_tags() const { + Dictionary ret; + GDVIRTUAL_CALL(_get_tags, ret); + return ret; +} + void AudioStream::tag_used(float p_offset) { if (tagged_frame != AudioServer::get_singleton()->get_mixed_frames()) { offset_count = 0; @@ -350,6 +356,7 @@ void AudioStream::_bind_methods() { GDVIRTUAL_BIND(_is_monophonic); GDVIRTUAL_BIND(_get_bpm) GDVIRTUAL_BIND(_get_beat_count) + GDVIRTUAL_BIND(_get_tags); GDVIRTUAL_BIND(_get_parameter_list) GDVIRTUAL_BIND(_has_loop); GDVIRTUAL_BIND(_get_bar_beats); diff --git a/servers/audio/audio_stream.h b/servers/audio/audio_stream.h index 66876cb7cc0..57f78dd32bc 100644 --- a/servers/audio/audio_stream.h +++ b/servers/audio/audio_stream.h @@ -178,6 +178,7 @@ protected: GDVIRTUAL0RC(bool, _has_loop) GDVIRTUAL0RC(int, _get_bar_beats) GDVIRTUAL0RC(int, _get_beat_count) + GDVIRTUAL0RC(Dictionary, _get_tags); GDVIRTUAL0RC(TypedArray, _get_parameter_list) public: @@ -188,6 +189,7 @@ public: virtual bool has_loop() const; virtual int get_bar_beats() const; virtual int get_beat_count() const; + virtual Dictionary get_tags() const; virtual double get_length() const; virtual bool is_monophonic() const;