Jelajahi Sumber

LibAudio: Modernize WAV loader

With this, the WAV loader is a completely modern LibAudio loader:
- Own type header for RIFF data structures
- custom stream read functions for the types
- Final removal of legacy I/O error checking
- clearer error messages
- clean handling of header chunks

The latter will allow proper handling of other chunks (before "data") in
the future, such as metadata :^)
kleines Filmröllchen 2 tahun lalu
induk
melakukan
70970b2fa9

+ 1 - 0
Userland/Libraries/LibAudio/CMakeLists.txt

@@ -2,6 +2,7 @@ set(SOURCES
     GenericTypes.cpp
     GenericTypes.cpp
     SampleFormats.cpp
     SampleFormats.cpp
     Loader.cpp
     Loader.cpp
+    RIFFTypes.cpp
     WavLoader.cpp
     WavLoader.cpp
     FlacLoader.cpp
     FlacLoader.cpp
     WavWriter.cpp
     WavWriter.cpp

+ 51 - 0
Userland/Libraries/LibAudio/RIFFTypes.cpp

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "RIFFTypes.h"
+#include <AK/Endian.h>
+#include <AK/Stream.h>
+#include <AK/Try.h>
+
+namespace Audio::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()));
+
+    return Chunk {
+        id,
+        size,
+        move(data),
+    };
+}
+
+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() };
+}
+
+}

+ 55 - 0
Userland/Libraries/LibAudio/RIFFTypes.h

@@ -0,0 +1,55 @@
+/*
+ * 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>
+
+// RIFF-specific type definitions necessary for handling WAVE files.
+namespace Audio::RIFF {
+
+static constexpr StringView const riff_magic = "RIFF"sv;
+static constexpr StringView const wave_subformat_id = "WAVE"sv;
+static constexpr StringView const data_chunk_id = "data"sv;
+static constexpr StringView const list_chunk_id = "LIST"sv;
+static constexpr StringView const info_chunk_id = "INFO"sv;
+static constexpr StringView const format_chunk_id = "fmt "sv;
+
+// Constants for handling WAVE header data.
+enum class WaveFormat : u32 {
+    Pcm = 0x0001,        // WAVE_FORMAT_PCM
+    IEEEFloat = 0x0003,  // WAVE_FORMAT_IEEE_FLOAT
+    ALaw = 0x0006,       // 8-bit ITU-T G.711 A-law
+    MuLaw = 0x0007,      // 8-bit ITU-T G.711 µ-law
+    Extensible = 0xFFFE, // Determined by SubFormat
+};
+
+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;
+};
+
+}

+ 52 - 104
Userland/Libraries/LibAudio/WavLoader.cpp

@@ -1,12 +1,13 @@
 /*
 /*
  * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
- * Copyright (c) 2021, kleines Filmröllchen <filmroellchen@serenityos.org>
+ * Copyright (c) 2021-2023, kleines Filmröllchen <filmroellchen@serenityos.org>
  *
  *
  * SPDX-License-Identifier: BSD-2-Clause
  * SPDX-License-Identifier: BSD-2-Clause
  */
  */
 
 
 #include "WavLoader.h"
 #include "WavLoader.h"
 #include "LoaderError.h"
 #include "LoaderError.h"
+#include "RIFFTypes.h"
 #include <AK/Debug.h>
 #include <AK/Debug.h>
 #include <AK/Endian.h>
 #include <AK/Endian.h>
 #include <AK/FixedArray.h>
 #include <AK/FixedArray.h>
