Skip to content

Instantly share code, notes, and snippets.

@ForeverZer0
Last active May 31, 2026 19:00
Show Gist options
  • Select an option

  • Save ForeverZer0/afea88e8d0f49ea34e872103202aa1b6 to your computer and use it in GitHub Desktop.

Select an option

Save ForeverZer0/afea88e8d0f49ea34e872103202aa1b6 to your computer and use it in GitHub Desktop.
An allocation-free QOA (Quite OK Audio Format) decoder that streams signed 16-bit PCM samples (interleaved) into a buffer. (Zig 0.15.1 or higher)
/// The MIT License (MIT)
///
/// Copyright (c) 2026, Eric Freed
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
const std = @import("std");
const Reader = std.Io.Reader;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
/// The number of slices in a standard QOA frame.
/// The last frame in the track is an exception, and may contain less.
const slices_per_frame = 256;
/// The number of samples per frame slice.
const samples_per_slice = 20;
/// The maximum number of PCM frames per channel.
const max_samples_per_channel = slices_per_frame * samples_per_slice; // 5120
/// A table that maps each of the scale factors and quantized
/// residuals to their unscaled & dequantized version.
const dequant_table = [16][8]i32{
// zig fmt: off
.{ 1, -1, 3, -3, 5, -5, 7, -7},
.{ 5, -5, 18, -18, 32, -32, 49, -49},
.{ 16, -16, 53, -53, 95, -95, 147, -147},
.{ 34, -34, 113, -113, 203, -203, 315, -315},
.{ 63, -63, 210, -210, 378, -378, 588, -588},
.{ 104, -104, 345, -345, 621, -621, 966, -966},
.{ 158, -158, 528, -528, 950, -950, 1477, -1477},
.{ 228, -228, 760, -760, 1368, -1368, 2128, -2128},
.{ 316, -316, 1053, -1053, 1895, -1895, 2947, -2947},
.{ 422, -422, 1405, -1405, 2529, -2529, 3934, -3934},
.{ 548, -548, 1828, -1828, 3290, -3290, 5117, -5117},
.{ 696, -696, 2320, -2320, 4176, -4176, 6496, -6496},
.{ 868, -868, 2893, -2893, 5207, -5207, 8099, -8099},
.{1064, -1064, 3548, -3548, 6386, -6386, 9933, -9933},
.{1286, -1286, 4288, -4288, 7718, -7718, 12005, -12005},
.{1536, -1536, 5120, -5120, 9216, -9216, 14336, -14336},
// zig fmt: on
};
/// Allocation-free QOA (Quite OK Audio Format) decoder implementation.
/// Streams decoded PCM samples to a user-supplied buffer.
///
/// [*] `max_channels`: The maximum number of channels to support (1-8).
///
/// The size of the struct grows by ~5 kB for each channel.
pub fn QoaDecoder(max_channels: comptime_int) type {
return struct {
const Self = @This();
/// The number of discrete audio channels multiplexed within the stream.
channels: u8,
/// The playback frequency of the audio stream in frames per second (Hz).
sample_rate: u32,
/// Total number of multichannel PCM frames.
/// Use `frames * channels` for the total number of samples.
frames: u64,
/// Index of the current PCM frame.
current_frame: u64,
/// Reference to the source reader object.
reader: *Reader,
/// A least mean square filter used for sample prediction.
lms: [max_channels]Lms,
/// Internal cache that contains the data for one full QOA frame.
cache: struct {
/// Fixed length buffer containing the interleaved PCM samples.
buffer: [max_samples_per_channel * max_channels]i16 = undefined,
/// The number of samples that can be drained from the cache.
available: usize = 0,
/// The offset into the buffer to begin draining from.
offset: usize = 0,
} = .{},
/// Error set used by the decoder.
pub const DecodeError = error{
/// The data was not in the correct format is corrupted.
InvalidData,
} || Reader.Error;
/// Initializes the decoder with the given reader as the audio source.
/// Asserts that the reader has a buffer of at least 8 bytes in size.
///
/// [*] `reader`: The audio source in QOA format.
pub fn init(reader: *Reader) !Self {
assert(reader.buffer.len >= @sizeOf(u64));
// qoaf
const magic = 0x716F6166;
if (try reader.takeInt(u32, .big) != magic) {
return error.InvalidData;
}
const frames = try reader.takeInt(u32, .big);
// Peek the first frame header
const value = try reader.peekInt(u64, .big);
const frame_header = try FrameHeader(max_channels).init(value);
return .{
.frames = frames,
.channels = frame_header.channels,
.sample_rate = frame_header.sample_rate,
.current_frame = 0,
.reader = reader,
.lms = blk: {
var array: [max_channels]Lms = undefined;
for (&array) |*lms| lms.* = .{};
break :blk array;
},
};
}
/// Decodes interleaved signed 16-bit integer PCM samples directly into `samples`.
/// The decoder will fill the buffer up to its capacity based on available stream data.
///
/// [*] `ptr`: The type erased pointer to the decoder implementation.
/// [*] `num_frames`: The desired number of multi-channel frames to read.
/// [*] `samples`: The destination buffer for the decoded samples.
///
/// Returns the number of *frames* successfully decoded.
pub fn decode(self: *Self, num_frames: usize, samples: [*]i16) DecodeError!usize {
var remaining_frames = num_frames;
var dest = samples;
while (remaining_frames > 0) {
if (self.cache.available == 0) {
self.cache.offset = 0;
const frame_count = self.decodeFrame() catch |e| {
if (e == error.EndOfStream) break;
return e;
};
self.cache.available = frame_count * self.channels;
if (self.cache.available == 0) break;
}
const count = @min(self.cache.available, remaining_frames * self.channels);
const source: []i16 = self.cache.buffer[self.cache.offset..][0..count];
@memcpy(dest, source);
dest += count;
self.cache.offset += count;
self.cache.available -= count;
const read_frames = @divExact(count, self.channels);
remaining_frames -= read_frames;
self.current_frame += read_frames;
}
return num_frames - remaining_frames;
}
/// Decodes a full QOA frame and caches the decoded samples into an internal buffer.
/// Returns the number of PCM frames decoded.
fn decodeFrame(self: *Self) !u16 {
const header = try FrameHeader(max_channels).read(self.reader);
var sample_data = self.cache.buffer[0 .. header.channels * header.samples];
for (0..header.channels) |ch| {
try self.lms[ch].read(self.reader);
}
var sample_index: u16 = 0;
while (sample_index < header.samples) : (sample_index += samples_per_slice) {
for (0..header.channels) |c| {
var slice = try self.reader.takeInt(u64, .big);
const scalefactor = (slice >> 60) & 0xf;
const slice_start = sample_index * header.channels + c;
const slice_end = @min(sample_index + samples_per_slice, header.samples) * header.channels + c;
var si = slice_start;
while (si < slice_end) : (si += header.channels) {
const predicted = self.lms[c].predict();
const quantized = (slice >> 57) & 0x7;
const dequantized = dequant_table[@intCast(scalefactor)][@intCast(quantized)];
const reconstructed: i16 = @intCast(std.math.clamp(predicted + dequantized, std.math.minInt(i16), std.math.maxInt(i16)));
sample_data[si] = reconstructed;
slice <<= 3;
self.lms[c].update(reconstructed, dequantized);
}
}
}
return header.samples;
}
};
}
/// A least mean square filter, which predicts the next sample based on
/// the previous 4 reconstructed samples. It does so by continuously
/// adjusting 4 weights based on the residual of the previous prediction.
const Lms = struct {
/// The number of previous samples to track.
const len = 4;
/// Array of history values.
history: [len]i32 = @splat(0),
/// Array of weights values.
weights: [len]i32 = @splat(0),
/// Reads and processes the history/weights values from the stream.
///
/// [*] `stream`: The stream to read from.
fn read(lms: *Lms, stream: *Reader) !void {
for (&lms.history) |*h| {
h.* = try stream.takeInt(i16, .big);
}
for (&lms.weights) |*w| {
w.* = try stream.takeInt(i16, .big);
}
}
/// Computes and returns the next prediction value.
fn predict(lms: Lms) i32 {
var prediction: i32 = 0;
for (lms.weights, lms.history) |w, h| {
prediction += w * h;
}
return prediction >> 13;
}
/// Updates the filter a new sample and residual.
///
/// [*] `sample`: The value of the sample.
/// [*] `residual`: The residual (difference) value.
fn update(lms: *Lms, sample: i16, residual: i32) void {
const delta = residual >> 4;
for (0..len) |i| {
lms.weights[i] += if (lms.history[i] < 0) -delta else delta;
}
for (0..len - 1) |i| {
lms.history[i] = lms.history[i + 1];
}
lms.history[len - 1] = sample;
}
};
/// Describes the per-frame header.
fn FrameHeader(max_channels: comptime_int) type {
return struct {
/// The number of discrete audio channels multiplexed within the stream.
channels: u8,
/// The playback frequency of the audio stream in frames per second (Hz).
sample_rate: u24,
/// The number of samples per channel in this frame.
samples: u16,
/// The size of the frame, including the header.
size: u16,
/// Initializes and validates the frame header from an integer.
///
/// [*] `value`: The raw value of the header, as an integer.
///
/// Returns the header, or an error if data is invalid.
pub fn init(value: u64) error{InvalidData}!@This() {
const channels: u8 = @intCast((value >> 56) & 0x0000FF);
const sample_rate: u24 = @intCast((value >> 32) & 0xFFFFFF);
const samples: u16 = @intCast((value >> 16) & 0x00FFFF);
const size: u16 = @intCast((value) & 0x00FFFF);
const data_size = size - @sizeOf(u64) - Lms.len * 4 * channels;
const num_slices = data_size / 8;
const max_total_samples = num_slices * samples_per_slice;
if (channels == 0 or
channels > max_channels or
samples * channels > max_total_samples)
{
return error.InvalidData;
}
return .{
.channels = channels,
.sample_rate = sample_rate,
.samples = samples,
.size = size,
};
}
/// Reads and validates the frame header from the stream.
///
/// [*] `stream`: The stream to read from.
///
/// Returns the header, or an error if data is invalid.
pub fn read(reader: *Reader) !@This() {
const value = try reader.takeInt(u64, .big);
return .init(value);
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment