Skip to content

File WavParser.h

File List > external-docs > libDaisy > src > util > WavParser.h

Go to the documentation of this file

Source Code

#pragma once

// Minimal, allocation-free WAV (RIFF) parser suitable for embedded (e.g.,
// STM32). Supports canonical PCM / IEEE float WAV, handles JUNK and unknown
// chunks by skipping.
// Does not load sample data; records data offset & length
// so caller can stream.
//
// Limitations / Assumptions:
// - Little-endian host or platform where manual LE decoding is used (safe on
// STM32/Cortex-M).
// - No dynamic allocation; fixed maximum number of metadata entries.
// - Ignores extensible format extra fields beyond what is necessary for basic
// parsing.
// - Caller provides an abstract Reader (seek + read) so this can work with
// FatFS, cstdio, raw data, etc.
//
// Typical usage:
//   FileReader fr(fopen("file.wav", "rb"));
//   WavParser parser;
//   if(parser.parse(fr)) {
//      // Use parser.info() to get format, sampleRate, etc.
//      // Use parser.dataOffset(), parser.dataSize() to stream audio.
//   }
//

#include <cstddef>
#include <cstdint>
#include "FileReader.h"

namespace daisy
{
struct WavFormatInfo
{
    uint16_t audioFormat   = 0; // 1 = PCM, 3 = IEEE float, 0xFFFE = extensible
    uint16_t numChannels   = 0;
    uint32_t sampleRate    = 0;
    uint32_t byteRate      = 0;
    uint16_t blockAlign    = 0;
    uint16_t bitsPerSample = 0;
    // For extensible (0xFFFE)
    uint16_t validBitsPerSample = 0; // if provided
    uint32_t channelMask        = 0; // if provided
    uint16_t subFormat = 0; // wFormatTag of the sub-format GUID (first 2 bytes)
};

struct MetadataEntry
{
    uint32_t fourcc = 0; // chunk id
    uint32_t size   = 0; // payload size (before padding)
    uint32_t offset = 0; // file offset of chunk data
};

// Utility to form a FourCC constant at compile time: FCC("RIFF") not constexpr
// in pre-C++20 easily.
constexpr uint32_t make_fourcc(char a, char b, char c, char d)
{
    return (uint32_t(uint8_t(a))) | (uint32_t(uint8_t(b)) << 8)
           | (uint32_t(uint8_t(c)) << 16) | (uint32_t(uint8_t(d)) << 24);
}


class WavParser
{
  public:
    static constexpr uint32_t FOURCC_RIFF = make_fourcc('R', 'I', 'F', 'F');
    static constexpr uint32_t FOURCC_WAVE = make_fourcc('W', 'A', 'V', 'E');
    static constexpr uint32_t FOURCC_FMT  = make_fourcc('f', 'm', 't', ' ');
    static constexpr uint32_t FOURCC_DATA = make_fourcc('d', 'a', 't', 'a');
    static constexpr uint32_t FOURCC_JUNK = make_fourcc('J', 'U', 'N', 'K');
    static constexpr uint32_t FOURCC_FACT = make_fourcc('f', 'a', 'c', 't');
    static constexpr uint32_t FOURCC_LIST = make_fourcc('L', 'I', 'S', 'T');
    static constexpr uint32_t FOURCC_INFO = make_fourcc('I', 'N', 'F', 'O');

    static constexpr int MAX_METADATA_CHUNKS = 16; // tunable

    WavParser() = default;

    bool parse(IReader& r)
    {
        reset();
        if(!read_riff_header(r))
            return false;
        while(r.position() + 8 <= fileSize_)
        {
            ChunkHeader ch;
            if(!read_chunk_header(r, ch))
                return false;
            if(ch.id == FOURCC_FMT)
            {
                if(!parse_fmt_chunk(r, ch))
                    return false;
            }
            else if(ch.id == FOURCC_DATA)
            {
                dataOffset_ = r.position();
                dataSize_   = ch.size;
                // Skip data (we only record offset). Allow early break if we've got
                // fmt.
                if(!skip_chunk_payload(r, ch.size))
                    return false;
                haveData_ = true;
            }
            else
            {
                // Store metadata if room
                if(metadataCount_ < MAX_METADATA_CHUNKS)
                {
                    metadata_[metadataCount_].fourcc = ch.id;
                    metadata_[metadataCount_].size   = ch.size;
                    metadata_[metadataCount_].offset = r.position();
                    metadataCount_++;
                }
                if(!skip_chunk_payload(r, ch.size))
                    return false;
            }

            // Chunks are padded to even size
            if(ch.size & 1)
            {
                uint8_t pad;
                if(r.read(&pad, 1) != 1)
                    break;
            }

            if(haveFmt_ && haveData_)
                break; // parsed what we need
        }
        return haveFmt_ && haveData_;
    }

    const WavFormatInfo& info() const { return fmt_; }
    uint32_t             dataOffset() const { return dataOffset_; }
    uint32_t             dataSize() const { return dataSize_; }
    const MetadataEntry* metadata() const { return metadata_; }
    int                  metadataCount() const { return metadataCount_; }

  private:
    struct ChunkHeader
    {
        uint32_t id;
        uint32_t size;
    };

