-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. --[==[ Minimal WAV (RIFF/WAVE) decoder -> float32 sample buffer. Parses the header in pure Lua and hands the raw PCM data chunk to the C `sound.from_pcm` decoder (fast, format-aware). Supports uncompressed integer PCM (8/16/24/32-bit) and IEEE float32, including the WAVE_FORMAT_EXTENSIBLE wrapper. Optionally resamples to a target rate so an asset recorded at 44.1 kHz plays correctly on a 48 kHz device. ]==] local core = require("sound.core") local le16 = function(s, o) local a, b = s:byte(o, o + 1) return a + b * 256 end local le32 = function(s, o) local a, b, c, d = s:byte(o, o + 3) return a + b * 256 + c * 65536 + d * 16777216 end -- Map (format tag, bits) to the C from_pcm format string. local pcm_format = function(tag, bits) if tag == 3 then if bits == 32 then return "f32" end return nil elseif tag == 1 then if bits == 8 then return "u8" elseif bits == 16 then return "s16" elseif bits == 24 then return "s24" elseif bits == 32 then return "s32" end end return nil end ---! Decode WAV bytes into a sample buffer ---@ decode(`data`{.str}, `opts`{.tbl .opt}) -> `buf`{.tbl .or_nil}, `info`{.tbl .err} --[===[ `opts.rate` (optional) resamples the result to that sample rate. Returns the float32 buffer and an info table `{ channels, rate, bits, frames }`, or `nil, err`. ]===] local decode = function(data, opts) opts = opts or {} if type(data) ~= "string" or #data < 44 then return nil, "not a WAV file (too short)" end if data:sub(1, 4) ~= "RIFF" or data:sub(9, 12) ~= "WAVE" then return nil, "not a RIFF/WAVE file" end local channels, rate, bits, tag local pcm = nil local pos = 13 -- first chunk after "WAVE" local n = #data while pos + 8 <= n + 1 do local id = data:sub(pos, pos + 3) local size = le32(data, pos + 4) local body = pos + 8 if id == "fmt " then tag = le16(data, body) channels = le16(data, body + 2) rate = le32(data, body + 4) bits = le16(data, body + 14) if tag == 0xFFFE and size >= 24 then -- WAVE_FORMAT_EXTENSIBLE: real tag is the first 2 bytes of the GUID tag = le16(data, body + 24) end elseif id == "data" then local last = body + size - 1 if last > n then last = n -- tolerate a truncated/short data chunk end pcm = data:sub(body, last) break end -- chunks are word-aligned: skip the padding byte on odd sizes pos = body + size + (size % 2) end if not channels then return nil, "WAV missing fmt chunk" end if not pcm then return nil, "WAV missing data chunk" end local fmt = pcm_format(tag, bits) if not fmt then return nil, "unsupported WAV format (tag " .. tostring(tag) .. ", " .. tostring(bits) .. " bits)" end local buf, err = core.from_pcm(pcm, channels, fmt) if not buf then return nil, err end if opts.rate and opts.rate ~= rate then buf, err = core.resample(buf, rate, opts.rate) if not buf then return nil, err end rate = opts.rate end return buf, { channels = channels, rate = rate, bits = bits, frames = buf:frames() } end ---! Load and decode a WAV file from disk ---@ load(`path`{.str}, `opts`{.tbl .opt}) -> `buf`{.tbl .or_nil}, `info`{.tbl .err} local load = function(path, opts) local f, err = io.open(path, "rb") if not f then return nil, err end local data = f:read("*a") f:close() return decode(data, opts) end return { decode = decode, load = load, }