audio: update Audio module to new object system

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-12-01 13:25:55 -07:00 committed by Emi Gutekanst
parent 281884e9b0
commit 314abeb988
4 changed files with 210 additions and 223 deletions

View file

@ -77,7 +77,7 @@ pub fn build(b: *std.Build) !void {
// .{ .name = "glyphs", .deps = &.{ .assets, .freetype } }, // .{ .name = "glyphs", .deps = &.{ .assets, .freetype } },
// .{ .name = "hardware-check", .deps = &.{ .assets, .zigimg } }, // .{ .name = "hardware-check", .deps = &.{ .assets, .zigimg } },
// .{ .name = "piano", .deps = &.{} }, // .{ .name = "piano", .deps = &.{} },
// .{ .name = "play-opus", .deps = &.{.assets} }, .{ .name = "play-opus", .deps = &.{.assets} },
// .{ .name = "sprite", .deps = &.{ .zigimg, .assets } }, // .{ .name = "sprite", .deps = &.{ .zigimg, .assets } },
// .{ .name = "text", .deps = &.{.assets} }, // .{ .name = "text", .deps = &.{.assets} },
}; };

View file

@ -1,6 +1,7 @@
/// Load two opus sound files: /// Loads and plays opus sound files.
/// - One long ~3 minute sound file (BGM/Background music) that plays on repeat ///
/// - One short sound file (SFX/Sound effect) that plays when you press a key /// Plays a long background music sound file that plays on repeat, and a short sound effect that
/// plays when pressing keys.
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
@ -12,61 +13,68 @@ const sysaudio = mach.sysaudio;
pub const App = @This(); pub const App = @This();
// TODO: banish global allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub const mach_module = .app; pub const mach_module = .app;
pub const mach_systems = .{ .start, .init, .deinit, .tick, .audio_state_change }; pub const mach_systems = .{ .main, .init, .tick, .deinit, .audioStateChange };
// TODO(object) pub const main = mach.schedule(.{
pub const components = .{ .{ mach.Core, .init },
.is_bgm = .{ .type = void }, .{ mach.Audio, .init },
}; .{ App, .init },
.{ mach.Core, .main },
});
pub const deinit = mach.schedule(.{
.{ mach.Audio, .deinit },
});
/// Tag object we set as a child of mach.Audio objects to indicate they are background music.
// TODO(object): consider adding a better object 'tagging' system?
bgm: mach.Objects(.{}, struct {}),
sfx: mach.Audio.Opus, sfx: mach.Audio.Opus,
fn start( pub fn init(
core: *mach.Core,
audio: *mach.Audio,
app: *App,
) !void {
core.schedule(.init);
audio.schedule(.init);
app.schedule(.init);
}
fn init(
entities: *mach.Entities.Mod,
core: *mach.Core, core: *mach.Core,
audio: *mach.Audio, audio: *mach.Audio,
app: *App, app: *App,
app_mod: mach.Mod(App), app_mod: mach.Mod(App),
) !void { ) !void {
// TODO(allocator): find a better way to get an allocator here
const allocator = std.heap.c_allocator;
core.on_tick = app_mod.id.tick; core.on_tick = app_mod.id.tick;
core.on_exit = app_mod.id.deinit; core.on_exit = app_mod.id.deinit;
// Configure the audio module to send our app's .audio_state_change event when an entity's sound // Configure the audio module to send our app's .audio_state_change event when an entity's sound
// finishes playing. // finishes playing.
audio.on_state_change = app_audio_state_change.id; audio.on_state_change = app_mod.id.audioStateChange;
const bgm_fbs = std.io.fixedBufferStream(assets.bgm.bit_bit_loop); const bgm_fbs = std.io.fixedBufferStream(assets.bgm.bit_bit_loop);
const bgm_sound_stream = std.io.StreamSource{ .const_buffer = bgm_fbs }; const bgm_sound_stream = std.io.StreamSource{ .const_buffer = bgm_fbs };
const bgm = try mach.Audio.Opus.decodeStream(gpa.allocator(), bgm_sound_stream); const bgm = try mach.Audio.Opus.decodeStream(allocator, bgm_sound_stream);
// TODO(object): bgm here is not freed inside of deinit(), if we had object-scoped allocators we
// could do this more nicely in real applications
const sfx_fbs = std.io.fixedBufferStream(assets.sfx.sword1); const sfx_fbs = std.io.fixedBufferStream(assets.sfx.sword1);
const sfx_sound_stream = std.io.StreamSource{ .const_buffer = sfx_fbs }; const sfx_sound_stream = std.io.StreamSource{ .const_buffer = sfx_fbs };
const sfx = try mach.Audio.Opus.decodeStream(gpa.allocator(), sfx_sound_stream); const sfx = try mach.Audio.Opus.decodeStream(allocator, sfx_sound_stream);
// Initialize module state // Initialize module state
app.init(.{ .sfx = sfx }); app.* = .{ .sfx = sfx, .bgm = app.bgm };
const bgm_entity = try entities.new(); const bgm_buffer = blk: {
try app.set(bgm_entity, .is_bgm, {}); audio.buffers.lock();
try audio.set(bgm_entity, .samples, bgm.samples); defer audio.buffers.unlock();
try audio.set(bgm_entity, .channels, bgm.channels);
try audio.set(bgm_entity, .playing, true); break :blk try audio.buffers.new(.{
try audio.set(bgm_entity, .index, 0); .samples = bgm.samples,
.channels = bgm.channels,
});
};
const bgm_obj = try app.bgm.new(.{});
try app.bgm.setParent(bgm_obj, bgm_buffer);
std.debug.print("controls:\n", .{}); std.debug.print("controls:\n", .{});
std.debug.print("[typing] Play SFX\n", .{}); std.debug.print("[typing] Play SFX\n", .{});
@ -74,38 +82,25 @@ fn init(
std.debug.print("[arrow down] decrease volume 10%\n", .{}); std.debug.print("[arrow down] decrease volume 10%\n", .{});
} }
fn deinit(audio: *mach.Audio) void { pub fn audioStateChange(audio: *mach.Audio, app: *App) !void {
audio.schedule(.deinit); audio.buffers.lock();
} defer audio.buffers.unlock();
fn audioStateChange(
entities: *mach.Entities.Mod,
audio: *mach.Audio,
app: *App,
) !void {
// Find audio entities that are no longer playing // Find audio entities that are no longer playing
var q = try entities.query(.{ var buffers = audio.buffers.slice();
.ids = mach.Entities.Mod.read(.id), while (buffers.next()) |buf_id| {
.playings = mach.Audio.read(.playing), if (audio.buffers.get(buf_id, .playing)) continue;
});
while (q.next()) |v| {
for (v.ids, v.playings) |id, playing| {
if (playing) continue;
if (app.get(id, .is_bgm)) |_| { // If the buffer has a bgm object as a child, then we consider it background music
// Repeat background music if (try app.bgm.getFirstChildOfType(buf_id)) |_| {
try audio.set(id, .index, 0); // Repeat background music forever
try audio.set(id, .playing, true); audio.buffers.set(buf_id, .index, 0);
} else { audio.buffers.set(buf_id, .playing, true);
// Remove the entity for the old sound } else audio.buffers.delete(buf_id);
try entities.remove(id);
}
}
} }
} }
fn tick( pub fn tick(
entities: *mach.Entities.Mod,
core: *mach.Core, core: *mach.Core,
audio: *mach.Audio, audio: *mach.Audio,
app: *App, app: *App,
@ -125,11 +120,13 @@ fn tick(
}, },
else => { else => {
// Play a new SFX // Play a new SFX
const e = try entities.new(); audio.buffers.lock();
try audio.set(e, .samples, app.sfx.samples); defer audio.buffers.unlock();
try audio.set(e, .channels, app.sfx.channels);
try audio.set(e, .index, 0); _ = try audio.buffers.new(.{
try audio.set(e, .playing, true); .samples = app.sfx.samples,
.channels = app.sfx.channels,
});
}, },
}, },
.close => core.exit(), .close => core.exit(),
@ -137,14 +134,16 @@ fn tick(
} }
} }
var main_window = core.windows.getValue(core.main_window);
// Grab the back buffer of the swapchain // Grab the back buffer of the swapchain
// TODO(Core) // TODO(Core)
const back_buffer_view = core.swap_chain.getCurrentTextureView().?; const back_buffer_view = main_window.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release(); defer back_buffer_view.release();
// Create a command encoder // Create a command encoder
const label = @tagName(mach_module) ++ ".tick"; const label = @tagName(mach_module) ++ ".tick";
const encoder = core.device.createCommandEncoder(&.{ .label = label }); const encoder = main_window.device.createCommandEncoder(&.{ .label = label });
defer encoder.release(); defer encoder.release();
// Begin render pass // Begin render pass
@ -169,5 +168,5 @@ fn tick(
// Submit our commands to the queue // Submit our commands to the queue
var command = encoder.finish(&.{ .label = label }); var command = encoder.finish(&.{ .label = label });
defer command.release(); defer command.release();
core.queue.submit(&[_]*gpu.CommandBuffer{command}); main_window.queue.submit(&[_]*gpu.CommandBuffer{command});
} }

