-
Notifications
You must be signed in to change notification settings - Fork 1
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).
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);| 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) |
pub fn init(
samples: []const T,
allocator: std.mem.Allocator,
options: InitOptions,
) std.mem.Allocator.Error!SelfCreates 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();pub fn deinit(self: Self) voidFrees the memory allocated for samples. Call this when the Wave is no longer needed to avoid memory leaks.
pub fn read(
file_extension: LowLevelInterfaces,
allocator: std.mem.Allocator,
reader: anytype,
) anyerror!SelfReads 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();pub fn write(
self: Self,
file_extension: LowLevelInterfaces,
writer: anytype,
options: LowLevelInterfaces.writeOptions(file_extension),
) anyerror!voidWrites 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();pub fn mix(
self: Self,
other: Self,
options: mixOptions,
) std.mem.Allocator.Error!SelfCombines 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();pub fn filter(
self: *Self,
comptime filter_fn: anytype,
) anyerror!voidApplies 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 samplespub fn filter_with(
self: *Self,
comptime args_type: type,
comptime filter_fn: anytype,
args: args_type,
) anyerror!voidLike 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)pub fn separate(
self: Self,
options: SeparateOptions,
) (SeparateErrors || std.mem.Allocator.Error)!SeparateResultSplits 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();pub fn fill_zero_to_end(
self: Self,
start: usize,
end: usize,
) std.mem.Allocator.Error!SelfReturns 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();pub fn play(
self: Self,
allocator: std.mem.Allocator,
) anyerror!voidPlays 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.
-
Composer-API — Sequence multiple
Waveinstances in time -
Build-Helpers — Generate WAV files during
zig build