Something went wrong. Try again.
Plays short audio files such as a terminal bell through PipeWire on Linux and WASAPI on Windows.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! RIFF/WAVE: integer PCM of 8, 16, 24 or 32 bits and floating point of 32 or//! 64, plainly or in `WAVE_FORMAT_EXTENSIBLE`, in either byte order.
const std = @import("std");const riff = @import("riff");
const decode_mod = @import("../decode.zig");const Builder = decode_mod.Builder;const Error = decode_mod.Error;
const format_pcm = 0x0001;const format_float = 0x0003;const format_extensible = 0xfffe;
const Encoding = enum { int, float };
const Fmt = struct { encoding: Encoding, channels: u16, rate: u32, /// Bytes per sample, which is the container's width rather than the /// count of valid bits an extensible header gives. width: u8,};
pub fn decode(bytes: []const u8, out: *Builder) Error!void { var input: std.Io.Reader = .fixed(bytes); var reader = riff.Reader.init(&input) catch return error.InvalidData; const endian: std.builtin.Endian = switch (reader.order) { .little => .little, .big => .big, };
var fmt: ?Fmt = null; while (reader.next() catch return error.InvalidData) |chunk| { if (chunk.is("fmt ")) { var buf: [40]u8 = @splat(0); const len = @min(chunk.len, buf.len); reader.readSome(buf[0..len]) catch return error.InvalidData; fmt = try parseFmt(buf[0..len], endian); try out.begin(fmt.?.rate, fmt.?.channels); } else if (chunk.is("data")) { const f = fmt orelse return error.InvalidData; // The payload starts where the walk has got to in the buffer, and // a file cut short ends early rather than being refused. const rest = bytes[input.seek..]; const len = if (chunk.isUnknownLength()) rest.len else @min(chunk.len, rest.len); const frame_bytes = @as(usize, f.width) * f.channels; const data = rest[0 .. len - len % frame_bytes]; const samples = try out.addFrames(data.len / frame_bytes); for (samples, 0..) |*s, i| s.* = sample(f, data[i * f.width ..][0..f.width], endian); return; } } return error.NoAudio;}
fn parseFmt(buf: []const u8, endian: std.builtin.Endian) Error!Fmt { if (buf.len < 16) return error.InvalidData; var tag = std.mem.readInt(u16, buf[0..2], endian); const channels = std.mem.readInt(u16, buf[2..4], endian); const rate = std.mem.readInt(u32, buf[4..8], endian); const block_align = std.mem.readInt(u16, buf[12..14], endian); const bits = std.mem.readInt(u16, buf[14..16], endian); if (tag == format_extensible) { // The real format is the first two bytes of the sub-format GUID. if (buf.len < 40) return error.InvalidData; tag = std.mem.readInt(u16, buf[24..26], endian); } if (channels == 0 or rate == 0) return error.InvalidData; const width = std.math.divCeil(u16, bits, 8) catch unreachable; if (block_align != width * channels) return error.InvalidData; const encoding: Encoding = switch (tag) { format_pcm => if (width >= 1 and width <= 4) .int else return error.UnsupportedFormat, format_float => if (width == 4 or width == 8) .float else return error.UnsupportedFormat, else => return error.UnsupportedFormat, }; return .{ .encoding = encoding, .channels = channels, .rate = rate, .width = @intCast(width) };}
fn sample(f: Fmt, b: []const u8, endian: std.builtin.Endian) f32 { return switch (f.encoding) { .float => switch (f.width) { 4 => @bitCast(std.mem.readInt(u32, b[0..4], endian)), 8 => @floatCast(@as(f64, @bitCast(std.mem.readInt(u64, b[0..8], endian)))), else => unreachable, }, .int => switch (f.width) { // Eight-bit WAVE samples are unsigned; every wider one is signed. 1 => @as(f32, @floatFromInt(@as(i16, b[0]) - 128)) / 128.0, 2 => @as(f32, @floatFromInt(std.mem.readInt(i16, b[0..2], endian))) / 32768.0, 3 => @as(f32, @floatFromInt(std.mem.readInt(i24, b[0..3], endian))) / 8388608.0, 4 => @as(f32, @floatFromInt(std.mem.readInt(i32, b[0..4], endian))) / 2147483648.0, else => unreachable, }, };}
test "integer and floating point WAVE" { try decode_mod.expectTone(@embedFile("../tests/data/tone-s16-mono-44100.wav"), 44100, 1, 4410); try decode_mod.expectTone(@embedFile("../tests/data/tone-f32-stereo-22050.wav"), 22050, 2, 2205); try decode_mod.expectTone(@embedFile("../tests/data/tone-u8-mono-8000.wav"), 8000, 1, 800); try decode_mod.expectTone(@embedFile("../tests/data/tone-s24-stereo-48000.wav"), 48000, 2, 4800);}
test "a truncated data chunk keeps the whole frames it has" { const full = @embedFile("../tests/data/tone-s16-mono-44100.wav"); var out: Builder = .{ .gpa = std.testing.allocator, .max_duration_ms = 30_000 }; defer out.samples.deinit(std.testing.allocator); try decode(full[0 .. full.len - 101], &out); try std.testing.expectEqual(@as(usize, 4410 - 51), out.frames());}
test "A-law is refused as unsupported" { var bytes = @embedFile("../tests/data/tone-s16-mono-44100.wav").*; // The format tag, just after `fmt ` and its length. std.mem.writeInt(u16, bytes[20..22], 0x0006, .little); std.mem.writeInt(u16, bytes[34..36], 8, .little); std.mem.writeInt(u16, bytes[32..34], 1, .little); var out: Builder = .{ .gpa = std.testing.allocator, .max_duration_ms = 30_000 }; defer out.samples.deinit(std.testing.allocator); try std.testing.expectError(error.UnsupportedFormat, decode(&bytes, &out));}