From 9e496a96a62f9f6b660421a9c2e432bf974aeec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Wed, 15 Mar 2023 23:47:50 +0100 Subject: [PATCH] LibAudio: Load WAV metadata This is quite straightforward with our new RIFF infrastructure. --- Userland/Libraries/LibAudio/RIFFTypes.cpp | 25 ++++++++++ Userland/Libraries/LibAudio/RIFFTypes.h | 8 +++ Userland/Libraries/LibAudio/WavLoader.cpp | 59 ++++++++++++++++++++++- Userland/Libraries/LibAudio/WavLoader.h | 2 + 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/Userland/Libraries/LibAudio/RIFFTypes.cpp b/Userland/Libraries/LibAudio/RIFFTypes.cpp index e392c34b74..b1cf1daf95 100644 --- a/Userland/Libraries/LibAudio/RIFFTypes.cpp +++ b/Userland/Libraries/LibAudio/RIFFTypes.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace Audio::RIFF { @@ -26,6 +27,17 @@ ErrorOr Chunk::read_from_stream(Stream& stream) auto data = TRY(FixedArray::create(size)); TRY(stream.read_until_filled(data.span())); + // RIFF chunks may have trailing padding to align to x86 "words" (i.e. 2 bytes). + if (is(stream)) { + if (!stream.is_eof()) { + auto stream_position = TRY(static_cast(stream).tell()); + if (stream_position % 2 != 0) + TRY(static_cast(stream).seek(1, SeekMode::FromCurrentPosition)); + } + } else { + dbgln("RIFF Warning: Cannot align stream to 2-byte boundary, next chunk may be bogus!"); + } + return Chunk { id, size, @@ -33,6 +45,19 @@ ErrorOr Chunk::read_from_stream(Stream& stream) }; } +ErrorOr List::read_from_stream(Stream& stream) +{ + auto type = TRY(stream.read_value()); + Vector chunks; + while (!stream.is_eof()) + TRY(chunks.try_append(TRY(stream.read_value()))); + + return List { + .type = type, + .chunks = move(chunks), + }; +} + StringView ChunkID::as_ascii_string() const { return StringView { id_data.span() }; diff --git a/Userland/Libraries/LibAudio/RIFFTypes.h b/Userland/Libraries/LibAudio/RIFFTypes.h index 8c17ab7e8b..e2c54b37e8 100644 --- a/Userland/Libraries/LibAudio/RIFFTypes.h +++ b/Userland/Libraries/LibAudio/RIFFTypes.h @@ -52,4 +52,12 @@ struct Chunk { FixedArray data; }; +// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf page 23 (LIST type) +struct List { + static ErrorOr read_from_stream(Stream& stream); + + ChunkID type; + Vector chunks; +}; + } diff --git a/Userland/Libraries/LibAudio/WavLoader.cpp b/Userland/Libraries/LibAudio/WavLoader.cpp index 9eb3784079..66b1de26c8 100644 --- a/Userland/Libraries/LibAudio/WavLoader.cpp +++ b/Userland/Libraries/LibAudio/WavLoader.cpp @@ -268,8 +268,25 @@ MaybeLoaderError WavLoaderPlugin::parse_header() } else { TRY(m_stream->seek(-RIFF::chunk_id_size, SeekMode::FromCurrentPosition)); auto chunk = TRY(m_stream->read_value()); - dbgln_if(AWAVLOADER_DEBUG, "Unhandled WAV chunk of type {}, size {} bytes", chunk.id.as_ascii_string(), chunk.size); - // TODO: Handle LIST INFO chunks. + if (chunk.id == RIFF::list_chunk_id) { + auto maybe_list = chunk.data_stream().read_value(); + if (maybe_list.is_error()) { + dbgln("WAV Warning: LIST chunk invalid, error: {}", maybe_list.release_error()); + continue; + } + + auto list = maybe_list.release_value(); + if (list.type == RIFF::info_chunk_id) { + auto maybe_error = load_wav_info_block(move(list.chunks)); + if (maybe_error.is_error()) + dbgln("WAV Warning: INFO chunk invalid, error: {}", maybe_error.release_error()); + + } else { + dbgln("Unhandled WAV list of type {} with {} subchunks", list.type.as_ascii_string(), list.chunks.size()); + } + } else { + dbgln_if(AWAVLOADER_DEBUG, "Unhandled WAV chunk of type {}, size {} bytes", chunk.id.as_ascii_string(), chunk.size); + } } } @@ -288,4 +305,42 @@ MaybeLoaderError WavLoaderPlugin::parse_header() return {}; } +// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf page 23 (LIST type) +// We only recognize the relevant official metadata types; types added in later errata of RIFF are not relevant for audio. +MaybeLoaderError WavLoaderPlugin::load_wav_info_block(Vector info_chunks) +{ + for (auto const& chunk : info_chunks) { + auto metadata_name = chunk.id.as_ascii_string(); + // Chunk contents are zero-terminated strings "ZSTR", so we just drop the null terminator. + StringView metadata_text { chunk.data.span().trim(chunk.data.size() - 1) }; + // Note that we assume chunks to be unique, since that seems to almost always be the case. + // Worst case we just drop some metadata. + if (metadata_name == "IART"sv) { + // Artists are combined together with semicolons, at least when you edit them in Windows File Explorer. + auto artists = metadata_text.split_view(";"sv); + for (auto artist : artists) + TRY(m_metadata.add_person(Person::Role::Artist, TRY(String::from_utf8(artist)))); + } else if (metadata_name == "ICMT"sv) { + m_metadata.comment = TRY(String::from_utf8(metadata_text)); + } else if (metadata_name == "ICOP"sv) { + m_metadata.copyright = TRY(String::from_utf8(metadata_text)); + } else if (metadata_name == "ICRD"sv) { + m_metadata.unparsed_time = TRY(String::from_utf8(metadata_text)); + } else if (metadata_name == "IENG"sv) { + TRY(m_metadata.add_person(Person::Role::Engineer, TRY(String::from_utf8(metadata_text)))); + } else if (metadata_name == "IGNR"sv) { + m_metadata.genre = TRY(String::from_utf8(metadata_text)); + } else if (metadata_name == "INAM"sv) { + m_metadata.title = TRY(String::from_utf8(metadata_text)); + } else if (metadata_name == "ISFT"sv) { + m_metadata.encoder = TRY(String::from_utf8(metadata_text)); + } else if (metadata_name == "ISRC"sv) { + TRY(m_metadata.add_person(Person::Role::Publisher, TRY(String::from_utf8(metadata_text)))); + } else { + TRY(m_metadata.add_miscellaneous(TRY(String::from_utf8(metadata_name)), TRY(String::from_utf8(metadata_text)))); + } + } + return {}; +} + } diff --git a/Userland/Libraries/LibAudio/WavLoader.h b/Userland/Libraries/LibAudio/WavLoader.h index e5c48428e4..b920ce9da7 100644 --- a/Userland/Libraries/LibAudio/WavLoader.h +++ b/Userland/Libraries/LibAudio/WavLoader.h @@ -14,6 +14,7 @@ #include #include #include +#include namespace Audio { @@ -46,6 +47,7 @@ private: MaybeLoaderError initialize(); MaybeLoaderError parse_header(); + MaybeLoaderError load_wav_info_block(Vector info_chunks); LoaderSamples samples_from_pcm_data(Bytes const& data, size_t samples_to_read) const; template