    void reset()
    {
        fmt_           = WavFormatInfo{};
        haveFmt_       = false;
        haveData_      = false;
        dataOffset_    = 0;
        dataSize_      = 0;
        metadataCount_ = 0;
        fileSize_      = 0;
    }

    static uint16_t rd_u16(const uint8_t* b)
    {
        return uint16_t(b[0]) | (uint16_t(b[1]) << 8);
    }
    static uint32_t rd_u32(const uint8_t* b)
    {
        return uint32_t(b[0]) | (uint32_t(b[1]) << 8) | (uint32_t(b[2]) << 16)
               | (uint32_t(b[3]) << 24);
    }

    bool read_exact(IReader& r, void* dst, size_t n)
    {
        return r.read(dst, n) == n;
    }

    bool read_riff_header(IReader& r)
    {
        uint8_t hdr[12];
        if(!read_exact(r, hdr, 12))
            return false;
        uint32_t riff           = rd_u32(hdr + 0);
        uint32_t fileSizeMinus8 = rd_u32(hdr + 4); // size of file - 8
        uint32_t wave           = rd_u32(hdr + 8);
        if(riff != FOURCC_RIFF || wave != FOURCC_WAVE)
            return false;
        fileSize_ = fileSizeMinus8 + 8; // nominal
        if(r.size() != 0)
            fileSize_ = r.size(); // trust reader if known
        return true;
    }

    bool read_chunk_header(IReader& r, ChunkHeader& ch)
    {
        uint8_t buf[8];
        if(!read_exact(r, buf, 8))
            return false;
        ch.id   = rd_u32(buf);
        ch.size = rd_u32(buf + 4);
        return true;
    }

    bool skip_chunk_payload(IReader& r, uint32_t sz)
    {
        // Seek ahead instead of reading to avoid buffer.
        uint32_t target = r.position() + sz;
        return r.seek(target);
    }

    bool parse_fmt_chunk(IReader& r, const ChunkHeader& ch)
    {
        if(ch.size < 16)
            return false;
        uint8_t core[16];
        if(!read_exact(r, core, 16))
            return false;
        fmt_.audioFormat   = rd_u16(core + 0);
        fmt_.numChannels   = rd_u16(core + 2);
        fmt_.sampleRate    = rd_u32(core + 4);
        fmt_.byteRate      = rd_u32(core + 8);
        fmt_.blockAlign    = rd_u16(core + 12);
        fmt_.bitsPerSample = rd_u16(core + 14);
        uint32_t consumed  = 16;

        if(fmt_.audioFormat != 1 && fmt_.audioFormat != 3
           && fmt_.audioFormat != 0xFFFE)
        {
            // unsupported basic format
            skip_rest_of_chunk(r, ch, consumed);
            return false;
        }

        if(ch.size > consumed)
        {
            // Read the remaining bytes (small), up to a cap we care about
            uint32_t remain = ch.size - consumed;
            // We'll process extension for extensible
            if(fmt_.audioFormat == 0xFFFE && remain >= 2)
            {
                uint8_t extSizeBuf[2];
                if(!read_exact(r, extSizeBuf, 2))
                    return false;
                consumed += 2;
                uint16_t extSize = rd_u16(extSizeBuf);
                if(extSize >= 22 && remain >= 2 + 22)
                { // extensible has at least 22 bytes after cbSize
                    uint8_t ext[22];
                    if(!read_exact(r, ext, 22))
                        return false;
                    consumed += 22;
                    fmt_.validBitsPerSample = rd_u16(ext + 0);
                    fmt_.channelMask        = rd_u32(ext + 2);
                    fmt_.subFormat          = rd_u16(
                        ext
                        + 6); // first two bytes of GUID contain the actual format tag
                    // skip any rest of ext
                    if(extSize > 22)
                    {
                        uint32_t skip = extSize - 22;
                        if(!skip_bytes(r, skip))
                            return false;
                        consumed += skip;
                    }
                }
                else
                {
                    // skip remainder if not long enough
                    if(!skip_bytes(r, remain - 2))
                        return false;   // we already read extSizeBuf
                    consumed = ch.size; // consumed all
                }
            }
            else
            {
                // skip any unneeded extended bytes for PCM / float
                if(!skip_bytes(r, remain))
                    return false;
                consumed = ch.size;
            }
        }
        haveFmt_ = true;
        return true;
    }

    bool skip_bytes(IReader& r, uint32_t count)
    {
        uint32_t target = r.position() + count;
        return r.seek(target);
    }

    bool
    skip_rest_of_chunk(IReader& r, const ChunkHeader& ch, uint32_t consumed)
    {
        if(consumed < ch.size)
            return skip_bytes(r, ch.size - consumed);
        return true;
    }

    WavFormatInfo fmt_{};
    bool          haveFmt_    = false;
    bool          haveData_   = false;
    uint32_t      dataOffset_ = 0;
    uint32_t      dataSize_   = 0;
    MetadataEntry metadata_[MAX_METADATA_CHUNKS];
    int           metadataCount_ = 0;
    uint32_t      fileSize_      = 0;
};

} // namespace daisy