View file

@ -5,22 +5,11 @@ const sysaudio = mach.sysaudio;
pub const Opus = @import("mach-opus"); pub const Opus = @import("mach-opus");
const Audio = @This();
pub const mach_module = .mach_audio; pub const mach_module = .mach_audio;
// TODO(object) pub const mach_systems = .{ .init, .tick, .deinit };
pub const components = .{
.samples = .{ .type = []const f32 },
.channels = .{ .type = u8 },
.volume = .{ .type = f32 },
.playing = .{ .type = bool },
.index = .{ .type = usize },
};
pub const systems = .{
.init = .{ .handler = init },
.deinit = .{ .handler = deinit },
.audio_tick = .{ .handler = audioTick },
};
const log = std.log.scoped(mach_module); const log = std.log.scoped(mach_module);
@ -32,24 +21,50 @@ const log = std.log.scoped(mach_module);
// stop playing smoothly assuming a 60hz application render rate. // stop playing smoothly assuming a 60hz application render rate.
ms_render_ahead: f32 = 16, ms_render_ahead: f32 = 16,
buffers: mach.Objects(
.{},
struct {
/// The actual audio samples
samples: []const f32,
/// The number of channels in the samples buffer
channels: u8,
/// Volume multiplier
volume: f32 = 1.0,
/// Whether the buffer should be playing currently
playing: bool = true,
/// The currently playhead of the samples
index: usize = 0,
},
),
/// Whether to debug audio sync issues
debug: bool = false,
/// Callback which is ran when buffers change state from playing -> not playing
on_state_change: ?mach.FunctionID = null,
/// Audio player (has global volume controls)
player: sysaudio.Player,
// Internal fields
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
ctx: sysaudio.Context, ctx: sysaudio.Context,
player: sysaudio.Player,
on_state_change: ?mach.AnySystem = null,
output_mu: std.Thread.Mutex = .{},
output: SampleBuffer, output: SampleBuffer,
mixing_buffer: ?std.ArrayListUnmanaged(f32) = null, mixing_buffer: ?std.ArrayListUnmanaged(f32) = null,
render_num_samples: usize = 0, shutdown: std.atomic.Value(bool) = .init(false),
debug: bool = false, mod: mach.Mod(Audio),
running_mu: std.Thread.Mutex = .{}, driver_needs_num_samples: usize = 0,
running: bool = true,
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const SampleBuffer = std.fifo.LinearFifo(u8, .Dynamic); const SampleBuffer = std.fifo.LinearFifo(u8, .Dynamic);
fn init(audio: *Mod) !void { pub fn init(audio: *Audio, audio_mod: mach.Mod(Audio)) !void {
const allocator = gpa.allocator(); // TODO(allocator): find a better way for modules to get allocators
const allocator = std.heap.c_allocator;
const ctx = try sysaudio.Context.init(null, allocator, .{}); const ctx = try sysaudio.Context.init(null, allocator, .{});
try ctx.refresh(); try ctx.refresh();
@ -71,128 +86,108 @@ fn init(audio: *Mod) !void {
break :blk std.ascii.eqlIgnoreCase(s, "true"); break :blk std.ascii.eqlIgnoreCase(s, "true");
} else false; } else false;
audio.init(.{ audio.* = .{
.buffers = audio.buffers,
.allocator = allocator, .allocator = allocator,
.ctx = ctx, .ctx = ctx,
.player = player, .player = player,
.output = SampleBuffer.init(allocator), .output = SampleBuffer.init(allocator),
.debug = debug, .debug = debug,
}); .mod = audio_mod,
};
try player.start(); try player.start();
} }
fn deinit(audio: *Mod) void { pub fn deinit(audio: *Audio) void {
audio.state().running_mu.lock(); audio.shutdown.store(true, .release);
defer audio.state().running_mu.unlock(); audio.player.deinit();
audio.state().running = false; audio.ctx.deinit();
if (audio.mixing_buffer) |*b| b.deinit(audio.allocator);
// TODO: make sure this doesn't hang forever
// audio.state().player.deinit();
audio.state().ctx.deinit();
if (audio.state().mixing_buffer) |*b| b.deinit(audio.state().allocator);
} }
/// .audio_tick is sent whenever the audio driver requests more audio samples to output to the /// Audio.tick is called on the high-priority OS audio thread when the audio driver is waiting for
/// speakers. Usually the driver is requesting a small amount of samples, e.g. ~4096 samples. /// more audio samples because the audio.output buffer does not currently have enough to satisfy the
/// driver.
/// ///
/// The audio driver asks for more samples on a different, often high-priority OS thread. It does /// Its goal is to fill the audio.output buffer with enough samples to satisfy the immediate
/// not block waiting for .audio_tick to be dispatched, instead it simply returns whatever samples /// requirements of the audio driver (audio.driver_needs_num_samples), and prepare some amount of
/// are already prepared in the audio.state().output buffer ahead of time. This ensures that even /// additional samples ahead of time to satisfy the driver in the future.
/// if the system is under heavy load, or a few frames are particularly slow, that audio pub fn tick(audio: *Audio, audio_mod: mach.Mod(Audio)) !void {
/// (hopefully) continues playing uninterrupted. // If the other thread called deinit(), return.
/// if (audio.shutdown.load(.acquire)) {
/// The goal of this event handler, then, is to prepare enough audio samples ahead of time in the return;
/// audio.state().output buffer that feed the driver so it does not get hungry and play silence }
/// instead. At the same time, we don't want to play too far ahead as that would cause latency
/// between e.g. user interactions and audio actually playing - so in practice the amount we play
/// ahead is rather small and imperceivable to most humans.
fn audioTick(entities: *mach.Entities.Mod, audio: *Mod) !void {
audio.state().running_mu.lock();
const running = audio.state().running;
const driver_expects = audio.state().render_num_samples; // How many samples the driver last expected us to produce.
audio.state().running_mu.unlock();
if (!running) return; // Scheduled by the other thread e.g. right before .deinit, ignore it.
const allocator = audio.state().allocator; const allocator = audio.allocator;
const player = &audio.state().player; const player = &audio.player;
const player_channels: u8 = @intCast(player.channels().len); const player_channels: u8 = @intCast(player.channels().len);
const driver_needs = audio.driver_needs_num_samples;
// How many audio samples we will render ahead by // How many audio samples we will render ahead by
const samples_per_ms = @as(f32, @floatFromInt(player.sampleRate())) / 1000.0; const samples_per_ms = @as(f32, @floatFromInt(player.sampleRate())) / 1000.0;
const render_ahead: u32 = @as(u32, @intFromFloat(@trunc(audio.state().ms_render_ahead * samples_per_ms))) * player_channels; const render_ahead: u32 = @as(u32, @intFromFloat(@trunc(audio.ms_render_ahead * samples_per_ms))) * player_channels;
// Our goal is to ensure that we always have pre-rendered the number of samples the driver last // Our goal is to satisfy the driver's immediate needs, plus prepare render_head number of samples.
// expected, expects, plus the play ahead amount. const goal_pre_rendered = driver_needs + render_ahead;
const goal_pre_rendered = driver_expects + render_ahead;
audio.state().output_mu.lock(); const already_prepared = audio.output.readableLength() / player.format().size();
const already_prepared = audio.state().output.readableLength() / player.format().size();
const render_num_samples = if (already_prepared > goal_pre_rendered) 0 else goal_pre_rendered - already_prepared; const render_num_samples = if (already_prepared > goal_pre_rendered) 0 else goal_pre_rendered - already_prepared;
audio.state().output_mu.unlock(); if (render_num_samples < 0) @panic("invariant: Audio.tick ran when more audio samples are not needed");
if (render_num_samples < 0) return; // we do not need to render more audio right now
// Ensure our f32 mixing buffer has enough space for the samples we will render right now. // Ensure our f32 mixing buffer has enough space for the samples we will render right now.
// This will allocate to grow but never shrink. // This will allocate to grow but never shrink.
var mixing_buffer = if (audio.state().mixing_buffer) |*b| b else blk: { var mixing_buffer = if (audio.mixing_buffer) |*b| b else blk: {
const b = try std.ArrayListUnmanaged(f32).initCapacity(allocator, render_num_samples); const b = try std.ArrayListUnmanaged(f32).initCapacity(allocator, render_num_samples);
audio.state().mixing_buffer = b; audio.mixing_buffer = b;
break :blk &audio.state().mixing_buffer.?; break :blk &audio.mixing_buffer.?;
}; };
try mixing_buffer.resize(allocator, render_num_samples); // grows, but never shrinks try mixing_buffer.resize(allocator, render_num_samples); // grows, but never shrinks
// Zero the mixing buffer to silence: if no audio is mixed in below, then we want silence // Zero the mixing buffer to silence: if no audio is mixed in below, then we want silence
// not undefined memory. // not undefined memory noise.
@memset(mixing_buffer.items, 0); @memset(mixing_buffer.items, 0);
var did_state_change = false; var did_state_change = false;
var q = try entities.query(.{ {
.ids = mach.Entities.Mod.read(.id), audio.buffers.lock();
.samples_slices = Mod.read(.samples), defer audio.buffers.unlock();
.channels = Mod.read(.channels),
.playings = Mod.write(.playing),
.indexes = Mod.write(.index),
});
while (q.next()) |v| {
for (v.ids, v.samples_slices, v.channels, v.playings, v.indexes) |id, samples, channels, *playing, *index| {
if (!playing.*) continue;
const volume = audio.get(id, .volume) orelse 1.0; var buffers = audio.buffers.slice();
const channels_diff = player_channels - channels + 1; while (buffers.next()) |buf_id| {
const to_read = (@min(samples.len - index.*, mixing_buffer.items.len) / channels_diff) + @rem(@min(samples.len - index.*, mixing_buffer.items.len), channels_diff); var buffer = buffers.get(buf_id);
if (channels == 1 and player_channels > 1) { if (!buffer.playing) continue;
const channels_diff = player_channels - buffer.channels + 1;
const to_read = (@min(buffer.samples.len - buffer.index, mixing_buffer.items.len) / channels_diff) + @rem(@min(buffer.samples.len - buffer.index, mixing_buffer.items.len), channels_diff);
if (buffer.channels == 1 and player_channels > 1) {
// Duplicate samples for mono sounds // Duplicate samples for mono sounds
var i: usize = 0; var i: usize = 0;
for (samples[index.*..][0..to_read]) |sample| { for (buffer.samples[buffer.index..][0..to_read]) |sample| {
mixSamplesDuplicate(mixing_buffer.items[i..][0..player_channels], sample * volume); mixSamplesDuplicate(mixing_buffer.items[i..][0..player_channels], sample * buffer.volume);
i += player_channels; i += player_channels;
} }
} else { } else {
mixSamples(mixing_buffer.items[0..to_read], samples[index.*..][0..to_read], volume); mixSamples(mixing_buffer.items[0..to_read], buffer.samples[buffer.index..][0..to_read], buffer.volume);
} }
if (index.* + to_read >= samples.len) { if (buffer.index + to_read >= buffer.samples.len) {
// No longer playing, we've read all samples // No longer playing, we've read all samples
did_state_change = true; did_state_change = true;
playing.* = false; buffer.playing = false;
index.* = 0; buffer.index = 0;
continue; } else buffer.index = buffer.index + to_read;
}
index.* = index.* + to_read; // Save changes to the buffer object
buffers.set(buf_id, buffer);
} }
} }
if (audio.state().on_state_change) |on_state_change_event| { if (did_state_change) if (audio.on_state_change) |f| audio_mod.run(f);
if (did_state_change) audio.scheduleAny(on_state_change_event);
}
// Write our rendered samples to the fifo, expanding its size as needed and converting our f32 // Write our rendered samples to the fifo, expanding its size as needed and converting our f32
// samples to the format the driver expects. // samples to the format the driver expects.
// TODO(audio): handle potential OOM here
audio.state().output_mu.lock();
defer audio.state().output_mu.unlock();
const out_buffer_len = render_num_samples * player.format().size(); const out_buffer_len = render_num_samples * player.format().size();
const out_buffer = try audio.state().output.writableWithSize(out_buffer_len); const out_buffer = try audio.output.writableWithSize(out_buffer_len); // TODO(audio): handle potential OOM here better
std.debug.assert(mixing_buffer.items.len == render_num_samples); std.debug.assert(mixing_buffer.items.len == render_num_samples);
sysaudio.convertTo( sysaudio.convertTo(
f32, f32,
@ -200,71 +195,66 @@ fn audioTick(entities: *mach.Entities.Mod, audio: *Mod) !void {
player.format(), player.format(),
out_buffer[0..out_buffer_len], // writableWithSize may return a larger slice than needed out_buffer[0..out_buffer_len], // writableWithSize may return a larger slice than needed
); );
audio.state().output.update(out_buffer_len); audio.output.update(out_buffer_len);
} }
// Callback invoked on the audio thread /// Called by the system audio driver when the output buffer needs to be filled. Called on a
/// dedicated OS thread for high-priority audio. Its goal is to fill the output buffer as quickly
/// as possible and return, else audio skips will occur.
fn writeFn(audio_opaque: ?*anyopaque, output: []u8) void { fn writeFn(audio_opaque: ?*anyopaque, output: []u8) void {
const audio: *Mod = @ptrCast(@alignCast(audio_opaque)); const audio: *Audio = @ptrCast(@alignCast(audio_opaque));
const format_size = audio.player.format().size();
// Make sure any audio.state() we access is covered by a mutex so it is not accessed during // If the other thread called deinit(), write zeros to the buffer (no sound) and return.
// .deinit in the main thread. if (audio.shutdown.load(.acquire)) {
audio.state().running_mu.lock();
const running = audio.state().running;
if (!running) {
audio.state().running_mu.unlock();
@memset(output, 0); @memset(output, 0);
return; return;
} }
const format_size = audio.state().player.format().size();
const render_num_samples = @divExact(output.len, format_size);
audio.state().render_num_samples = render_num_samples;
audio.state().running_mu.unlock(); // Do we have enough audio samples in our audio.output buffer to fill the output buffer?
// Notify that we are writing audio frames now
// //
// Note that we do not *wait* at all for .audio_tick to complete, this is an asynchronous // This is the most common case, because audio.output should have much more data prepared
// dispatch of the event. The expectation is that audio.state().output already has enough // ahead of time than what the audio driver needs.
// samples in it that we can return right now. The event is just a signal dispatched on another var read_slice = audio.output.readableSlice(0);
// thread to enable reacting to audio events in realtime. if (read_slice.len >= output.len) {
audio.schedule(.audio_tick); if (read_slice.len > output.len) read_slice = read_slice[0..output.len];
@memcpy(output[0..read_slice.len], read_slice);
audio.output.discard(read_slice.len);
return;
}
// Read the prepared audio samples and directly @memcpy them to the output buffer. // At this point, we don't have enough audio data prepared in our audio.output buffer. so we
audio.state().output_mu.lock(); // must prepare it now.
defer audio.state().output_mu.unlock(); while (true) {
var read_slice = audio.state().output.readableSlice(0); // Run the audio tick function, which should fill the audio.output buffer with more audio
if (read_slice.len < output.len) { // samples.
// We do not have enough audio data prepared. Busy-wait until we do, otherwise the audio audio.driver_needs_num_samples = @divExact(output.len, format_size);
// thread may become de-sync'd with the loop responsible for producing it. audio.mod.call(.tick);
audio.schedule(.audio_tick);
if (audio.state().debug) log.debug("resync, found {} samples but need {} (nano timestamp {})", .{ read_slice.len / format_size, output.len / format_size, std.time.nanoTimestamp() });
audio.state().output_mu.unlock(); // Check if we now have enough data in our audio.output buffer. If we do, then we're done.
l: while (true) { read_slice = audio.output.readableSlice(0);
audio.state().output_mu.lock(); if (read_slice.len >= output.len) {
if (audio.state().output.readableLength() >= output.len) { if (read_slice.len > output.len) read_slice = read_slice[0..output.len];
read_slice = audio.state().output.readableSlice(0); @memcpy(output[0..read_slice.len], read_slice);
break :l; audio.output.discard(read_slice.len);
} return;
audio.state().output_mu.unlock(); }
// Handle potential exit // The audio tick didn't produce enough data, this might indicate some subtle mismatch in
audio.state().running_mu.lock(); // the audio tick function not producing a multiple of the audio driver's actual buffer
if (!audio.state().running) { // size.
audio.state().running_mu.unlock(); if (audio.debug) log.debug("resync, found {} samples but need {} (nano timestamp {})", .{
@memset(output, 0); @divExact(read_slice.len, format_size),
return; @divExact(output.len, format_size),
} std.time.nanoTimestamp(),
audio.state().running_mu.unlock(); });
// If the other thread called deinit(), write zeros to the buffer (no sound) and return.
if (audio.shutdown.load(.acquire)) {
@memset(output, 0);
return;
} }
} }
if (read_slice.len > output.len) {
read_slice = read_slice[0..output.len];
}
@memcpy(output[0..read_slice.len], read_slice);
audio.state().output.discard(read_slice.len);
} }
// TODO(audio): remove this switch, currently ReleaseFast/ReleaseSmall have some weird behavior if // TODO(audio): remove this switch, currently ReleaseFast/ReleaseSmall have some weird behavior if

