diff --git a/build.zig b/build.zig index 1efb1157..bce4fcbf 100644 --- a/build.zig +++ b/build.zig @@ -554,6 +554,7 @@ fn buildExamples( ) !void { const Dependency = enum { assets, + opus, model3d, freetype, zigimg, @@ -569,6 +570,7 @@ fn buildExamples( .{ .name = "custom-renderer", .deps = &.{} }, .{ .name = "glyphs", .deps = &.{ .freetype, .assets } }, .{ .name = "piano", .deps = &.{} }, + .{ .name = "play-opus", .deps = &.{ .opus, .assets } }, .{ .name = "sprite", .deps = &.{ .zigimg, .assets } }, .{ .name = "text", .deps = &.{ .freetype, .assets } }, }) |example| { @@ -592,6 +594,12 @@ fn buildExamples( .optimize = optimize, })) |dep| exe.root_module.addImport("assets", dep.module("mach-example-assets")); }, + .opus => { + if (b.lazyDependency("mach_opus", .{ + .target = target, + .optimize = optimize, + })) |dep| exe.root_module.addImport("opus", dep.module("mach-opus")); + }, .model3d => { if (b.lazyDependency("mach_model3d", .{ .target = target, diff --git a/build.zig.zon b/build.zig.zon index bdc8740c..749c42cb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -92,9 +92,14 @@ .hash = "1220c1c16fa17783379f773c291af3693b84744200099f1cf5c1ddb66c4fe88bcf3a", .lazy = true, }, + .mach_opus = .{ + .url = "https://pkg.machengine.org/mach-opus/58a22201881d7639339f3c029d13a00bbfe005e2.tar.gz", + .hash = "1220821426e087874779f8d76a6bb74844aa3934648aad5b7331701e5f386791c51e", + .lazy = true, + }, .mach_example_assets = .{ - .url = "https://pkg.machengine.org/mach-example-assets/591715d872f4aa2d74e01447139c2000db0f7d96.tar.gz", - .hash = "12206ef714c72a3e934990e5fbe7160824c5bad0a917a42cdd5ba31f7c85142f18d6", + .url = "https://pkg.machengine.org/mach-example-assets/2200080af808e70fb20062c78c8e3e110605ac74.tar.gz", + .hash = "1220cb4b50aa4178b9b10a5e5628fc743abb2e3b264ce63c837c21b3b22a64dc3be2", .lazy = true, }, }, diff --git a/examples/play-opus/App.zig b/examples/play-opus/App.zig new file mode 100644 index 00000000..8dfbaca1 --- /dev/null +++ b/examples/play-opus/App.zig @@ -0,0 +1,163 @@ +/// 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 +const std = @import("std"); +const builtin = @import("builtin"); + +const mach = @import("mach"); +const assets = @import("assets"); +const opus = @import("opus"); +const gpu = mach.gpu; +const math = mach.math; +const sysaudio = mach.sysaudio; + +pub const App = @This(); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +pub const name = .app; +pub const Mod = mach.Mod(@This()); + +pub const events = .{ + .init = .{ .handler = init }, + .deinit = .{ .handler = deinit }, + .tick = .{ .handler = tick }, + .audio_state_change = .{ .handler = audioStateChange }, +}; + +pub const components = .{ + .is_bgm = .{ .type = void }, +}; + +sfx: []const f32, + +fn init(core: *mach.Core.Mod, 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)}); + + const bgm_fbs = std.io.fixedBufferStream(assets.bgm.bit_bit_loop); + const sfx_fbs = std.io.fixedBufferStream(assets.sfx.death); + + var sound_stream = std.io.StreamSource{ .const_buffer = bgm_fbs }; + const bgm = try opus.decodeStream(gpa.allocator(), sound_stream); + + sound_stream = std.io.StreamSource{ .const_buffer = sfx_fbs }; + const sfx = try opus.decodeStream(gpa.allocator(), sound_stream); + + // Initialize module state + app.init(.{ .sfx = sfx.samples }); + + const bgm_entity = try audio.newEntity(); + try app.set(bgm_entity, .is_bgm, {}); + try audio.set(bgm_entity, .samples, bgm.samples); + try audio.set(bgm_entity, .playing, true); + try audio.set(bgm_entity, .index, 0); + + 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", .{}); + + core.send(.start, .{}); +} + +fn deinit(core: *mach.Core.Mod, audio: *mach.Audio.Mod) void { + audio.send(.deinit, .{}); + core.send(.deinit, .{}); +} + +fn audioStateChange( + audio: *mach.Audio.Mod, + app: *Mod, +) !void { + // 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; + + 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 audio.removeEntity(id); + } + } + } +} + +fn tick( + core: *mach.Core.Mod, + audio: *mach.Audio.Mod, + app: *Mod, +) !void { + // TODO(Core) + var iter = mach.core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .key_press => |ev| switch (ev.key) { + .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}); + }, + else => { + // Play a new SFX + const entity = try audio.newEntity(); + try audio.set(entity, .samples, app.state().sfx); + try audio.set(entity, .index, 0); + try audio.set(entity, .playing, true); + }, + }, + .close => core.send(.exit, .{}), + else => {}, + } + } + + // Grab the back buffer of the swapchain + // TODO(Core) + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Create a command encoder + const label = @tagName(name) ++ ".tick"; + const encoder = core.state().device.createCommandEncoder(&.{ .label = label }); + defer encoder.release(); + + // Begin render pass + const sky_blue_background = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 }; + const color_attachments = [_]gpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .clear_value = sky_blue_background, + .load_op = .clear, + .store_op = .store, + }}; + const render_pass = encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ + .label = label, + .color_attachments = &color_attachments, + })); + + // Draw nothing + + // Finish render pass + render_pass.end(); + + // Submit our commands to the queue + var command = encoder.finish(&.{ .label = label }); + defer command.release(); + core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Present the frame + core.send(.present_frame, .{}); +} diff --git a/examples/play-opus/main.zig b/examples/play-opus/main.zig new file mode 100644 index 00000000..17065b6b --- /dev/null +++ b/examples/play-opus/main.zig @@ -0,0 +1,17 @@ +const mach = @import("mach"); + +// The global list of Mach modules registered for use in our application. +pub const modules = .{ + mach.Core, + mach.Audio, + @import("App.zig"), +}; + +// TODO(important): use standard entrypoint instead +pub fn main() !void { + // Initialize mach.Core + try mach.core.initModule(); + + // Main loop + while (try mach.core.tick()) {} +}