Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

various object system fixes; Audio module and play-opus working #1314

Merged
merged 11 commits into from
Dec 1, 2024
2 changes: 1 addition & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub fn build(b: *std.Build) !void {
// .{ .name = "glyphs", .deps = &.{ .assets, .freetype } },
// .{ .name = "hardware-check", .deps = &.{ .assets, .zigimg } },
// .{ .name = "piano", .deps = &.{} },
// .{ .name = "play-opus", .deps = &.{.assets} },
.{ .name = "play-opus", .deps = &.{.assets} },
// .{ .name = "sprite", .deps = &.{ .zigimg, .assets } },
// .{ .name = "text", .deps = &.{.assets} },
};
Expand Down
8 changes: 2 additions & 6 deletions examples/core-triangle/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,10 @@ pub fn tick(app: *App, core: *mach.Core) void {
.key_press => |ev| {
switch (ev.key) {
.right => {
var w = core.windows.getValue(core.main_window);
w.width = w.width + 10;
core.windows.setValue(core.main_window, w);
core.windows.set(core.main_window, .width, core.windows.get(core.main_window, .width) + 10);
},
.left => {
var w = core.windows.getValue(core.main_window);
w.width = w.width - 10;
core.windows.setValue(core.main_window, w);
core.windows.set(core.main_window, .width, core.windows.get(core.main_window, .width) - 10);
},
.up => {
core.windows.set(core.main_window, .height, core.windows.get(core.main_window, .height) + 10);
Expand Down
139 changes: 71 additions & 68 deletions examples/play-opus/App.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// Load two 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
/// Loads and plays opus sound files.
///
/// 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 builtin = @import("builtin");

Expand All @@ -12,100 +13,93 @@ const sysaudio = mach.sysaudio;

pub const App = @This();

// TODO: banish global allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};

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 components = .{
.is_bgm = .{ .type = void },
};
pub const main = mach.schedule(.{
.{ mach.Core, .init },
.{ mach.Audio, .init },
.{ App, .init },
.{ mach.Core, .main },
});

sfx: mach.Audio.Opus,
pub const deinit = mach.schedule(.{
.{ mach.Audio, .deinit },
});

fn start(
core: *mach.Core,
audio: *mach.Audio,
app: *App,
) !void {
core.schedule(.init);
audio.schedule(.init);
app.schedule(.init);
}
/// 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,

fn init(
entities: *mach.Entities.Mod,
pub fn init(
core: *mach.Core,
audio: *mach.Audio,
app: *App,
app_mod: mach.Mod(App),
) !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_exit = app_mod.id.deinit;

// Configure the audio module to send our app's .audio_state_change event when an entity's sound
// 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_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_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
app.init(.{ .sfx = sfx });
app.* = .{ .sfx = sfx, .bgm = app.bgm };

const bgm_buffer = blk: {
audio.buffers.lock();
defer audio.buffers.unlock();

const bgm_entity = try entities.new();
try app.set(bgm_entity, .is_bgm, {});
try audio.set(bgm_entity, .samples, bgm.samples);
try audio.set(bgm_entity, .channels, bgm.channels);
try audio.set(bgm_entity, .playing, true);
try audio.set(bgm_entity, .index, 0);
break :blk try audio.buffers.new(.{
.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("[typing] Play SFX\n", .{});
std.debug.print("[arrow up] increase volume 10%\n", .{});
std.debug.print("[arrow down] decrease volume 10%\n", .{});
}

fn deinit(audio: *mach.Audio) void {
audio.schedule(.deinit);
}
pub fn audioStateChange(audio: *mach.Audio, app: *App) !void {
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
var q = try entities.query(.{
.ids = mach.Entities.Mod.read(.id),
.playings = mach.Audio.read(.playing),
});
while (q.next()) |v| {
for (v.ids, v.playings) |id, playing| {
if (playing) continue;

if (app.get(id, .is_bgm)) |_| {
// Repeat background music
try audio.set(id, .index, 0);
try audio.set(id, .playing, true);
} else {
// Remove the entity for the old sound
try entities.remove(id);
}
}
var buffers = audio.buffers.slice();
while (buffers.next()) |buf_id| {
if (audio.buffers.get(buf_id, .playing)) continue;

// If the buffer has a bgm object as a child, then we consider it background music
if (try app.bgm.getFirstChildOfType(buf_id)) |_| {
// Repeat background music forever
audio.buffers.set(buf_id, .index, 0);
audio.buffers.set(buf_id, .playing, true);
} else audio.buffers.delete(buf_id);
}
}

fn tick(
entities: *mach.Entities.Mod,
pub fn tick(
core: *mach.Core,
audio: *mach.Audio,
app: *App,
Expand All @@ -125,26 +119,35 @@ fn tick(
},
else => {
// Play a new SFX
const e = try entities.new();
try audio.set(e, .samples, app.sfx.samples);
try audio.set(e, .channels, app.sfx.channels);
try audio.set(e, .index, 0);
try audio.set(e, .playing, true);
audio.buffers.lock();
defer audio.buffers.unlock();

_ = try audio.buffers.new(.{
.samples = app.sfx.samples,
.channels = app.sfx.channels,

// Start 0.15s into the sfx, which removes the silence at the start of the
// audio clip and makes it more apparent the low latency between pressing a
// key and sfx actually playing.
.index = @intFromFloat(@as(f32, @floatFromInt(audio.player.sampleRate() * app.sfx.channels)) * 0.15),
});
},
},
.close => core.exit(),
else => {},
}
}

var main_window = core.windows.getValue(core.main_window);

// Grab the back buffer of the swapchain
// TODO(Core)
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const back_buffer_view = main_window.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();

// Create a command encoder
const label = @tagName(mach_module) ++ ".tick";
const encoder = core.device.createCommandEncoder(&.{ .label = label });
const encoder = main_window.device.createCommandEncoder(&.{ .label = label });
defer encoder.release();

// Begin render pass
Expand All @@ -169,5 +172,5 @@ fn tick(
// Submit our commands to the queue
var command = encoder.finish(&.{ .label = label });
defer command.release();
core.queue.submit(&[_]*gpu.CommandBuffer{command});
main_window.queue.submit(&[_]*gpu.CommandBuffer{command});
}
Loading
Loading