From a99bf9c17b9e750ac706571a5b5c0f5e73e98044 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 11 Aug 2026 01:05:55 +0300 Subject: [PATCH] implement playing sin waves --- src/app.zig | 20 +++- src/platform/audio.zig | 213 +++++++++++++++++++++++++++++++++++++---- src/platform/root.zig | 2 +- 3 files changed, 213 insertions(+), 22 deletions(-) diff --git a/src/app.zig b/src/app.zig index d7b46ff..0f76344 100644 --- a/src/app.zig +++ b/src/app.zig @@ -11,6 +11,7 @@ const Tiled = @import("tiled"); const Platform = @import("platform/root.zig"); const Gfx = Platform.Gfx; +const Audio = Platform.Audio; const ImGUI = Platform.ImGUI; const Rect = Platform.Rect; @@ -52,6 +53,8 @@ player_direction: Direction, player_frame_index: u32, animation_timer: Platform.Nanoseconds, +sound: Audio.Sound.Id, + const Tilesheet = struct { image: Gfx.ImageData, sprites: []Gfx.Sprite.Id, @@ -161,6 +164,12 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { } } + const sound = Audio.initSound(.{ + .cb = .{ .sample = sinSampleCallback }, + .playing = false, + .frequency = 1 + }); + self.* = App{ .player_pos = .init(100, 100), .show_first_window = true, @@ -171,12 +180,17 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .player_direction = .left, .player_frame_index = 0, .animation_timer = 0, - .tilesheet = tilesheet + .tilesheet = tilesheet, + .sound = sound }; return null; } +fn sinSampleCallback(sound: *Audio.Sound) f32 { + return @sin(440 * sound.phase); +} + pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; @@ -198,6 +212,10 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { dir = dir.normalized(); self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); + if (input.isKeyPressed(.E)) { + Audio.setPlaying(self.sound, !Audio.getPlaying(self.sound)); + } + var new_direction = self.player_direction; if (dir.x < 0) { new_direction = .left; diff --git a/src/platform/audio.zig b/src/platform/audio.zig index 5d9ffb6..3cf3664 100644 --- a/src/platform/audio.zig +++ b/src/platform/audio.zig @@ -1,5 +1,7 @@ const std = @import("std"); const log = std.log.scoped(.audio); +const Io = std.Io; +const assert = std.debug.assert; const sokol = @import("sokol"); const sapp = sokol.app; @@ -10,73 +12,244 @@ const tracy = @import("tracy"); const SlotMapType = @import("./slot_map.zig").SlotMapType; const State = struct { - sounds: Sound.SlotMap + io: Io, + mutex: std.Io.Mutex, + sounds: Sound.SlotMap, + volume: f32 = 0.1, + running: std.atomic.Value(bool) = .init(false), + + temp_buffer: []f32 }; var g_state: State = undefined; pub const InitOptions = struct { logger: saudio.Logger = .{}, - buffer_frames: u32 = 2048, + buffer_frames: u32 = 512, max_sounds: usize = 128, }; pub const Sound = struct { cb: Callback, - userdata: *anyopaque, - volume: f32 = 1, + userdata: ?*anyopaque, + volume: f32, + looping: bool, + adsr: ADSR, + playing: bool, - pub const Callback = *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void; + phase_increment: f32 = 0, + duration_frames: u32 = 0, + + frame_index: u32 = 0, + phase: f32 = 0, + + pub const ADSR = struct { + attack: f32, + decay: f32, + sustain: f32, + release: f32, + + pub const default = ADSR{ + .attack = 0, + .decay = 0, + .sustain = 1, + .release = 0, + }; + }; + + pub const Callback = union(enum) { + sample: *const fn(sound: *Sound) f32, + block: *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void, + }; const SlotMap = SlotMapType(u8, u16, Sound); pub const Id = SlotMap.Id; }; -pub fn init(gpa: std.mem.Allocator, opts: InitOptions) !void { +pub fn init(io: Io, gpa: std.mem.Allocator, opts: InitOptions) !void { const channels = 1; // TODO: Stereo audio const self = &g_state; const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds); errdefer gpa.free(sounds_buffer); + + const temp_buffer = try gpa.alloc(f32, opts.buffer_frames); + errdefer gpa.free(temp_buffer); + self.* = State{ - .sounds = .init(sounds_buffer) + .io = io, + .mutex = .init, + .sounds = .init(sounds_buffer), + .temp_buffer = temp_buffer, }; + self.running.store(true, .seq_cst); saudio.setup(.{ - .stream_userdata_cb = sokolStreamCallback, + .stream_cb = sokolStreamCallback, .logger = opts.logger, .num_channels = channels, .buffer_frames = @intCast(opts.buffer_frames), }); - const sample_rate: f32 = @floatFromInt(saudio.sampleRate()); - const max_latency_s: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate; - const max_latency_ns: i64 = @intFromFloat(max_latency_s * std.time.ns_per_s); - log.debug("Init:", .{}); log.debug("- sample_rate: {}", .{saudio.sampleRate()}); log.debug("- channels: {}", .{saudio.channels()}); log.debug("- buffer_frames: {}", .{saudio.bufferFrames()}); - log.debug("- max_latency: {f}", .{std.Io.Duration.fromNanoseconds(max_latency_ns)}); } pub fn deinit(gpa: std.mem.Allocator) void { var self = &g_state; + self.running.store(false, .seq_cst); + saudio.shutdown(); gpa.free(self.sounds.slots.allocatedSlice()); + gpa.free(self.temp_buffer); } -pub fn addSound() Sound.Id { +pub fn getSampleRate() f32 { + return @floatFromInt(saudio.sampleRate()); } -pub fn removeSound() void { +pub const SoundOptions = struct { + cb: Sound.Callback, + userdata: ?*anyopaque = null, + adsr: Sound.ADSR = .default, + volume: f32 = 1, + playing: bool = true, + looping: bool = false, + + frequency: ?f32 = null, + duration_ns: ?u64 = null +}; + +pub fn initSound(opts: SoundOptions) Sound.Id { + var self = &g_state; + + self.mutex.lock(self.io) catch return .nil; + defer self.mutex.unlock(self.io); + + const id = self.sounds.insertUndefined() catch { + log.warn("Sound limit reached! limit: {}", .{self.sounds.slots.capacity}); + return .nil; + }; + + var phase_increment: f32 = 0; + if (opts.frequency) |frequency| { + phase_increment = frequency * 2 * std.math.pi / getSampleRate(); + } + + var duration_frames: u32 = 0; + if (opts.duration_ns) |duration_ns| { + duration_frames = @intCast(duration_ns * @as(u64, @intCast(saudio.sampleRate())) / std.time.ns_per_s); + } + + const sound = self.sounds.getAssumeExists(id); + sound.* = Sound{ + .cb = opts.cb, + .userdata = opts.userdata, + .adsr = opts.adsr, + .looping = opts.looping, + .volume = opts.volume, + .playing = opts.playing, + .duration_frames = duration_frames, + .phase_increment = phase_increment + }; + + return id; } -fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32, user_data: ?*anyopaque) callconv(.c) void { - _ = buffer; // autofix - _ = num_frames; // autofix - _ = num_channels; // autofix - _ = user_data; // autofix +pub fn deinitSound(id: Sound.Id) void { + var self = &g_state; + + if (self.sounds.get(id)) |sound| { + self.mutex.lock(self.io) catch return; + defer self.mutex.unlock(self.io); + + _ = sound; // autofix + self.sounds.removeAssumeExists(id); + } +} + +pub fn setPlaying(id: Sound.Id, playing: bool) void { + var self = &g_state; + + if (self.sounds.get(id)) |sound| { + self.mutex.lock(self.io) catch return; + defer self.mutex.unlock(self.io); + + sound.playing = playing; + } +} + +pub fn getPlaying(id: Sound.Id) bool { + var self = &g_state; + + if (self.sounds.get(id)) |sound| { + return sound.playing; + } else { + return false; + } +} + +fn bumpSoundCursor(sound: *Sound, count: u32) void { + if (sound.duration_frames > 0) { + sound.frame_index += count; + sound.frame_index = @mod(sound.frame_index, sound.duration_frames); + } + + if (sound.phase_increment > 0) { + sound.phase += sound.phase_increment * @as(f32, @floatFromInt(count)); + sound.phase = @mod(sound.phase, 2 * std.math.pi); + } +} + +fn sokolStream(buffer: [*c]f32, num_frames: u32, num_channels: u32) !void { + const self = &g_state; + + if (!self.running.load(.seq_cst)) { + return; + } + + try self.mutex.lock(self.io); + defer self.mutex.unlock(self.io); + + assert(num_channels == 1); + assert(self.temp_buffer.len >= num_frames); + + @memset(buffer[0..num_frames], 0); + + var iter = self.sounds.iterator(); + while (iter.next()) |id| { + const sound = self.sounds.getAssumeExists(id); + if (!sound.playing) { + continue; + } + + var samples = std.ArrayList(f32).initBuffer(self.temp_buffer[0..num_frames]); + switch (sound.cb) { + .block => |cb| { + cb(sound, &samples); + bumpSoundCursor(sound, @intCast(samples.items.len)); + }, + .sample => |cb| { + samples.expandToCapacity(); + for (samples.items) |*sample| { + sample.* = cb(sound); + bumpSoundCursor(sound, 1); + } + } + } + + for (0..samples.items.len) |i| { + buffer[i] += samples.items[i] * sound.volume * self.volume; + } + } +} + +fn sokolStreamCallback(buffer: [*c]f32, num_frames_i32: i32, num_channels: i32) callconv(.c) void { + sokolStream(buffer, @intCast(num_frames_i32), @intCast(num_channels)) catch |e| { + log.err("Stream audio error: {}", .{e}); + }; } diff --git a/src/platform/root.zig b/src/platform/root.zig index 8f4bdee..720bdf3 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -67,7 +67,7 @@ fn PlatformType(App: type) type { try Gfx.init(self.gpa, .{ .func = sokolLogCallback }); - try Audio.init(self.gpa, .{ + try Audio.init(self.io, self.gpa, .{ .logger = .{ .func = sokolLogCallback } }); ImGUI.init(.{