@@ -183,95 +184,56 @@ MaybeLoaderError WavLoaderPlugin::seek(int sample_index)
 // Specification reference: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
 // Specification reference: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
 MaybeLoaderError WavLoaderPlugin::parse_header()
 MaybeLoaderError WavLoaderPlugin::parse_header()
 {
 {
-    bool ok = true;
-    size_t bytes_read = 0;
-
-    auto read_u8 = [&]() -> ErrorOr<u8, LoaderError> {
-        u8 value = LOADER_TRY(m_stream->read_value<LittleEndian<u8>>());
-        bytes_read += 1;
-        return value;
-    };
-
-    auto read_u16 = [&]() -> ErrorOr<u16, LoaderError> {
-        u16 value = LOADER_TRY(m_stream->read_value<LittleEndian<u16>>());
-        bytes_read += 2;
-        return value;
-    };
-
-    auto read_u32 = [&]() -> ErrorOr<u32, LoaderError> {
-        u32 value = LOADER_TRY(m_stream->read_value<LittleEndian<u32>>());
-        bytes_read += 4;
-        return value;
-    };
-
-#define CHECK_OK(category, msg)                                                                      \
-    do {                                                                                             \
-        if (!ok)                                                                                     \
-            return LoaderError { category, DeprecatedString::formatted("Parsing failed: {}", msg) }; \
+#define CHECK(check, category, msg)                                                                                                                 \
+    do {                                                                                                                                            \
+        if (!(check)) {                                                                                                                             \
+            return LoaderError { category, static_cast<size_t>(LOADER_TRY(m_stream->tell())), DeprecatedString::formatted("WAV header: {}", msg) }; \
+        }                                                                                                                                           \
     } while (0)
     } while (0)
 
 
-    u32 riff = TRY(read_u32());
-    ok = ok && riff == 0x46464952; // "RIFF"
-    CHECK_OK(LoaderError::Category::Format, "RIFF header");
+    auto riff = TRY(m_stream->read_value<RIFF::ChunkID>());
+    CHECK(riff == RIFF::riff_magic, LoaderError::Category::Format, "RIFF header magic invalid");
 
 
-    u32 sz = TRY(read_u32());
-    ok = ok && sz < maximum_wav_size;
-    CHECK_OK(LoaderError::Category::Format, "File size");
+    u32 size = TRY(m_stream->read_value<LittleEndian<u32>>());
+    CHECK(size < maximum_wav_size, LoaderError::Category::Format, "File size too large");
 
 
-    u32 wave = TRY(read_u32());
-    ok = ok && wave == 0x45564157; // "WAVE"
-    CHECK_OK(LoaderError::Category::Format, "WAVE header");
+    auto wave = TRY(m_stream->read_value<RIFF::ChunkID>());
+    CHECK(wave == RIFF::wave_subformat_id, LoaderError::Category::Format, "WAVE subformat id invalid");
 
 
-    u32 fmt_id = TRY(read_u32());
-    ok = ok && fmt_id == 0x20746D66; // "fmt "
-    CHECK_OK(LoaderError::Category::Format, "FMT header");
+    auto format_chunk = TRY(m_stream->read_value<RIFF::Chunk>());
+    CHECK(format_chunk.id.as_ascii_string() == RIFF::format_chunk_id, LoaderError::Category::Format, "FMT chunk id invalid");
 
 
-    u32 fmt_size = TRY(read_u32());
-    ok = ok && (fmt_size == 16 || fmt_size == 18 || fmt_size == 40);
-    CHECK_OK(LoaderError::Category::Format, "FMT size");
+    auto format_stream = format_chunk.data_stream();
+    u16 audio_format = TRY(format_stream.read_value<LittleEndian<u16>>());
+    CHECK(audio_format == to_underlying(RIFF::WaveFormat::Pcm) || audio_format == to_underlying(RIFF::WaveFormat::IEEEFloat) || audio_format == to_underlying(RIFF::WaveFormat::Extensible),
+        LoaderError::Category::Unimplemented, "Audio format not supported");
 
 
-    u16 audio_format = TRY(read_u16());
-    CHECK_OK(LoaderError::Category::Format, "Audio format"); // incomplete read check
-    ok = ok && (audio_format == WAVE_FORMAT_PCM || audio_format == WAVE_FORMAT_IEEE_FLOAT || audio_format == WAVE_FORMAT_EXTENSIBLE);
-    CHECK_OK(LoaderError::Category::Unimplemented, "Audio format PCM/Float"); // value check
+    m_num_channels = TRY(format_stream.read_value<LittleEndian<u16>>());
+    CHECK(m_num_channels == 1 || m_num_channels == 2, LoaderError::Category::Unimplemented, "Channel count");
 
 
-    m_num_channels = TRY(read_u16());
-    ok = ok && (m_num_channels == 1 || m_num_channels == 2);
-    CHECK_OK(LoaderError::Category::Unimplemented, "Channel count");
+    m_sample_rate = TRY(format_stream.read_value<LittleEndian<u32>>());
+    // Data rate; can be ignored.
+    TRY(format_stream.read_value<LittleEndian<u32>>());
+    u16 block_size_bytes = TRY(format_stream.read_value<LittleEndian<u16>>());
 
 
-    m_sample_rate = TRY(read_u32());
-    CHECK_OK(LoaderError::Category::IO, "Sample rate");
+    u16 bits_per_sample = TRY(format_stream.read_value<LittleEndian<u16>>());
 
 
-    TRY(read_u32());
-    CHECK_OK(LoaderError::Category::IO, "Data rate");
-
-    u16 block_size_bytes = TRY(read_u16());
-    CHECK_OK(LoaderError::Category::IO, "Block size");
-
-    u16 bits_per_sample = TRY(read_u16());
-    CHECK_OK(LoaderError::Category::IO, "Bits per sample");
-
-    if (audio_format == WAVE_FORMAT_EXTENSIBLE) {
-        ok = ok && (fmt_size == 40);
-        CHECK_OK(LoaderError::Category::Format, "Extensible fmt size"); // value check
+    if (audio_format == to_underlying(RIFF::WaveFormat::Extensible)) {
+        CHECK(format_chunk.size == 40, LoaderError::Category::Format, "Extensible fmt size is not 40 bytes");
 
 
         // Discard everything until the GUID.
         // Discard everything until the GUID.
         // We've already read 16 bytes from the stream. The GUID starts in another 8 bytes.
         // We've already read 16 bytes from the stream. The GUID starts in another 8 bytes.
-        TRY(read_u32());
-        TRY(read_u32());
-        CHECK_OK(LoaderError::Category::IO, "Discard until GUID");
+        TRY(format_stream.read_value<LittleEndian<u64>>());
 
 
         // Get the underlying audio format from the first two bytes of GUID
         // Get the underlying audio format from the first two bytes of GUID
-        u16 guid_subformat = TRY(read_u16());
-        ok = ok && (guid_subformat == WAVE_FORMAT_PCM || guid_subformat == WAVE_FORMAT_IEEE_FLOAT);
-        CHECK_OK(LoaderError::Category::Unimplemented, "GUID SubFormat");
+        u16 guid_subformat = TRY(format_stream.read_value<LittleEndian<u16>>());
+        CHECK(guid_subformat == to_underlying(RIFF::WaveFormat::Pcm) || guid_subformat == to_underlying(RIFF::WaveFormat::IEEEFloat), LoaderError::Category::Unimplemented, "GUID SubFormat not supported");
 
 
         audio_format = guid_subformat;
         audio_format = guid_subformat;
     }
     }
 
 
-    if (audio_format == WAVE_FORMAT_PCM) {
-        ok = ok && (bits_per_sample == 8 || bits_per_sample == 16 || bits_per_sample == 24);
-        CHECK_OK(LoaderError::Category::Unimplemented, "Bits per sample (PCM)"); // value check
+    if (audio_format == to_underlying(RIFF::WaveFormat::Pcm)) {
+        CHECK(bits_per_sample == 8 || bits_per_sample == 16 || bits_per_sample == 24, LoaderError::Category::Unimplemented, "PCM bits per sample not supported");
 
 
         // We only support 8-24 bit audio right now because other formats are uncommon
         // We only support 8-24 bit audio right now because other formats are uncommon
         if (bits_per_sample == 8) {
         if (bits_per_sample == 8) {
@@ -281,9 +243,8 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
         } else if (bits_per_sample == 24) {
         } else if (bits_per_sample == 24) {
             m_sample_format = PcmSampleFormat::Int24;
             m_sample_format = PcmSampleFormat::Int24;
         }
         }
-    } else if (audio_format == WAVE_FORMAT_IEEE_FLOAT) {
-        ok = ok && (bits_per_sample == 32 || bits_per_sample == 64);
-        CHECK_OK(LoaderError::Category::Unimplemented, "Bits per sample (Float)"); // value check
+    } else if (audio_format == to_underlying(RIFF::WaveFormat::IEEEFloat)) {
+        CHECK(bits_per_sample == 32 || bits_per_sample == 64, LoaderError::Category::Unimplemented, "Float bits per sample not supported");
 
 
         // Again, only the common 32 and 64 bit
         // Again, only the common 32 and 64 bit
         if (bits_per_sample == 32) {
         if (bits_per_sample == 32) {
@@ -293,42 +254,28 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
         }
         }
     }
     }
 
 
-    ok = ok && (block_size_bytes == (m_num_channels * (bits_per_sample / 8)));
-    CHECK_OK(LoaderError::Category::Format, "Block size sanity check");
+    CHECK(block_size_bytes == (m_num_channels * (bits_per_sample / 8)), LoaderError::Category::Format, "Block size invalid");
 
 
     dbgln_if(AWAVLOADER_DEBUG, "WAV format {} at {} bit, {} channels, rate {}Hz ",
     dbgln_if(AWAVLOADER_DEBUG, "WAV format {} at {} bit, {} channels, rate {}Hz ",
         sample_format_name(m_sample_format), pcm_bits_per_sample(m_sample_format), m_num_channels, m_sample_rate);
         sample_format_name(m_sample_format), pcm_bits_per_sample(m_sample_format), m_num_channels, m_sample_rate);
 
 
-    // Read chunks until we find DATA
+    // Read all chunks before DATA.
     bool found_data = false;
     bool found_data = false;
-    u32 data_size = 0;
-    u8 search_byte = 0;
-    while (true) {
-        search_byte = TRY(read_u8());
-        CHECK_OK(LoaderError::Category::IO, "Reading byte searching for data");
-        if (search_byte != 0x64) // D
-            continue;
-
-        search_byte = TRY(read_u8());
-        CHECK_OK(LoaderError::Category::IO, "Reading next byte searching for data");
-        if (search_byte != 0x61) // A
-            continue;
-
-        u16 search_remaining = TRY(read_u16());
-        CHECK_OK(LoaderError::Category::IO, "Reading remaining bytes searching for data");
-        if (search_remaining != 0x6174) // TA
-            continue;
-
-        data_size = TRY(read_u32());
-        found_data = true;
-        break;
+    while (!found_data) {
+        auto chunk_header = TRY(m_stream->read_value<RIFF::ChunkID>());
+        if (chunk_header == RIFF::data_chunk_id) {
+            found_data = true;
+        } else {
+            TRY(m_stream->seek(-RIFF::chunk_id_size, SeekMode::FromCurrentPosition));
+            auto chunk = TRY(m_stream->read_value<RIFF::Chunk>());
+            dbgln_if(AWAVLOADER_DEBUG, "Unhandled WAV chunk of type {}, size {} bytes", chunk.id.as_ascii_string(), chunk.size);
+            // TODO: Handle LIST INFO chunks.
+        }
     }
     }
 
 
-    ok = ok && found_data;
-    CHECK_OK(LoaderError::Category::Format, "Found no data chunk");
-
-    ok = ok && data_size < maximum_wav_size;
-    CHECK_OK(LoaderError::Category::Format, "Data was too large");
+    u32 data_size = TRY(m_stream->read_value<LittleEndian<u32>>());
+    CHECK(found_data, LoaderError::Category::Format, "Found no data chunk");
+    CHECK(data_size < maximum_wav_size, LoaderError::Category::Format, "Data too large");
 
 
     m_total_samples = data_size / block_size_bytes;
     m_total_samples = data_size / block_size_bytes;
 
 
@@ -337,7 +284,8 @@ MaybeLoaderError WavLoaderPlugin::parse_header()
         block_size_bytes,
         block_size_bytes,
         m_total_samples);
         m_total_samples);
 
 
