// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Plays nothing. A stream here has no thread: the test drives it with `pull`, //! which does what a sound server's cycle does, so everything a Player does //! can be checked without a sound device. const std = @import("std"); const options_mod = @import("options.zig"); const StreamOptions = options_mod.StreamOptions; pub const Error = std.mem.Allocator.Error || error{OutputUnavailable}; /// Set to make the next `open` fail, as a missing sound server would. pub var fail_next_open: bool = false; pub const Stream = struct { gpa: std.mem.Allocator, process: options_mod.Process, name: []u8, rate: u32, channels: u16, active: bool = true, /// One cycle of `frames`, interleaved: the callback's output if the stream /// is running, silence if it is paused. The caller frees it. pub fn pull(s: *Stream, frames: u32) std.mem.Allocator.Error![]f32 { const planes = try s.gpa.alloc([]f32, s.channels); defer s.gpa.free(planes); const flat = try s.gpa.alloc(f32, @as(usize, frames) * s.channels); defer s.gpa.free(flat); for (planes, 0..) |*p, c| p.* = flat[c * frames ..][0..frames]; const out = try s.gpa.alloc(f32, flat.len); if (!s.active) { @memset(out, 0); return out; } // What a real backend's previous cycle would have left behind. @memset(flat, std.math.nan(f32)); s.process.func(s.process.ctx, planes, frames); for (0..frames) |i| { for (planes, 0..) |p, c| out[i * s.channels + c] = p[i]; } return out; } }; pub fn open(gpa: std.mem.Allocator, io: std.Io, options: StreamOptions) Error!*Stream { _ = io; if (fail_next_open) { fail_next_open = false; return error.OutputUnavailable; } const s = try gpa.create(Stream); errdefer gpa.destroy(s); s.* = .{ .gpa = gpa, .process = options.process, .name = try gpa.dupe(u8, options.name), .rate = options.rate, .channels = options.channels, }; return s; } pub fn close(s: *Stream, io: std.Io) void { _ = io; s.gpa.free(s.name); s.gpa.destroy(s); } pub fn pause(s: *Stream) void { s.active = false; } pub fn unpause(s: *Stream) void { s.active = true; }