Skip to content

Wave API

haruki7049 edited this page Mar 29, 2026 · 1 revision

Wave API

Wave(T) is a generic type that represents a PCM audio waveform. It holds an owned slice of floating-point audio samples together with metadata (sample rate and channel count).

Type parameter

T is the sample data type. Supported values are f64, f80, and f128.

const Wave = lightmix.Wave;

// Wave type for 64-bit floating-point samples
const MyWave = Wave(f64);

Fields

Field Type Description
samples []const T Owned slice of audio samples
allocator std.mem.Allocator Allocator that owns samples
sample_rate u32 Samples per second (e.g., 44100)
channels u16 Number of audio channels (1 = mono, 2 = stereo)

Creating a Wave

init

pub fn init(
    samples: []const T,
    allocator: std.mem.Allocator,
    options: InitOptions,
) std.mem.Allocator.Error!Self

Creates a Wave from an existing sample slice. The samples are deep-copied into memory owned by allocator, so the original slice can be freed or reused after the call.

InitOptions

Field Type Description
sample_rate u32 Samples per second
channels u16 Number of audio channels

Example

const allocator = std.heap.page_allocator;

// Generate a 440 Hz sine wave (A4), 1 second at 44100 Hz
var samples: [44100]f64 = undefined;
for (0..samples.len) |i| {
    const t = @as(f64, @floatFromInt(i)) / 44100.0;
    samples[i] = 0.5 * @sin(440.0 * 2.0 * std.math.pi * t);
}

const wave = try Wave(f64).init(samples[0..], allocator, .{
    .sample_rate = 44100,
    .channels = 1,
});
defer wave.deinit();

Freeing a Wave

deinit

pub fn deinit(self: Self) void

Frees the memory allocated for samples. Call this when the Wave is no longer needed to avoid memory leaks.


Reading a Wave from a file

read

pub fn read(
    file_extension: LowLevelInterfaces,
    allocator: std.mem.Allocator,
    reader: anytype,
) anyerror!Self

Reads audio data into a new Wave. Currently only .wav is supported as file_extension.

Example — reading from an embedded file

var reader = std.Io.Reader.fixed(@embedFile("audio.wav"));
const wave = try Wave(f64).read(.wav, allocator, &reader);
defer wave.deinit();

Writing a Wave to a file

write

pub fn write(
    self: Self,
    file_extension: LowLevelInterfaces,
    writer: anytype,
    options: LowLevelInterfaces.writeOptions(file_extension),
) anyerror!void

Writes the wave to a file. Currently only .wav is supported as file_extension.

Because the underlying I/O API requires buffered writes, you must allocate a buffer and use a buffered writer. Call writer.interface.flush() after writing to ensure all bytes are written to disk.

writeWavOptions (used when file_extension is .wav)

Field Type Default Description
bits u16 (required) Bits per sample (e.g., 16, 24, 32)
format_code zigggwavvv.FormatCode (required) .pcm or .ieee_float
use_fact bool false Include a fact chunk in the WAV file
use_peak bool false Include a PEAK chunk in the WAV file
peak_timestamp u32 0 Timestamp written into the PEAK chunk

Example

const allocator = std.heap.page_allocator;

const file = try std.fs.cwd().createFile("result.wav", .{});
defer file.close();

// Allocate a write buffer (10 MB is enough for most waves)
const buf = try allocator.alloc(u8, 10 * 1024 * 1024);
defer allocator.free(buf);
var writer = file.writer(buf);

try wave.write(.wav, &writer.interface, .{
    .bits = 16,
    .format_code = .pcm,
});
try writer.interface.flush();

Mixing two Waves

mix

pub fn mix(
    self: Self,
    other: Self,
    options: mixOptions,
) std.mem.Allocator.Error!Self

Combines two waves sample-by-sample using a mixer function, returning a new Wave.

Both waves must have identical sample_rate, channels, and sample count—otherwise the program panics.

mixOptions

Field Type Default Description
mixer fn(T, T) T default_mixing_expression Function applied to each sample pair

The default mixer adds the two samples: left + right.

Example — default (additive) mix

const mixed = try wave_a.mix(wave_b, .{});
defer mixed.deinit();

Example — custom mixer (average)

const mixed = try wave_a.mix(wave_b, .{
    .mixer = struct {
        fn avg(left: f64, right: f64) f64 {
            return (left + right) / 2.0;
        }
    }.avg,
});
defer mixed.deinit();

Applying filters

filter

pub fn filter(
    self: *Self,
    comptime filter_fn: anytype,
) anyerror!void

Applies a filter function in-place. The original samples are freed and replaced by the filtered output.

The filter function must have the signature:

fn my_filter(comptime T: type, wave: Wave(T)) anyerror!Wave(T)

Example — linear fade-out (decay) filter

fn decay(comptime T: type, original: Wave(T)) !Wave(T) {
    const allocator = original.allocator;
    var result: std.ArrayListUnmanaged(T) = .empty;
    defer result.deinit(allocator);

    for (original.samples, 0..) |sample, i| {
        const factor = @as(T, @floatFromInt(original.samples.len - i)) /
                       @as(T, @floatFromInt(original.samples.len));
        try result.append(allocator, sample * factor);
    }

    return Wave(T).init(result.items, allocator, .{
        .sample_rate = original.sample_rate,
        .channels = original.channels,
    });
}

var wave = try Wave(f64).init(samples[0..], allocator, .{ .sample_rate = 44100, .channels = 1 });
defer wave.deinit();

try wave.filter(decay); // wave now holds the decayed samples

filter_with

pub fn filter_with(
    self: *Self,
    comptime args_type: type,
    comptime filter_fn: anytype,
    args: args_type,
) anyerror!void

Like filter, but passes extra arguments to the filter function. The filter function must have the signature:

fn my_filter(comptime T: type, wave: Wave(T), args: ArgsType) anyerror!Wave(T)

Separating a Wave

separate

pub fn separate(
    self: Self,
    options: SeparateOptions,
) (SeparateErrors || std.mem.Allocator.Error)!SeparateResult

Splits the wave into two new Wave instances at a given sample index.

SeparateOptions

Field Type Description
allocator std.mem.Allocator Allocator for the two new Wave instances
separate_point usize Sample index at which to split

SeparateResult

Field Type Description
initial Wave(T) Samples from index 0 to separate_point (exclusive)
terminal Wave(T) Samples from separate_point to the end

SeparateErrors

Error Cause
SeparatingZeroLengthWave The wave contains no samples
TooBigSeparatePoint separate_point is larger than the sample count

Example

// Split at 0.5 seconds (22050 samples at 44100 Hz)
const parts = try wave.separate(.{
    .allocator = allocator,
    .separate_point = 22050,
});
defer parts.initial.deinit();
defer parts.terminal.deinit();

Padding a Wave

fill_zero_to_end

pub fn fill_zero_to_end(
    self: Self,
    start: usize,
    end: usize,
) std.mem.Allocator.Error!Self

Returns a new Wave of length end that contains the original samples up to start, with the remaining samples (from start to end) replaced by zeros (silence).

Example

// Keep the first 0.5 seconds, fill the rest with silence up to 1 second
const padded = try wave.fill_zero_to_end(22050, 44100);
defer padded.deinit();

Playing a Wave

play

pub fn play(
    self: Self,
    allocator: std.mem.Allocator,
) anyerror!void

Plays the wave through the system audio output. Blocks until playback is complete.

Uses zaudio (included as a lightmix dependency) internally.

Example

const allocator = std.heap.page_allocator;
try wave.play(allocator);

Note: For build-time playback, see Build-Helpers.


See also

Clone this wiki locally