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 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;
|
||||||
|
|||||||
@ -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});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(.{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user