LibRIFF: Rework to match LibGfx needs

There's now two namespaces, RIFF (little-endian) and IFF (big-endian)
which (for the most part) contain the same kinds of structures for
handling similar data in both formats. (They also share almost all of
their implementation) The main types are ChunkHeader and (Owned)Chunk.
While Chunk has no ownership over the data it accesses (and can only be
constructed from a byte view), OwnedChunk has ownership over this data
and is aimed at reading from streams.

OwnedList, implementing the standard RIFF LIST type, is currently only
implemented for RIFF due to its only user being WAV, but it may be
generalized in the future for use by IFF.

Co-authored-by: Timothy Flynn <trflynn89@pm.me>
This commit is contained in:
kleines Filmröllchen 2023-10-12 22:36:23 +02:00 committed by Andrew Kaster
parent d125d16287
commit 64598473cc
Notes: sideshowbarker 2024-07-17 07:08:37 +09:00
11 changed files with 366 additions and 145 deletions

View file

@ -185,16 +185,12 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
} \
} while (0)
auto riff = TRY(m_stream->read_value<RIFF::ChunkID>());
CHECK(riff == RIFF::riff_magic, LoaderError::Category::Format, "RIFF header magic invalid");
auto file_header = TRY(m_stream->read_value<RIFF::FileHeader>());
CHECK(file_header.magic() == RIFF::riff_magic, LoaderError::Category::Format, "RIFF header magic invalid");
CHECK(file_header.subformat == Wav::wave_subformat_id, LoaderError::Category::Format, "WAVE subformat id invalid");
TRY(m_stream->read_value<LittleEndian<u32>>()); // File size header
auto wave = TRY(m_stream->read_value<RIFF::ChunkID>());
CHECK(wave == Wav::wave_subformat_id, LoaderError::Category::Format, "WAVE subformat id invalid");
auto format_chunk = TRY(m_stream->read_value<RIFF::Chunk>());
CHECK(format_chunk.id.as_ascii_string() == Wav::format_chunk_id, LoaderError::Category::Format, "FMT chunk id invalid");
auto format_chunk = TRY(m_stream->read_value<RIFF::OwnedChunk>());
CHECK(format_chunk.id().as_ascii_string() == Wav::format_chunk_id, LoaderError::Category::Format, "FMT chunk id invalid");
auto format_stream = format_chunk.data_stream();
u16 audio_format = TRY(format_stream.read_value<LittleEndian<u16>>());
@ -212,7 +208,7 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
u16 bits_per_sample = TRY(format_stream.read_value<LittleEndian<u16>>());
if (audio_format == to_underlying(Wav::WaveFormat::Extensible)) {
CHECK(format_chunk.size == 40, LoaderError::Category::Format, "Extensible fmt size is not 40 bytes");
CHECK(format_chunk.size() == 40, LoaderError::Category::Format, "Extensible fmt size is not 40 bytes");
// Discard everything until the GUID.
// We've already read 16 bytes from the stream. The GUID starts in another 8 bytes.
@ -260,9 +256,9 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
found_data = true;
} else {
TRY(m_stream->seek(-RIFF::chunk_id_size, SeekMode::FromCurrentPosition));
auto chunk = TRY(m_stream->read_value<RIFF::Chunk>());
if (chunk.id == RIFF::list_chunk_id) {
auto maybe_list = chunk.data_stream().read_value<RIFF::List>();
auto chunk = TRY(m_stream->read_value<RIFF::OwnedChunk>());
if (chunk.id() == RIFF::list_chunk_id) {
auto maybe_list = chunk.data_stream().read_value<RIFF::OwnedList>();
if (maybe_list.is_error()) {
dbgln("WAV Warning: LIST chunk invalid, error: {}", maybe_list.release_error());
continue;
@ -278,7 +274,7 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
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);
dbgln_if(AWAVLOADER_DEBUG, "Unhandled WAV chunk of type {}, size {} bytes", chunk.id().as_ascii_string(), chunk.size());
}
}
}
@ -299,12 +295,12 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
// 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<RIFF::Chunk> info_chunks)
MaybeLoaderError WavLoaderPlugin::load_wav_info_block(Vector<RIFF::OwnedChunk> info_chunks)
{
for (auto const& chunk : info_chunks) {
auto metadata_name = chunk.id.as_ascii_string();
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) };
StringView metadata_text { chunk.data().trim(chunk.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) {

View file

@ -14,7 +14,7 @@
#include <AK/Span.h>
#include <AK/StringView.h>
#include <LibAudio/Loader.h>
#include <LibRIFF/Types.h>
#include <LibRIFF/RIFF.h>
namespace Audio {
@ -46,7 +46,7 @@ public:
private:
MaybeLoaderError parse_header();
MaybeLoaderError load_wav_info_block(Vector<RIFF::Chunk> info_chunks);
MaybeLoaderError load_wav_info_block(Vector<RIFF::OwnedChunk> info_chunks);
LoaderSamples samples_from_pcm_data(ReadonlyBytes data, size_t samples_to_read) const;
template<typename SampleReader>

View file

@ -1,5 +1,6 @@
set(SOURCES
Types.cpp
Decoding.cpp
Details.cpp
)
serenity_lib(LibRIFF riff)

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Array.h>
#include <AK/Endian.h>
#include <AK/Format.h>
#include <AK/StringView.h>
#include <AK/Types.h>
namespace RIFF {
static constexpr size_t const chunk_id_size = 4;
// Also referred to as "FourCC" (four character code) in the context of some formats.
struct ChunkID {
constexpr ChunkID(char const name[4])
{
id_data[0] = static_cast<u8>(name[0]);
id_data[1] = static_cast<u8>(name[1]);
id_data[2] = static_cast<u8>(name[2]);
id_data[3] = static_cast<u8>(name[3]);
}
constexpr ChunkID(Array<u8, chunk_id_size> data)
: id_data(data)
{
}
constexpr ChunkID(ChunkID const&) = default;
constexpr ChunkID(ChunkID&&) = default;
constexpr ChunkID& operator=(ChunkID const&) = default;
static constexpr ChunkID from_big_endian_number(u32 number) { return bit_cast<Array<u8, chunk_id_size>>(AK::convert_between_host_and_big_endian(number)); }
static ErrorOr<ChunkID> read_from_stream(Stream& stream);
StringView as_ascii_string() const;
constexpr u32 as_big_endian_number() const
{
return AK::convert_between_host_and_big_endian((id_data[0] << 24) | (id_data[1] << 16) | (id_data[2] << 8) | id_data[3]);
}
bool operator==(ChunkID const&) const = default;
bool operator==(StringView) const;
Array<u8, chunk_id_size> id_data;
};
static_assert(AssertSize<ChunkID, chunk_id_size>());
}
template<>
struct AK::Formatter<RIFF::ChunkID> : StandardFormatter {
ErrorOr<void> format(FormatBuilder& builder, RIFF::ChunkID const& chunk_id)
{
TRY(builder.put_padding('\'', 1));
TRY(builder.put_literal(chunk_id.as_ascii_string()));
TRY(builder.put_padding('\'', 1));
return {};
}
};

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
* Copyright (c) 2023, Nicolas Ramz <nicolas.ramz@gmail.com>
* Copyright (c) 2023, Nico Weber <thakis@chromium.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Stream.h>
#include <LibRIFF/ChunkID.h>
#include <LibRIFF/RIFF.h>
ErrorOr<RIFF::ChunkID> RIFF::ChunkID::read_from_stream(Stream& stream)
{
Array<u8, chunk_id_size> id;
TRY(stream.read_until_filled(id.span()));
return ChunkID { id };
}
ErrorOr<RIFF::OwnedList> RIFF::OwnedList::read_from_stream(Stream& stream)
{
auto type = TRY(stream.read_value<ChunkID>());
Vector<RIFF::OwnedChunk> chunks;
while (!stream.is_eof())
TRY(chunks.try_append(TRY(stream.read_value<RIFF::OwnedChunk>())));
return RIFF::OwnedList { .type = type, .chunks = move(chunks) };
}

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Details.h"
#include <AK/TypeCasts.h>
#include <LibRIFF/IFF.h>
#include <LibRIFF/RIFF.h>
namespace RIFF {
StringView ChunkID::as_ascii_string() const
{
return StringView { id_data.span() };
}
bool ChunkID::operator==(StringView other_string) const
{
return as_ascii_string() == other_string;
}
namespace Detail {
template<typename WordType>
auto ChunkHeader<WordType>::read_from_stream(Stream& stream) -> ErrorOr<ChunkHeader>
{
auto id = TRY(stream.read_value<RIFF::ChunkID>());
u32 size = TRY(stream.read_value<WordType>());
return ChunkHeader { id, size };
}
template<typename HeaderType>
auto FileHeader<HeaderType>::read_from_stream(Stream& stream) -> ErrorOr<FileHeader>
{
auto header = TRY(stream.read_value<HeaderType>());
auto subformat = TRY(stream.read_value<RIFF::ChunkID>());
return FileHeader { header, subformat };
}
template<typename HeaderType>
Chunk<HeaderType>::Chunk(HeaderType header, ReadonlyBytes data)
: m_header(header)
, m_data(data)
{
VERIFY(data.size() == header.size);
}
template<typename HeaderType>
FixedMemoryStream Chunk<HeaderType>::data_stream() const
{
return FixedMemoryStream { m_data };
}
template<typename HeaderType>
auto Chunk<HeaderType>::decode(ReadonlyBytes data) -> ErrorOr<Chunk>
{
auto data_stream = FixedMemoryStream { data };
auto header = TRY(HeaderType::read_from_stream(data_stream));
if (data.size() < sizeof(HeaderType) + header.size)
return Error::from_string_literal("Not enough data for IFF/RIFF chunk");
return Chunk { header, data.slice(sizeof(HeaderType), header.size) };
}
template<typename HeaderType>
auto Chunk<HeaderType>::decode_and_advance(ReadonlyBytes& data) -> ErrorOr<Chunk>
{
auto chunk = TRY(decode(data));
data = data.slice(sizeof(HeaderType) + chunk.size());
// add padding if needed
if (chunk.size() % 2 != 0) {
if (data.is_empty())
return Error::from_string_literal("Missing data for padding byte");
if (*data.data() != 0)
return Error::from_string_literal("Padding byte is not 0");
data = data.slice(1);
}
return chunk;
}
template<typename HeaderType>
OwnedChunk<HeaderType>::OwnedChunk(HeaderType header, Buffer backing_data)
: Chunk<HeaderType>(header, backing_data.span())
, m_backing_data(move(backing_data))
{
}
template<typename HeaderType>
auto OwnedChunk<HeaderType>::read_from_stream(Stream& stream) -> ErrorOr<OwnedChunk>
{
auto header = TRY(stream.read_value<HeaderType>());
auto data = TRY(Buffer::create_uninitialized(header.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<SeekableStream>(stream)) {
if (!stream.is_eof()) {
auto stream_position = TRY(static_cast<SeekableStream&>(stream).tell());
if (stream_position % 2 != 0)
TRY(static_cast<SeekableStream&>(stream).seek(1, SeekMode::FromCurrentPosition));
}
} else {
dbgln("RIFF Warning: Cannot align stream to 2-byte boundary, next chunk may be bogus!");
}
return OwnedChunk { header, data };
}
template class Chunk<IFF::ChunkHeader>;
template class Chunk<RIFF::ChunkHeader>;
template class OwnedChunk<IFF::ChunkHeader>;
template class OwnedChunk<RIFF::ChunkHeader>;
template struct ChunkHeader<IFF::WordType>;
template struct ChunkHeader<RIFF::WordType>;
template struct FileHeader<IFF::ChunkHeader>;
template struct FileHeader<RIFF::ChunkHeader>;
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/MemoryStream.h>
#include <LibRIFF/ChunkID.h>
// Despite the name, this header contains details for both RIFF and IFF
namespace RIFF::Detail {
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf page 11 (Chunks)
template<typename WordType>
struct ChunkHeader {
static ErrorOr<ChunkHeader> read_from_stream(Stream& stream);
RIFF::ChunkID id;
u32 size;
};
// Standard RIFF/IFF file formats use a global chunk with a chunk ID (magic bytes) such as "RIFF" or "FORM".
// A chunk ID right at the start of the global chunk specifies the subformat specific to the file type.
// Example for RIFF from WebP: https://developers.google.com/speed/webp/docs/riff_container#webp_file_header
template<typename HeaderType>
struct FileHeader {
HeaderType global_header;
RIFF::ChunkID subformat;
static ErrorOr<FileHeader> read_from_stream(Stream& stream);
constexpr ChunkID magic() const { return global_header.id; }
constexpr u32 file_size() const { return global_header.size; }
};
// An RIFF or IFF chunk.
template<typename HeaderType>
class Chunk {
public:
Chunk(HeaderType header, ReadonlyBytes data);
// Note that the resulting chunk will refer to the provided data.
static ErrorOr<Chunk> decode(ReadonlyBytes data);
static ErrorOr<Chunk> decode_and_advance(ReadonlyBytes& data);
RIFF::ChunkID id() const { return m_header.id; }
u32 size() const { return m_header.size; }
ReadonlyBytes data() const { return m_data; }
FixedMemoryStream data_stream() const;
u8 operator[](size_t index) const { return data()[index]; }
private:
HeaderType m_header;
ReadonlyBytes m_data;
};
// Owns the chunk data and can therefore be parsed from a stream.
template<typename HeaderType>
class OwnedChunk : public Chunk<HeaderType> {
public:
using Buffer = AK::Detail::ByteBuffer<0>;
OwnedChunk(HeaderType, Buffer);
static ErrorOr<OwnedChunk> read_from_stream(Stream& stream);
private:
Buffer m_backing_data;
};
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2018-2023, the SerenityOS developers.
* Copyright (c) 2023, Nico Weber <thakis@chromium.org>
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibRIFF/ChunkID.h>
#include <LibRIFF/Details.h>
// IFF chunks (as often used by Amiga, EA and more modern formats) use big-endian fields.
namespace IFF {
using WordType = BigEndian<u32>;
using ChunkHeader = RIFF::Detail::ChunkHeader<WordType>;
using FileHeader = RIFF::Detail::FileHeader<ChunkHeader>;
using Chunk = RIFF::Detail::Chunk<ChunkHeader>;
using OwnedChunk = RIFF::Detail::OwnedChunk<ChunkHeader>;
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2018-2023, the SerenityOS developers.
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/Vector.h>
#include <LibRIFF/ChunkID.h>
#include <LibRIFF/Details.h>
// RIFF chunks (as often used by Microsoft's older formats) use little-endian fields.
namespace RIFF {
static constexpr StringView const riff_magic = "RIFF"sv;
static constexpr StringView const list_chunk_id = "LIST"sv;
using WordType = LittleEndian<u32>;
using ChunkHeader = RIFF::Detail::ChunkHeader<WordType>;
using FileHeader = RIFF::Detail::FileHeader<ChunkHeader>;
using Chunk = RIFF::Detail::Chunk<ChunkHeader>;
using OwnedChunk = RIFF::Detail::OwnedChunk<ChunkHeader>;
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf page 23 (LIST type)
struct OwnedList {
static ErrorOr<OwnedList> read_from_stream(Stream& stream);
ChunkID type;
Vector<OwnedChunk> chunks;
};
}

View file

@ -1,76 +0,0 @@
/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Types.h"
#include <AK/Endian.h>
#include <AK/Stream.h>
#include <AK/Try.h>
#include <AK/TypeCasts.h>
namespace RIFF {
ErrorOr<ChunkID> ChunkID::read_from_stream(Stream& stream)
{
Array<u8, chunk_id_size> id;
TRY(stream.read_until_filled(id.span()));
return ChunkID { id };
}
ErrorOr<Chunk> Chunk::read_from_stream(Stream& stream)
{
auto id = TRY(stream.read_value<ChunkID>());
u32 size = TRY(stream.read_value<LittleEndian<u32>>());
auto data = TRY(FixedArray<u8>::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<SeekableStream>(stream)) {
if (!stream.is_eof()) {
auto stream_position = TRY(static_cast<SeekableStream&>(stream).tell());
if (stream_position % 2 != 0)
TRY(static_cast<SeekableStream&>(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,
move(data),
};
}
ErrorOr<List> List::read_from_stream(Stream& stream)
{
auto type = TRY(stream.read_value<ChunkID>());
Vector<Chunk> chunks;
while (!stream.is_eof())
TRY(chunks.try_append(TRY(stream.read_value<Chunk>())));
return List {
.type = type,
.chunks = move(chunks),
};
}
StringView ChunkID::as_ascii_string() const
{
return StringView { id_data.span() };
}
bool ChunkID::operator==(StringView const& other_string) const
{
return as_ascii_string() == other_string;
}
FixedMemoryStream Chunk::data_stream()
{
return FixedMemoryStream { data.span() };
}
}

View file

@ -1,49 +0,0 @@
/*
* Copyright (c) 2018-2023, the SerenityOS developers.
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FixedArray.h>
#include <AK/Forward.h>
#include <AK/MemoryStream.h>
#include <AK/Types.h>
namespace RIFF {
static constexpr StringView const riff_magic = "RIFF"sv;
static constexpr StringView const list_chunk_id = "LIST"sv;
static constexpr size_t const chunk_id_size = 4;
struct ChunkID {
static ErrorOr<ChunkID> read_from_stream(Stream& stream);
StringView as_ascii_string() const;
bool operator==(ChunkID const&) const = default;
bool operator==(StringView const&) const;
Array<u8, chunk_id_size> id_data;
};
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf page 11 (Chunks)
struct Chunk {
static ErrorOr<Chunk> read_from_stream(Stream& stream);
FixedMemoryStream data_stream();
ChunkID id;
u32 size;
FixedArray<u8> data;
};
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf page 23 (LIST type)
struct List {
static ErrorOr<List> read_from_stream(Stream& stream);
ChunkID type;
Vector<Chunk> chunks;
};
}