-    m_byte_offset_of_data_samples = bytes_read;
+    m_byte_offset_of_data_samples = TRY(m_stream->tell());
     return {};
     return {};
 }
 }
+
 }
 }

+ 4 - 8
Userland/Libraries/LibAudio/WavLoader.h

@@ -17,14 +17,10 @@
 
 
 namespace Audio {
 namespace Audio {
 
 
-// constants for handling the WAV header data
-static constexpr unsigned const WAVE_FORMAT_PCM = 0x0001;        // PCM
-static constexpr unsigned const WAVE_FORMAT_IEEE_FLOAT = 0x0003; // IEEE float
-static constexpr unsigned const WAVE_FORMAT_ALAW = 0x0006;       // 8-bit ITU-T G.711 A-law
-static constexpr unsigned const WAVE_FORMAT_MULAW = 0x0007;      // 8-bit ITU-T G.711 µ-law
-static constexpr unsigned const WAVE_FORMAT_EXTENSIBLE = 0xFFFE; // Determined by SubFormat
-
-// Parses and reads audio data from a WAV file.
+// Loader for the WAVE (file extension .wav) uncompressed audio file format.
+// WAVE uses the Microsoft RIFF container.
+// Original RIFF Spec, without later extensions: https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
+// More concise WAVE information plus various spec links: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
 class WavLoaderPlugin : public LoaderPlugin {
 class WavLoaderPlugin : public LoaderPlugin {
 public:
 public:
     explicit WavLoaderPlugin(NonnullOwnPtr<SeekableStream> stream);
     explicit WavLoaderPlugin(NonnullOwnPtr<SeekableStream> stream);