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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Recognizing a file by its contents and decoding it into memory.//!//! Every format comes out the same way: interleaved `f32` at the file's own//! rate and channel count, in the WAVE channel order. Metadata of every kind//! is ignored.
const std = @import("std");const Allocator = std.mem.Allocator;
const riff = @import("riff");const ogg = @import("ogg");
const wav = @import("decode/wav.zig");const flac = @import("decode/flac.zig");const opus = @import("decode/opus.zig");const mp3 = @import("decode/mp3.zig");
pub const Format = enum { /// RIFF/WAVE holding integer or floating point PCM. wav, /// Native FLAC, beginning `fLaC`. flac, /// FLAC in Ogg. ogg_flac, /// Opus in Ogg. ogg_opus, /// MPEG audio, Layers I to III. mp3,};
pub const Error = Allocator.Error || error{ /// Not a format this library knows. UnknownFormat, /// A format it knows holding something it does not decode: Ogg Vorbis, /// A-law in a WAVE file, an Opus channel mapping without a layout. UnsupportedFormat, /// Damaged or truncated past decoding. InvalidData, /// No audio at all. NoAudio, /// Longer than the limit the caller set. SoundTooLong,};
pub const Decoded = struct { samples: []f32, rate: u32, channels: u16,};
/// What the file is, from its first bytes.pub fn detect(bytes: []const u8) error{ UnknownFormat, UnsupportedFormat }!Format { if (riff.looksLike(bytes, "WAVE")) return .wav; if (std.mem.startsWith(u8, bytes, "fLaC")) return .flac; if (std.mem.startsWith(u8, bytes, "OggS")) { // The first page's body begins with the first packet, which names // its codec. The header is 27 bytes and then one byte per segment. if (bytes.len < 27) return error.UnknownFormat; const body = 27 + @as(usize, bytes[26]); if (body > bytes.len) return error.UnknownFormat; return switch (ogg.codec.identify(bytes[body..]) orelse return error.UnknownFormat) { .flac => .ogg_flac, .opus => .ogg_opus, else => error.UnsupportedFormat, }; } if (std.mem.startsWith(u8, bytes, "ID3")) { // An ID3v2 tag goes in front of MP3 files, and now and then in front // of a FLAC one. const after = id3v2Length(bytes) orelse return error.UnknownFormat; if (after <= bytes.len and std.mem.startsWith(u8, bytes[after..], "fLaC")) return .flac; return .mp3; } // An MPEG audio frame sync: eleven set bits, then a layer that is not the // reserved 00. if (bytes.len >= 2 and bytes[0] == 0xff and bytes[1] & 0xe0 == 0xe0 and bytes[1] & 0x06 != 0) return .mp3; return error.UnknownFormat;}
/// The length of an ID3v2 tag, header and footer included: ID3v2.4, Section 3.1.fn id3v2Length(bytes: []const u8) ?usize { if (bytes.len < 10) return null; var size: usize = 0; for (bytes[6..10]) |b| { if (b & 0x80 != 0) return null; size = size << 7 | b; } const footer: usize = if (bytes[5] & 0x10 != 0) 10 else 0; return 10 + size + footer;}
pub fn decode(gpa: Allocator, bytes: []const u8, max_duration_ms: u32) Error!Decoded { var out: Builder = .{ .gpa = gpa, .max_duration_ms = max_duration_ms }; errdefer out.samples.deinit(gpa); switch (try detect(bytes)) { .wav => try wav.decode(bytes, &out), .flac => try flac.decode(gpa, bytes, false, &out), .ogg_flac => try flac.decode(gpa, bytes, true, &out), .ogg_opus => try opus.decode(gpa, bytes, &out), .mp3 => try mp3.decode(bytes, &out), } return out.finish();}
/// Collects decoded samples, holding every format to the same limits.pub const Builder = struct { gpa: Allocator, max_duration_ms: u32, samples: std.ArrayList(f32) = .empty, rate: u32 = 0, channels: u16 = 0, max_frames: u64 = 0,
/// Says what the samples are, before any are added. pub fn begin(b: *Builder, rate: u32, channels: u16) Error!void { if (rate == 0 or channels == 0) return error.InvalidData; b.rate = rate; b.channels = channels; b.max_frames = @as(u64, rate) * b.max_duration_ms / std.time.ms_per_s; }
/// Room for `count` more frames, which the caller then fills. pub fn addFrames(b: *Builder, count: usize) Error![]f32 { std.debug.assert(b.channels != 0); const have = b.samples.items.len / b.channels; if (have + count > b.max_frames) return error.SoundTooLong; return b.samples.addManyAsSlice(b.gpa, count * b.channels); }
pub fn appendInterleaved(b: *Builder, samples: []const f32) Error!void { @memcpy(try b.addFrames(samples.len / b.channels), samples); }
/// Frames decoded so far. pub fn frames(b: Builder) usize { return if (b.channels == 0) 0 else b.samples.items.len / b.channels; }
fn finish(b: *Builder) Error!Decoded { if (b.frames() == 0) return error.NoAudio; return .{ .samples = try b.samples.toOwnedSlice(b.gpa), .rate = b.rate, .channels = b.channels, }; }};
const testing = std.testing;
test "detect" { try testing.expectEqual(Format.wav, try detect(@embedFile("tests/data/tone-s16-mono-44100.wav"))); try testing.expectEqual(Format.flac, try detect(@embedFile("tests/data/tone-stereo-44100.flac"))); try testing.expectEqual(Format.ogg_flac, try detect(@embedFile("tests/data/tone-mono-44100.oga"))); try testing.expectEqual(Format.ogg_opus, try detect(@embedFile("tests/data/tone-mono.opus"))); try testing.expectEqual(Format.mp3, try detect(@embedFile("tests/data/tone-mono-44100.mp3")));
try testing.expectError(error.UnknownFormat, detect("")); try testing.expectError(error.UnknownFormat, detect("not audio at all")); try testing.expectError(error.UnknownFormat, detect("RIFF\x04\x00\x00\x00AVI "));
// An Ogg page whose first packet is Vorbis's identification header. var vorbis: [27 + 1 + 7]u8 = @splat(0); @memcpy(vorbis[0..4], "OggS"); vorbis[26] = 1; vorbis[27] = 7; @memcpy(vorbis[28..], "\x01vorbis"); try testing.expectError(error.UnsupportedFormat, detect(&vorbis));
// A FLAC file behind an ID3v2 tag of 4 bytes. try testing.expectEqual(Format.flac, try detect("ID3\x04\x00\x00\x00\x00\x00\x04abcdfLaC")); try testing.expectEqual(Format.mp3, try detect("ID3\x04\x00\x00\x00\x00\x00\x04abcd\xff\xfb"));}
/// The fixtures are 0.1 s of a 440 Hz sine at ffmpeg's default amplitude of/// 1/8, which ffmpeg spreads over two channels at 1/sqrt(2) of that. Checks/// the shape and that the signal survived: its peak is near the amplitude in/// every channel.pub fn expectTone(bytes: []const u8, rate: u32, channels: u16, frame_count: ?usize) !void { const d = try decode(testing.allocator, bytes, 30_000); defer testing.allocator.free(d.samples); try testing.expectEqual(rate, d.rate); try testing.expectEqual(channels, d.channels); if (frame_count) |n| try testing.expectEqual(n, d.samples.len / channels); const amplitude: f32 = if (channels == 2) 0.125 / std.math.sqrt2 else 0.125; for (0..channels) |c| { var peak: f32 = 0; var i = c; while (i < d.samples.len) : (i += channels) peak = @max(peak, @abs(d.samples[i])); try testing.expectApproxEqAbs(amplitude, peak, 0.01); }}
test { _ = wav; _ = flac; _ = opus; _ = mp3;}