audio: update Audio module to new object system
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
281884e9b0
commit
314abeb988
4 changed files with 210 additions and 223 deletions
|
|
@ -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} },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
290
src/Audio.zig
290
src/Audio.zig
|
|
@ -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);
|
||||||
// Read the prepared audio samples and directly @memcpy them to the output buffer.
|
audio.output.discard(read_slice.len);
|
||||||
audio.state().output_mu.lock();
|
return;
|
||||||
defer audio.state().output_mu.unlock();
|
|
||||||
var read_slice = audio.state().output.readableSlice(0);
|
|
||||||
if (read_slice.len < output.len) {
|
|
||||||
// We do not have enough audio data prepared. Busy-wait until we do, otherwise the audio
|
|
||||||
// thread may become de-sync'd with the loop responsible for producing it.
|
|
||||||
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();
|
|
||||||
l: while (true) {
|
|
||||||
audio.state().output_mu.lock();
|
|
||||||
if (audio.state().output.readableLength() >= output.len) {
|
|
||||||
read_slice = audio.state().output.readableSlice(0);
|
|
||||||
break :l;
|
|
||||||
}
|
}
|
||||||
audio.state().output_mu.unlock();
|
|
||||||
|
|
||||||
// Handle potential exit
|
// At this point, we don't have enough audio data prepared in our audio.output buffer. so we
|
||||||
audio.state().running_mu.lock();
|
// must prepare it now.
|
||||||
if (!audio.state().running) {
|
while (true) {
|
||||||
audio.state().running_mu.unlock();
|
// Run the audio tick function, which should fill the audio.output buffer with more audio
|
||||||
|
// samples.
|
||||||
|
audio.driver_needs_num_samples = @divExact(output.len, format_size);
|
||||||
|
audio.mod.call(.tick);
|
||||||
|
|
||||||
|
// Check if we now have enough data in our audio.output buffer. If we do, then we're done.
|
||||||
|
read_slice = audio.output.readableSlice(0);
|
||||||
|
if (read_slice.len >= output.len) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The audio tick didn't produce enough data, this might indicate some subtle mismatch in
|
||||||
|
// the audio tick function not producing a multiple of the audio driver's actual buffer
|
||||||
|
// size.
|
||||||
|
if (audio.debug) log.debug("resync, found {} samples but need {} (nano timestamp {})", .{
|
||||||
|
@divExact(read_slice.len, format_size),
|
||||||
|
@divExact(output.len, format_size),
|
||||||
|
std.time.nanoTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the other thread called deinit(), write zeros to the buffer (no sound) and return.
|
||||||
|
if (audio.shutdown.load(.acquire)) {
|
||||||
@memset(output, 0);
|
@memset(output, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
audio.state().running_mu.unlock();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue