diff --git a/examples/piano/App.zig b/examples/piano/App.zig index ed835fc8..15c52350 100644 --- a/examples/piano/App.zig +++ b/examples/piano/App.zig @@ -23,14 +23,11 @@ var gpa = std.heap.GeneralPurposeAllocator(.{}){}; pub const name = .app; pub const Mod = mach.Mod(@This()); -pub const global_events = .{ - .audio_state_change = .{ .handler = audioStateChange }, -}; - pub const local_events = .{ .init = .{ .handler = init }, .deinit = .{ .handler = deinit }, .tick = .{ .handler = tick }, + .audio_state_change = .{ .handler = audioStateChange }, }; pub const components = .{ @@ -39,12 +36,13 @@ pub const components = .{ ghost_key_mode: bool = false, -fn init(audio: *mach.Audio.Mod, piano: *Mod) void { - // Initialize audio module - audio.send(.init, .{}); +fn init(audio: *mach.Audio.Mod, app: *Mod) void { + // Initialize audio module, telling it to send our module's .audio_state_change event when an + // entity's sound stops playing + audio.send(.init, .{app.event(.audio_state_change)}); // Initialize piano module state - piano.init(.{}); + app.init(.{}); std.debug.print("controls:\n", .{}); std.debug.print("[typing] Play piano noises\n", .{}); @@ -60,39 +58,56 @@ fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void { fn audioStateChange( audio: *mach.Audio.Mod, - which: mach.EntityID, - piano: *Mod, + app: *Mod, ) !void { - if (audio.get(which, .playing) == false) { - if (piano.get(which, .play_after)) |frequency| { - // Play a new sound - const entity = try audio.newEntity(); - try audio.set(entity, .samples, try fillTone(audio, frequency)); - try audio.set(entity, .playing, true); - try audio.set(entity, .index, 0); - } + // Find audio entities that are no longer playing + var archetypes_iter = audio.entities.query(.{ .all = &.{ + .{ .mach_audio = &.{.playing} }, + } }); + while (archetypes_iter.next()) |archetype| { + for ( + archetype.slice(.entity, .id), + archetype.slice(.mach_audio, .playing), + ) |id, playing| { + if (playing) continue; - // Remove the entity for the old sound - try audio.removeEntity(which); + if (app.get(id, .play_after)) |frequency| { + // Play a new sound + const entity = try audio.newEntity(); + try audio.set(entity, .samples, try fillTone(audio, frequency)); + try audio.set(entity, .playing, true); + try audio.set(entity, .index, 0); + } + + // Remove the entity for the old sound + try audio.removeEntity(id); + } } } fn tick( core: *mach.Core.Mod, audio: *mach.Audio.Mod, - piano: *Mod, + app: *Mod, ) !void { // TODO(Core) var iter = mach.core.pollEvents(); while (iter.next()) |event| { switch (event) { .key_press => |ev| { - const vol = try audio.state().player.volume(); switch (ev.key) { // Controls - .space => piano.state().ghost_key_mode = !piano.state().ghost_key_mode, - .down => try audio.state().player.setVolume(@max(0.0, vol - 0.1)), - .up => try audio.state().player.setVolume(@min(1.0, vol + 0.1)), + .space => app.state().ghost_key_mode = !app.state().ghost_key_mode, + .down => { + const vol = math.clamp(try audio.state().player.volume() - 0.1, 0, 1); + try audio.state().player.setVolume(vol); + std.debug.print("[volume] {d:.0}%\n", .{vol * 100.0}); + }, + .up => { + const vol = math.clamp(try audio.state().player.volume() + 0.1, 0, 1); + try audio.state().player.setVolume(vol); + std.debug.print("[volume] {d:.0}%\n", .{vol * 100.0}); + }, // Piano keys else => { @@ -102,10 +117,10 @@ fn tick( try audio.set(entity, .playing, true); try audio.set(entity, .index, 0); - if (piano.state().ghost_key_mode) { + if (app.state().ghost_key_mode) { // After that sound plays, we'll chain on another sound that is one semi-tone higher. const one_semi_tone_higher = keyToFrequency(ev.key) * math.pow(f32, 2.0, (1.0 / 12.0)); - try piano.set(entity, .play_after, one_semi_tone_higher); + try app.set(entity, .play_after, one_semi_tone_higher); } }, } diff --git a/src/Audio.zig b/src/Audio.zig index 2732af19..1cb7f5c8 100644 --- a/src/Audio.zig +++ b/src/Audio.zig @@ -18,10 +18,6 @@ pub const local_events = .{ .audio_tick = .{ .handler = audioTick }, }; -pub const global_events = .{ - .audio_state_change = .{ .handler = fn (mach.EntityID) void }, -}; - const log = std.log.scoped(name); // The number of milliseconds worth of audio to render ahead of time. The lower this number is, the @@ -35,6 +31,7 @@ ms_render_ahead: f32 = 16, allocator: std.mem.Allocator, ctx: sysaudio.Context, player: sysaudio.Player, +on_state_change: mach.AnyEvent, output_mu: std.Thread.Mutex = .{}, output: SampleBuffer, mixing_buffer: ?std.ArrayListUnmanaged(f32) = null, @@ -45,7 +42,7 @@ var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const SampleBuffer = std.fifo.LinearFifo(u8, .Dynamic); -fn init(audio: *Mod) !void { +fn init(audio: *Mod, on_state_change: mach.AnyEvent) !void { const allocator = gpa.allocator(); const ctx = try sysaudio.Context.init(null, allocator, .{}); try ctx.refresh(); @@ -71,6 +68,7 @@ fn init(audio: *Mod) !void { .allocator = allocator, .ctx = ctx, .player = player, + .on_state_change = on_state_change, .output = SampleBuffer.init(allocator), .debug = debug, }); @@ -133,6 +131,7 @@ fn audioTick(audio: *Mod) !void { // not undefined memory. @memset(mixing_buffer.items, 0); + var did_state_change = false; var max_samples: usize = 0; var archetypes_iter = audio.entities.query(.{ .all = &.{ .{ .mach_audio = &.{ .samples, .playing, .index } }, @@ -151,7 +150,7 @@ fn audioTick(audio: *Mod) !void { max_samples = @max(max_samples, to_read); if (index + to_read >= samples.len) { // No longer playing, we've read all samples - audio.sendGlobal(.audio_state_change, .{id}); + did_state_change = true; try audio.set(id, .playing, false); try audio.set(id, .index, 0); continue; @@ -159,6 +158,7 @@ fn audioTick(audio: *Mod) !void { try audio.set(id, .index, index + to_read); } } + if (did_state_change) audio.sendAnyEvent(audio.state().on_state_change); // Write our rendered samples to the fifo, expanding its size as needed and converting our f32 // samples to the format the driver expects. diff --git a/src/main.zig b/src/main.zig index a9869c79..fb270cc0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,6 +32,10 @@ pub const Mod = ModSet(modules).Mod; pub const EntityID = @import("module/main.zig").EntityID; // TODO: rename to just Entity? pub const Archetype = @import("module/main.zig").Archetype; +pub const ModuleID = @import("module/main.zig").ModuleID; +pub const EventID = @import("module/main.zig").EventID; +pub const AnyEvent = @import("module/main.zig").AnyEvent; + /// To use experimental sysgpu graphics API, you can write this in your main.zig: /// /// ``` diff --git a/src/module/main.zig b/src/module/main.zig index 7d963671..4ea7fe72 100644 --- a/src/module/main.zig +++ b/src/module/main.zig @@ -7,6 +7,9 @@ pub const Entities = @import("entities.zig").Entities; pub const Archetype = @import("Archetype.zig"); pub const ModSet = @import("module.zig").ModSet; pub const Modules = @import("module.zig").Modules; +pub const ModuleID = @import("module.zig").ModuleID; +pub const EventID = @import("module.zig").EventID; +pub const AnyEvent = @import("module.zig").AnyEvent; test { std.testing.refAllDeclsRecursive(@This()); diff --git a/src/module/module.zig b/src/module/module.zig index 14326fff..46c63a4f 100644 --- a/src/module/module.zig +++ b/src/module/module.zig @@ -93,16 +93,21 @@ fn Serializable(comptime T: type) type { return T; } +// TODO: add runtime module support +pub const ModuleID = u32; +pub const EventID = u32; + +pub const AnyEvent = struct { + module_id: ModuleID, + event_id: EventID, +}; + /// Manages comptime .{A, B, C} modules and runtime modules. pub fn Modules(comptime modules: anytype) type { // Verify that each module is valid. inline for (modules) |M| _ = ModuleInterface(M); return struct { - // TODO: add runtime module support - pub const ModuleID = u32; - pub const EventID = u32; - pub const GlobalEvent = GlobalEventEnum(modules); pub const LocalEvent = LocalEventEnum(modules); @@ -593,6 +598,28 @@ pub fn ModSet(comptime modules: anytype) type { const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr); mods.sendGlobal(module_tag, event_name, args); } + + pub inline fn event(_: *@This(), comptime event_name: LocalEventEnumM(M)) AnyEvent { + const module_name_g: ModuleName(modules) = M.name; + const event_name_g: Modules(modules).LocalEvent = comptime Modules(modules).moduleToGlobalEvent( + M, + LocalEventEnumM, + LocalEventEnum, + event_name, + ); + return .{ + .module_id = @intFromEnum(module_name_g), + .event_id = @intFromEnum(event_name_g), + }; + } + + pub inline fn sendAnyEvent(m: *@This(), ev: AnyEvent) void { + const ModulesT = Modules(modules); + const MByName = ModsByName(modules); + const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); + const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr); + mods.sendDynamic(ev.module_id, ev.event_id, .{}); + } }; } };