// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! FLAC, native and in Ogg. FLAC's own channel order for up to eight //! channels is the WAVE order (RFC 9639, Section 9.1.3), so nothing is moved. const std = @import("std"); const Allocator = std.mem.Allocator; const flac = @import("flac"); const decode_mod = @import("../decode.zig"); const Builder = decode_mod.Builder; const Error = decode_mod.Error; pub fn decode(gpa: Allocator, bytes: []const u8, in_ogg: bool, out: *Builder) Error!void { var input: std.Io.Reader = .fixed(bytes); // The MD5 is not checked: a Sound is for hearing, and a file that decodes // is worth playing whether or not its checksum agrees. const options: flac.DecodeOptions = .{ .verify_md5 = false }; if (in_ogg) { const buffer = try gpa.create([flac.ogg.page_buffer_size]u8); defer gpa.destroy(buffer); var d = flac.ogg.Decoder.init(gpa, &input, buffer, .{ .flac = options }) catch |err| return mapError(err); defer d.deinit(); try drain(&d, d.flac.streaminfo, out); } else { var d = flac.Decoder.init(gpa, &input, options) catch |err| return mapError(err); defer d.deinit(); try drain(&d, d.streaminfo, out); } } fn drain(d: anytype, info: flac.StreamInfo, out: *Builder) Error!void { try out.begin(info.sample_rate, info.channels); if (info.bits_per_sample == 0 or info.bits_per_sample > 32) return error.InvalidData; const scale = 1.0 / @as(f32, @floatFromInt(@as(u64, 1) << @intCast(info.bits_per_sample - 1))); while (d.next() catch |err| return mapError(err)) |frame| { if (frame.channels.len != info.channels) return error.InvalidData; const n = frame.header.block_size; const samples = try out.addFrames(n); for (frame.channels, 0..) |channel, c| { for (channel[0..n], 0..) |s, i| samples[i * info.channels + c] = @as(f32, @floatFromInt(s)) * scale; } } } fn mapError(err: anyerror) Error { return switch (err) { error.OutOfMemory => error.OutOfMemory, else => error.InvalidData, }; } test "native FLAC" { try decode_mod.expectTone(@embedFile("../tests/data/tone-stereo-44100.flac"), 44100, 2, 4410); } test "Ogg FLAC" { try decode_mod.expectTone(@embedFile("../tests/data/tone-mono-44100.oga"), 44100, 1, 4410); } test "a FLAC stream cut off inside its streaminfo block is invalid" { const bytes = @embedFile("../tests/data/tone-stereo-44100.flac"); try std.testing.expectError(error.InvalidData, decode_mod.decode(std.testing.allocator, bytes[0..20], 30_000)); }