Repository navigation
Composer API
Composer(T) is a generic type for arranging and overlaying multiple Wave(T) instances at specific points in time, then mixing them into a single Wave(T).
T is the sample data type. Must match the T used by the Wave instances you want to compose. Supported values are f32, f64, f80, and f128.
const Composer = lightmix.Composer;
// Composer type for 64-bit floating-point samples
const MyComposer = Composer(f64);| Field | Type | Description |
|---|---|---|
items |
[]WaveInfo |
Waves and their start times, like std.ArrayList.items
|
capacity |
usize |
Number of WaveInfo slots allocated for items (do not modify) |
allocator |
std.mem.Allocator |
Allocator that owns items
|
sample_rate |
u32 |
Samples per second (e.g., 44100) |
channels |
u16 |
Number of audio channels (1 = mono, 2 = stereo) |
pub const WaveInfo = struct {
wave: Wave(T),
start_point: usize, // Sample index where this wave begins
};start_point is measured in samples. For example, at 44100 Hz, start_point = 44100 places the wave at the 1-second mark. It must be a multiple of the composer's channel count.
Note:
itemsis owned byallocatorand may be reallocated byappend,appendSliceandensureUnusedCapacity, so do not hold on to it across those calls. Likestd.ArrayList, aComposermust not be copied and used through both copies.Migration note (0.26.0): the
infofield was replaced byitems/capacity, andappend_convertedwas removed. Convert waves withWave(T).to_channelsbefore appending them.
pub fn init(
allocator: std.mem.Allocator,
options: InitOptions,
) InitErrors!SelfCreates an empty Composer with no waves. Use append or appendSlice to add waves afterwards.
InitErrors
| Error | Cause |
|---|---|
InvalidChannelCount |
options.channels is zero |
InvalidSampleRate |
options.sample_rate is zero |
InitOptions
| Field | Type | Description |
|---|---|---|
sample_rate |
u32 |
Samples per second |
channels |
u16 |
Number of audio channels |
Example
const allocator = std.heap.page_allocator;
var composer = try Composer(f64).init(allocator, .{
.sample_rate = 44100,
.channels = 1,
});
defer composer.deinit();pub fn init_capacity(
capacity: usize,
allocator: std.mem.Allocator,
options: InitOptions,
) (InitErrors || std.mem.Allocator.Error)!SelfCreates an empty Composer with room for capacity entries pre-allocated.
pub fn init_with(
info: []const WaveInfo,
allocator: std.mem.Allocator,
options: InitOptions,
) (InitErrors || Wave(T).MixErrors || std.mem.Allocator.Error)!SelfCreates a Composer pre-populated with the provided WaveInfo slice. The slice is copied internally.
Every entry is validated: error.MismatchedWaveProperties if a wave's sample_rate or channels differs from options, and error.UnalignedChannelOffset if a start_point is not a multiple of options.channels.
Example
const info: []const Composer(f64).WaveInfo = &.{
.{ .wave = wave_a, .start_point = 0 },
.{ .wave = wave_b, .start_point = 44100 }, // wave_b starts after 1 second
};
var composer = try Composer(f64).init_with(info, allocator, .{
.sample_rate = 44100,
.channels = 1,
});
defer composer.deinit();pub fn deinit(self: *Self) voidFrees the memory allocated for items and invalidates self. Because it takes *Self, the composer must be declared with var. This does not free the individual Wave instances inside each WaveInfo—those must be freed separately.
pub fn append(
self: *Self,
waveinfo: WaveInfo,
) (Wave(T).MixErrors || std.mem.Allocator.Error)!voidAdds a single WaveInfo to the composer in-place. The internal array grows with amortized O(1) cost, like std.ArrayList.
Returns error.MismatchedWaveProperties if the wave's sample_rate or channels differs from the composer's, and error.UnalignedChannelOffset if start_point is not a multiple of the channel count.
Example
var composer = try Composer(f64).init(allocator, .{ .sample_rate = 44100, .channels = 1 });
defer composer.deinit();
try composer.append(.{ .wave = wave_a, .start_point = 0 });
try composer.append(.{ .wave = wave_b, .start_point = 44100 });pub fn appendSlice(
self: *Self,
append_list: []const WaveInfo,
) (Wave(T).MixErrors || std.mem.Allocator.Error)!voidAdds multiple WaveInfo entries at once, in-place. Every entry is validated like append before anything is added.
Example
const extra: []const Composer(f64).WaveInfo = &.{
.{ .wave = wave_c, .start_point = 88200 },
.{ .wave = wave_d, .start_point = 132300 },
};
try composer.appendSlice(extra);pub fn ensureUnusedCapacity(
self: *Self,
additional_count: usize,
) std.mem.Allocator.Error!voidEnsures room for at least additional_count more entries without reallocating.
pub fn finalize(
self: Self,
options: Wave(T).mixOptions,
) (Wave(T).MixErrors || std.mem.Allocator.Error)!Wave(T)Mixes all waves in the composer into a single Wave(T) and returns it. An empty composer yields an empty wave.
The algorithm:
- Validates every entry (
MismatchedWaveProperties,UnalignedChannelOffset) and calculates the total length (furtheststart_point + wave.samples.len;error.Overflowif it overflowsusize). - Allocates one silent destination buffer of that length.
- Mixes each wave directly into position, sample-by-sample, using
options.mixer. No intermediate buffers are allocated.
Waves are not converted between channel counts; convert them with Wave(T).to_channels before appending. The default mixer is a plain sum that never clamps; pass .mixer = Wave(f64).saturating_mixing_expression to clamp to [-1.0, 1.0].
The returned Wave must be freed by the caller with deinit.
options is the same mixOptions used by Wave.mix. See Wave-API for details.
Example — basic sequence
var composer = try Composer(f64).init(allocator, .{ .sample_rate = 44100, .channels = 1 });
defer composer.deinit();
try composer.append(.{ .wave = note_c, .start_point = 0 });
try composer.append(.{ .wave = note_e, .start_point = 44100 });
try composer.append(.{ .wave = note_g, .start_point = 88200 });
const result = try composer.finalize(.{});
defer result.deinit();Example — overlapping (chord)
Waves with the same or overlapping start_point are mixed together:
try composer.append(.{ .wave = note_c, .start_point = 0 });
try composer.append(.{ .wave = note_e, .start_point = 0 }); // same start → chord
try composer.append(.{ .wave = note_g, .start_point = 0 });
const chord = try composer.finalize(.{});
defer chord.deinit();pub fn render_stream(
self: Self,
options: StreamOptions,
) (InitErrors || Wave(T).MixErrors || std.mem.Allocator.Error)!BlockIteratorReturns a BlockIterator that renders the composition in fixed-size blocks, keeping peak memory low for long compositions. It produces the same samples as finalize with the same mixer. The caller must call deinit on the iterator.
StreamOptions
| Field | Type | Default | Description |
|---|---|---|---|
mixer |
MixerFn |
Wave(T).default_mixing_expression |
Mixer for overlapping samples |
block_size |
usize |
0 |
Block size in samples. 0 selects 4096 rounded up to a multiple of the channel count; an explicit value must be a multiple of the channel count |
Returns the same validation errors as finalize, plus UnalignedChannelOffset for an unaligned explicit block_size.
The iterator borrows the composer's entries: the composer and every wave in it must outlive the iterator, and append, appendSlice and ensureUnusedCapacity must not be called while it is in use.
Example
var it = try composer.render_stream(.{});
defer it.deinit();
while (it.next()) |block| {
// `block` is a []const T valid until the next call to `next`
consume(block);
}const std = @import("std");
const lightmix = @import("lightmix");
const Wave = lightmix.Wave;
const Composer = lightmix.Composer;
pub fn main() !void {
const allocator = std.heap.page_allocator;
// Helper: generate a sine wave at a given frequency
const gen = struct {
fn sineWave(freq: f64, alloc: std.mem.Allocator) !Wave(f64) {
var samples: [44100]f64 = undefined;
for (0..samples.len) |i| {
const t = @as(f64, @floatFromInt(i)) / 44100.0;
samples[i] = 0.3 * @sin(freq * 2.0 * std.math.pi * t);
}
return Wave(f64).init(samples[0..], alloc, .{ .sample_rate = 44100, .channels = 1 });
}
};
const note_c = try gen.sineWave(261.63, allocator); // C4
defer note_c.deinit();
const note_e = try gen.sineWave(329.63, allocator); // E4
defer note_e.deinit();
const note_g = try gen.sineWave(392.00, allocator); // G4
defer note_g.deinit();
var composer = try Composer(f64).init(allocator, .{ .sample_rate = 44100, .channels = 1 });
defer composer.deinit();
// Sequence: C → E → G
try composer.append(.{ .wave = note_c, .start_point = 0 });
try composer.append(.{ .wave = note_e, .start_point = 44100 });
try composer.append(.{ .wave = note_g, .start_point = 88200 });
const result = try composer.finalize(.{});
defer result.deinit();
const file = try std.fs.cwd().createFile("sequence.wav", .{});
defer file.close();
const buf = try allocator.alloc(u8, 10 * 1024 * 1024);
defer allocator.free(buf);
var writer = file.writer(buf);
try result.write(.wav, &writer.interface, .{ .bits = 16, .format_code = .pcm });
try writer.interface.flush();
}- Wave-API — Creating and manipulating individual waveforms
- Build-Helpers — Generating WAV files at build time