View file

@ -13,8 +13,7 @@ pub const Core = if (build_options.want_core) @import("Core.zig") else struct {}
pub const gamemode = if (builtin.os.tag != .linux or builtin.link_libc) @import("gamemode.zig"); pub const gamemode = if (builtin.os.tag != .linux or builtin.link_libc) @import("gamemode.zig");
// TODO(object) // TODO(object)
// pub const gfx = if (build_options.want_mach) @import("gfx/main.zig") else struct {}; // pub const gfx = if (build_options.want_mach) @import("gfx/main.zig") else struct {};
// TODO(object) pub const Audio = if (build_options.want_sysaudio) @import("Audio.zig") else struct {};
// pub const Audio = if (build_options.want_sysaudio) @import("Audio.zig") else struct {};
pub const math = @import("math/main.zig"); pub const math = @import("math/main.zig");
pub const testing = @import("testing.zig"); pub const testing = @import("testing.zig");
pub const time = @import("time/main.zig"); pub const time = @import("time/main.zig");
@ -46,8 +45,7 @@ test {
_ = sysgpu; _ = sysgpu;
// TODO(object) // TODO(object)
// _ = gfx; // _ = gfx;
// TODO(object) _ = Audio;
// _ = Audio;
_ = math; _ = math;
_ = testing; _ = testing;
_ = time; _ = time;