implement playing sin waves
This commit is contained in:
parent
3cfd12d8ba
commit
a99bf9c17b
20
src/app.zig
20
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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
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});
|
||||
};
|
||||
}
|
||||
|
||||
@ -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(.{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user