From d8e8ddedf37e16b41186c2b8dd0b247e1676c4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Tue, 7 Mar 2023 16:50:32 +0100 Subject: LibAudio: Add a generic audio metadata container This container has several design goals: - Represent all common and relevant metadata fields of audio files in a unified way. - Allow perfect recreation of any metadata format from the in-memory structure. This requires that we allow non-detected fields to reside in an "untyped" miscellaneous collection. Like with pictures, plugins are free to store their metadata into the m_metadata field whenever they read it. It is recommended that this happens on loader creation; however failing to read metadata should not cause an error in the plugin. --- Userland/Libraries/LibAudio/CMakeLists.txt | 1 + Userland/Libraries/LibAudio/Loader.h | 4 ++ Userland/Libraries/LibAudio/Metadata.cpp | 94 ++++++++++++++++++++++++++++++ Userland/Libraries/LibAudio/Metadata.h | 69 ++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 Userland/Libraries/LibAudio/Metadata.cpp create mode 100644 Userland/Libraries/LibAudio/Metadata.h (limited to 'Userland') diff --git a/Userland/Libraries/LibAudio/CMakeLists.txt b/Userland/Libraries/LibAudio/CMakeLists.txt index cd13d42af7..b77d0bedb7 100644 --- a/Userland/Libraries/LibAudio/CMakeLists.txt +++ b/Userland/Libraries/LibAudio/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES WavLoader.cpp FlacLoader.cpp WavWriter.cpp + Metadata.cpp MP3Loader.cpp QOALoader.cpp QOATypes.cpp diff --git a/Userland/Libraries/LibAudio/Loader.h b/Userland/Libraries/LibAudio/Loader.h index 1b247c244c..37bb668a5f 100644 --- a/Userland/Libraries/LibAudio/Loader.h +++ b/Userland/Libraries/LibAudio/Loader.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -67,12 +68,14 @@ public: virtual DeprecatedString format_name() = 0; virtual PcmSampleFormat pcm_format() = 0; + Metadata const& metadata() const { return m_metadata; } Vector const& pictures() const { return m_pictures; }; protected: NonnullOwnPtr m_stream; Vector m_pictures; + Metadata m_metadata; }; class Loader : public RefCounted { @@ -96,6 +99,7 @@ public: u16 num_channels() const { return m_plugin->num_channels(); } DeprecatedString format_name() const { return m_plugin->format_name(); } u16 bits_per_sample() const { return pcm_bits_per_sample(m_plugin->pcm_format()); } + Metadata const& metadata() const { return m_plugin->metadata(); } Vector const& pictures() const { return m_plugin->pictures(); }; private: diff --git a/Userland/Libraries/LibAudio/Metadata.cpp b/Userland/Libraries/LibAudio/Metadata.cpp new file mode 100644 index 0000000000..394760a4d2 --- /dev/null +++ b/Userland/Libraries/LibAudio/Metadata.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Metadata.h" +#include +#include + +namespace Audio { + +bool Person::is_artist() const +{ + return role == Person::Role::Artist + || role == Person::Role::Composer + || role == Person::Role::Conductor + || role == Person::Role::Lyricist + || role == Person::Role::Performer; +} + +Optional Person::name_for_role() const +{ + switch (role) { + case Role::Artist: + case Role::Performer: + return {}; + case Role::Lyricist: + return "Lyricist"sv; + case Role::Conductor: + return "Conductor"sv; + case Role::Publisher: + return "Publisher"sv; + case Role::Engineer: + return "Engineer"sv; + case Role::Composer: + return "Composer"sv; + } + VERIFY_NOT_REACHED(); +} + +void Metadata::replace_encoder_with_serenity() +{ + auto version_or_error = Core::Version::read_long_version_string(); + // Unset the encoder field in this case; we definitely want to replace the existing encoder field. + if (version_or_error.is_error()) + encoder = {}; + auto encoder_string = String::formatted("SerenityOS LibAudio {}", version_or_error.release_value()); + if (encoder_string.is_error()) + encoder = {}; + encoder = encoder_string.release_value(); +} + +Optional Metadata::first_artist() const +{ + auto artist = people.find_if([](auto const& person) { return person.is_artist(); }); + if (artist.is_end()) + return {}; + return artist->name; +} + +ErrorOr> Metadata::all_artists(StringView concatenate_with) const +{ + // FIXME: This entire function could be similar to TRY(TRY(people.filter(...).try_map(...)).join(concatenate_with)) if these functional iterator transformers existed :^) + Vector artist_texts; + TRY(artist_texts.try_ensure_capacity(people.size())); + for (auto const& person : people) { + if (!person.is_artist()) + continue; + if (auto role_name = person.name_for_role(); role_name.has_value()) + artist_texts.unchecked_append(TRY(String::formatted("{} ({})", person.name, role_name.release_value()))); + else + artist_texts.unchecked_append(person.name); + } + if (artist_texts.is_empty()) + return Optional {}; + return String::join(concatenate_with, artist_texts); +} + +ErrorOr Metadata::add_miscellaneous(String const& field, String value) +{ + // FIXME: Since try_ensure does not return a reference to the contained value, we have to retrieve it separately. + // This is a try_ensure bug that should be fixed. + (void)TRY(miscellaneous.try_ensure(field, []() { return Vector {}; })); + auto& values_for_field = miscellaneous.get(field).release_value(); + return values_for_field.try_append(move(value)); +} + +ErrorOr Metadata::add_person(Person::Role role, String name) +{ + return people.try_append(Person { role, move(name) }); +} + +} diff --git a/Userland/Libraries/LibAudio/Metadata.h b/Userland/Libraries/LibAudio/Metadata.h new file mode 100644 index 0000000000..78de65211e --- /dev/null +++ b/Userland/Libraries/LibAudio/Metadata.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Audio { + +struct Person { + enum class Role { + Artist, + Performer, + Lyricist, + Conductor, + Publisher, + Engineer, + Composer, + }; + Role role; + String name; + + // Whether this person has creative involvement with the song (so not only Role::Artist!). + // This list is subjective and is intended to keep the artist display text in applications relevant. + // It is used for first_artist and all_artists in Metadata. + bool is_artist() const; + + Optional name_for_role() const; +}; + +// Audio metadata of the original format must be equivalently reconstructible from this struct. +// That means, (if the format allows it) fields can appear in a different order, but all fields must be present with the original values, +// including duplicate fields where allowed by the format. +struct Metadata { + using Year = unsigned; + + void replace_encoder_with_serenity(); + ErrorOr add_miscellaneous(String const& field, String value); + ErrorOr add_person(Person::Role role, String name); + Optional first_artist() const; + ErrorOr> all_artists(StringView concatenate_with = ", "sv) const; + + Optional title; + Optional subtitle; + Optional track_number; + Optional album; + Optional genre; + Optional comment; + Optional isrc; + Optional encoder; + Optional copyright; + Optional bpm; + // FIXME: Until the time data structure situation is solved in a good way, we don't parse ISO 8601 time specifications. + Optional unparsed_time; + Vector people; + + // Any other metadata, using the format-specific field names. This ensures reproducibility. + HashMap> miscellaneous; +}; + +} -- cgit v1.2.3