diff options
author | kleines Filmröllchen <filmroellchen@serenityos.org> | 2023-03-07 16:58:01 +0100 |
---|---|---|
committer | Tim Flynn <trflynn89@pm.me> | 2023-03-13 12:35:17 -0400 |
commit | a8963a270f09eb78d7cb6d23e1b9bd6c6dc734a1 (patch) | |
tree | 87e35a80b1e1bed74123a9ca88b32107a04e5150 /Userland/Libraries | |
parent | d8e8ddedf37e16b41186c2b8dd0b247e1676c4af (diff) | |
download | serenity-a8963a270f09eb78d7cb6d23e1b9bd6c6dc734a1.zip |
LibAudio: Detect and read FLAC metadata
FLAC uses the very simple vorbis comment metadata format, which we can
now read most standard and non-standard fields from.
Diffstat (limited to 'Userland/Libraries')
-rw-r--r-- | Userland/Libraries/LibAudio/CMakeLists.txt | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibAudio/FlacLoader.cpp | 16 | ||||
-rw-r--r-- | Userland/Libraries/LibAudio/FlacLoader.h | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibAudio/VorbisComment.cpp | 122 | ||||
-rw-r--r-- | Userland/Libraries/LibAudio/VorbisComment.h | 18 |
5 files changed, 160 insertions, 1 deletions
diff --git a/Userland/Libraries/LibAudio/CMakeLists.txt b/Userland/Libraries/LibAudio/CMakeLists.txt index b77d0bedb7..8f9f665acb 100644 --- a/Userland/Libraries/LibAudio/CMakeLists.txt +++ b/Userland/Libraries/LibAudio/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES QOALoader.cpp QOATypes.cpp UserSampleQueue.cpp + VorbisComment.cpp ) if (SERENITYOS) @@ -20,4 +21,4 @@ if (SERENITYOS) endif() serenity_lib(LibAudio audio) -target_link_libraries(LibAudio PRIVATE LibCore LibIPC LibThreading) +target_link_libraries(LibAudio PRIVATE LibCore LibIPC LibThreading LibUnicode) diff --git a/Userland/Libraries/LibAudio/FlacLoader.cpp b/Userland/Libraries/LibAudio/FlacLoader.cpp index 35206b949c..1f44634fdb 100644 --- a/Userland/Libraries/LibAudio/FlacLoader.cpp +++ b/Userland/Libraries/LibAudio/FlacLoader.cpp @@ -21,6 +21,7 @@ #include <LibAudio/FlacTypes.h> #include <LibAudio/LoaderError.h> #include <LibAudio/Resampler.h> +#include <LibAudio/VorbisComment.h> #include <LibCore/File.h> namespace Audio { @@ -131,6 +132,10 @@ MaybeLoaderError FlacLoaderPlugin::parse_header() [[fallthrough]]; case FlacMetadataBlockType::PADDING: // Note: A padding block is empty and does not need any treatment. + break; + case FlacMetadataBlockType::VORBIS_COMMENT: + load_vorbis_comment(block); + break; default: // TODO: Parse the remaining metadata block types. break; @@ -180,6 +185,17 @@ MaybeLoaderError FlacLoaderPlugin::load_picture(FlacRawMetadataBlock& block) return {}; } +// 11.15. METADATA_BLOCK_VORBIS_COMMENT +void FlacLoaderPlugin::load_vorbis_comment(FlacRawMetadataBlock& block) +{ + auto metadata_or_error = Audio::load_vorbis_comment(block.data); + if (metadata_or_error.is_error()) { + dbgln("FLAC Warning: Vorbis comment invalid, error: {}", metadata_or_error.release_error()); + return; + } + m_metadata = metadata_or_error.release_value(); +} + // 11.13. METADATA_BLOCK_SEEKTABLE MaybeLoaderError FlacLoaderPlugin::load_seektable(FlacRawMetadataBlock& block) { diff --git a/Userland/Libraries/LibAudio/FlacLoader.h b/Userland/Libraries/LibAudio/FlacLoader.h index 043ab58435..3bac59af78 100644 --- a/Userland/Libraries/LibAudio/FlacLoader.h +++ b/Userland/Libraries/LibAudio/FlacLoader.h @@ -78,6 +78,8 @@ private: // decode a single rice partition that has its own rice parameter ALWAYS_INLINE ErrorOr<Vector<i32>, LoaderError> decode_rice_partition(u8 partition_type, u32 partitions, u32 partition_index, FlacSubframeHeader& subframe, BigEndianInputBitStream& bit_input); MaybeLoaderError load_seektable(FlacRawMetadataBlock&); + // Note that failing to read a Vorbis comment block is not treated as an error of the FLAC loader, since metadata is optional. + void load_vorbis_comment(FlacRawMetadataBlock&); MaybeLoaderError load_picture(FlacRawMetadataBlock&); // Converters for special coding used in frame headers diff --git a/Userland/Libraries/LibAudio/VorbisComment.cpp b/Userland/Libraries/LibAudio/VorbisComment.cpp new file mode 100644 index 0000000000..78e211d9e4 --- /dev/null +++ b/Userland/Libraries/LibAudio/VorbisComment.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "VorbisComment.h" +#include <AK/Endian.h> +#include <AK/MemoryStream.h> +#include <AK/Span.h> +#include <AK/String.h> + +namespace Audio { + +// "Content vector format" +static ErrorOr<void> read_vorbis_field(Metadata& metadata_to_write_into, String const& unparsed_user_comment) +{ + // Technically the field name has to be ASCII, but we just accept all UTF-8. + auto field_name_and_contents = TRY(unparsed_user_comment.split_limit('=', 2)); + + if (field_name_and_contents.size() != 2) + return Error::from_string_view("User comment does not contain '='"sv); + auto contents = field_name_and_contents.take_last(); + auto field_name = TRY(field_name_and_contents.take_first().to_uppercase()); + + // Some of these are taken from https://age.hobba.nl/audio/tag_frame_reference.html + if (field_name == "TITLE"sv) { + if (metadata_to_write_into.title.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.title = contents; + } else if (field_name == "VERSION"sv) { + if (metadata_to_write_into.subtitle.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.subtitle = contents; + } else if (field_name == "ALBUM"sv) { + if (metadata_to_write_into.album.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.album = contents; + } else if (field_name == "COPYRIGHT"sv) { + if (metadata_to_write_into.copyright.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.copyright = contents; + } else if (field_name == "ISRC"sv) { + if (metadata_to_write_into.isrc.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.isrc = contents; + } else if (field_name == "GENRE"sv) { + if (metadata_to_write_into.genre.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.genre = contents; + } else if (field_name == "COMMENT"sv) { + if (metadata_to_write_into.comment.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.comment = contents; + } else if (field_name == "TRACKNUMBER"sv) { + if (metadata_to_write_into.track_number.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else if (auto maybe_number = contents.to_number<unsigned>(); maybe_number.has_value()) + metadata_to_write_into.track_number = maybe_number.release_value(); + else + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + } else if (field_name == "DATE"sv) { + if (metadata_to_write_into.unparsed_time.has_value()) + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + else + metadata_to_write_into.unparsed_time = contents; + } else if (field_name == "PERFORMER"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Performer, contents)); + } else if (field_name == "ARTIST"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Artist, contents)); + } else if (field_name == "COMPOSER"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Composer, contents)); + } else if (field_name == "CONDUCTOR"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Conductor, contents)); + } else if (field_name == "LYRICIST"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Lyricist, contents)); + } else if (field_name == "ORGANIZATION"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Publisher, contents)); + } else if (field_name == "PUBLISHER"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Publisher, contents)); + } else if (field_name == "ENCODED-BY"sv) { + TRY(metadata_to_write_into.add_person(Person::Role::Engineer, contents)); + } else { + TRY(metadata_to_write_into.add_miscellaneous(field_name, contents)); + } + + return {}; +} + +ErrorOr<Metadata, LoaderError> load_vorbis_comment(ByteBuffer const& vorbis_comment) +{ + FixedMemoryStream stream { vorbis_comment }; + auto vendor_length = TRY(stream.read_value<LittleEndian<u32>>()); + Vector<u8> raw_vendor_string; + TRY(raw_vendor_string.try_resize(vendor_length)); + TRY(stream.read_entire_buffer(raw_vendor_string)); + auto vendor_string = TRY(String::from_utf8(StringView { raw_vendor_string.span() })); + + Metadata metadata; + metadata.encoder = move(vendor_string); + + auto user_comment_count = TRY(stream.read_value<LittleEndian<u32>>()); + for (size_t i = 0; i < user_comment_count; ++i) { + auto user_comment_length = TRY(stream.read_value<LittleEndian<u32>>()); + Vector<u8> raw_user_comment; + TRY(raw_user_comment.try_resize(user_comment_length)); + TRY(stream.read_entire_buffer(raw_user_comment)); + auto unparsed_user_comment = TRY(String::from_utf8(StringView { raw_user_comment.span() })); + TRY(read_vorbis_field(metadata, unparsed_user_comment)); + } + + return metadata; +} + +} diff --git a/Userland/Libraries/LibAudio/VorbisComment.h b/Userland/Libraries/LibAudio/VorbisComment.h new file mode 100644 index 0000000000..f505dc315d --- /dev/null +++ b/Userland/Libraries/LibAudio/VorbisComment.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <LibAudio/LoaderError.h> +#include <LibAudio/Metadata.h> + +namespace Audio { + +// https://www.xiph.org/vorbis/doc/v-comment.html +ErrorOr<Metadata, LoaderError> load_vorbis_comment(ByteBuffer const& vorbis_comment); + +} |