implement playing sin waves

This commit is contained in:
Rokas Puzonas 2026-08-11 01:05:55 +03:00
parent 3cfd12d8ba
commit a99bf9c17b
3 changed files with 213 additions and 22 deletions

View File

@ -11,6 +11,7 @@ const Tiled = @import("tiled");
const Platform = @import("platform/root.zig"); const Platform = @import("platform/root.zig");
const Gfx = Platform.Gfx; const Gfx = Platform.Gfx;
const Audio = Platform.Audio;
const ImGUI = Platform.ImGUI; const ImGUI = Platform.ImGUI;
const Rect = Platform.Rect; const Rect = Platform.Rect;
@ -52,6 +53,8 @@ player_direction: Direction,
player_frame_index: u32, player_frame_index: u32,
animation_timer: Platform.Nanoseconds, animation_timer: Platform.Nanoseconds,
sound: Audio.Sound.Id,
const Tilesheet = struct { const Tilesheet = struct {
image: Gfx.ImageData, image: Gfx.ImageData,
sprites: []Gfx.Sprite.Id, 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{ self.* = App{
.player_pos = .init(100, 100), .player_pos = .init(100, 100),
.show_first_window = true, .show_first_window = true,
@ -171,12 +180,17 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.player_direction = .left, .player_direction = .left,
.player_frame_index = 0, .player_frame_index = 0,
.animation_timer = 0, .animation_timer = 0,
.tilesheet = tilesheet .tilesheet = tilesheet,
.sound = sound
}; };
return null; return null;
} }
fn sinSampleCallback(sound: *Audio.Sound) f32 {
return @sin(440 * sound.phase);
}
pub fn frame(self: *App, plt: Platform.Frame) !void { pub fn frame(self: *App, plt: Platform.Frame) !void {
const input = plt.input; const input = plt.input;
@ -198,6 +212,10 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
dir = dir.normalized(); dir = dir.normalized();
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); 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; var new_direction = self.player_direction;
if (dir.x < 0) { if (dir.x < 0) {
new_direction = .left; new_direction = .left;

View File

@ -1,5 +1,7 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.audio); const log = std.log.scoped(.audio);
const Io = std.Io;
const assert = std.debug.assert;
const sokol = @import("sokol"); const sokol = @import("sokol");
const sapp = sokol.app; const sapp = sokol.app;
@ -10,73 +12,244 @@ const tracy = @import("tracy");
const SlotMapType = @import("./slot_map.zig").SlotMapType; const SlotMapType = @import("./slot_map.zig").SlotMapType;
const State = struct { 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; var g_state: State = undefined;
pub const InitOptions = struct { pub const InitOptions = struct {
logger: saudio.Logger = .{}, logger: saudio.Logger = .{},
buffer_frames: u32 = 2048, buffer_frames: u32 = 512,
max_sounds: usize = 128, max_sounds: usize = 128,
}; };
pub const Sound = struct { pub const Sound = struct {
cb: Callback, cb: Callback,
userdata: *anyopaque, userdata: ?*anyopaque,
volume: f32 = 1, 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); const SlotMap = SlotMapType(u8, u16, Sound);
pub const Id = SlotMap.Id; 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 channels = 1; // TODO: Stereo audio
const self = &g_state; const self = &g_state;
const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds); const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds);
errdefer gpa.free(sounds_buffer); errdefer gpa.free(sounds_buffer);
const temp_buffer = try gpa.alloc(f32, opts.buffer_frames);
errdefer gpa.free(temp_buffer);
self.* = State{ 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(.{ saudio.setup(.{
.stream_userdata_cb = sokolStreamCallback, .stream_cb = sokolStreamCallback,
.logger = opts.logger, .logger = opts.logger,
.num_channels = channels, .num_channels = channels,
.buffer_frames = @intCast(opts.buffer_frames), .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("Init:", .{});
log.debug("- sample_rate: {}", .{saudio.sampleRate()}); log.debug("- sample_rate: {}", .{saudio.sampleRate()});
log.debug("- channels: {}", .{saudio.channels()}); log.debug("- channels: {}", .{saudio.channels()});
log.debug("- buffer_frames: {}", .{saudio.bufferFrames()}); 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 { pub fn deinit(gpa: std.mem.Allocator) void {
var self = &g_state; var self = &g_state;
self.running.store(false, .seq_cst);
saudio.shutdown(); saudio.shutdown();
gpa.free(self.sounds.slots.allocatedSlice()); 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 { pub fn deinitSound(id: Sound.Id) void {
_ = buffer; // autofix var self = &g_state;
_ = num_frames; // autofix
_ = num_channels; // autofix if (self.sounds.get(id)) |sound| {
_ = user_data; // autofix 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});
};
} }

View File

@ -67,7 +67,7 @@ fn PlatformType(App: type) type {
try Gfx.init(self.gpa, .{ try Gfx.init(self.gpa, .{
.func = sokolLogCallback .func = sokolLogCallback
}); });
try Audio.init(self.gpa, .{ try Audio.init(self.io, self.gpa, .{
.logger = .{ .func = sokolLogCallback } .logger = .{ .func = sokolLogCallback }
}); });
ImGUI.init(.{ ImGUI.init(.{