From e711f69faddaabf3c338c5e44e845533db44debd Mon Sep 17 00:00:00 2001 From: Ali Chraghi Date: Mon, 6 May 2024 10:10:11 +0330 Subject: [PATCH] Audio: duplicate mono sounds to all channels --- examples/piano/App.zig | 2 ++ examples/play-opus/App.zig | 14 +++++----- src/Audio.zig | 52 ++++++++++++++++++++++++++++---------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/examples/piano/App.zig b/examples/piano/App.zig index 27edfb09..25ea4390 100644 --- a/examples/piano/App.zig +++ b/examples/piano/App.zig @@ -77,6 +77,7 @@ fn audioStateChange( // Play a new sound const entity = try audio.newEntity(); try audio.set(entity, .samples, try fillTone(audio, frequency)); + try audio.set(entity, .channels, @intCast(audio.state().player.channels().len)); try audio.set(entity, .playing, true); try audio.set(entity, .index, 0); } @@ -116,6 +117,7 @@ fn tick( // Play a new sound const entity = try audio.newEntity(); try audio.set(entity, .samples, try fillTone(audio, keyToFrequency(ev.key))); + try audio.set(entity, .channels, @intCast(audio.state().player.channels().len)); try audio.set(entity, .playing, true); try audio.set(entity, .index, 0); diff --git a/examples/play-opus/App.zig b/examples/play-opus/App.zig index 8dfbaca1..7cb469e3 100644 --- a/examples/play-opus/App.zig +++ b/examples/play-opus/App.zig @@ -6,7 +6,7 @@ const builtin = @import("builtin"); const mach = @import("mach"); const assets = @import("assets"); -const opus = @import("opus"); +const Opus = @import("opus"); const gpu = mach.gpu; const math = mach.math; const sysaudio = mach.sysaudio; @@ -29,7 +29,7 @@ pub const components = .{ .is_bgm = .{ .type = void }, }; -sfx: []const f32, +sfx: Opus, 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 @@ -40,17 +40,18 @@ fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) !void { 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); + 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); + const sfx = try Opus.decodeStream(gpa.allocator(), sound_stream); // Initialize module state - app.init(.{ .sfx = sfx.samples }); + app.init(.{ .sfx = sfx }); 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, .channels, bgm.channels); try audio.set(bgm_entity, .playing, true); try audio.set(bgm_entity, .index, 0); @@ -115,7 +116,8 @@ fn tick( else => { // Play a new SFX const entity = try audio.newEntity(); - try audio.set(entity, .samples, app.state().sfx); + try audio.set(entity, .samples, app.state().sfx.samples); + try audio.set(entity, .channels, app.state().sfx.channels); try audio.set(entity, .index, 0); try audio.set(entity, .playing, true); }, diff --git a/src/Audio.zig b/src/Audio.zig index 5121823e..4de1a6bf 100644 --- a/src/Audio.zig +++ b/src/Audio.zig @@ -8,6 +8,7 @@ pub const Mod = mach.Mod(@This()); pub const components = .{ .samples = .{ .type = []const f32 }, + .channels = .{ .type = u8 }, .playing = .{ .type = bool }, .index = .{ .type = usize }, }; @@ -35,7 +36,7 @@ on_state_change: mach.AnyEvent, output_mu: std.Thread.Mutex = .{}, output: SampleBuffer, mixing_buffer: ?std.ArrayListUnmanaged(f32) = null, -render_num_samples: usize = undefined, +render_num_samples: usize = 0, debug: bool = false, var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -99,14 +100,15 @@ fn deinit(audio: *Mod) void { /// ahead is rather small and imperceivable to most humans. fn audioTick(audio: *Mod) !void { const allocator = audio.state().allocator; - var player = audio.state().player; + const player = &audio.state().player; + const player_channels: u8 = @intCast(player.channels().len); // How many samples the driver last expected us to produce. const driver_expects = audio.state().render_num_samples; // How many audio samples we will render ahead by const samples_per_ms = @as(f32, @floatFromInt(player.sampleRate())) / 1000.0; - const render_ahead: u32 = @as(u32, @intFromFloat(@trunc(audio.state().ms_render_ahead * samples_per_ms))) * @as(u32, @intCast(player.channels().len)); + const render_ahead: u32 = @as(u32, @intFromFloat(@trunc(audio.state().ms_render_ahead * samples_per_ms))) * player_channels; // Our goal is to ensure that we always have pre-rendered the number of samples the driver last // expected, expects, plus the play ahead amount. @@ -133,22 +135,32 @@ fn audioTick(audio: *Mod) !void { @memset(mixing_buffer.items, 0); var did_state_change = false; - var max_samples: usize = 0; var archetypes_iter = audio.entities.query(.{ .all = &.{ - .{ .mach_audio = &.{ .samples, .playing, .index } }, + .{ .mach_audio = &.{ .samples, .channels, .playing, .index } }, } }); while (archetypes_iter.next()) |archetype| { for ( archetype.slice(.entity, .id), archetype.slice(.mach_audio, .samples), + archetype.slice(.mach_audio, .channels), archetype.slice(.mach_audio, .playing), archetype.slice(.mach_audio, .index), - ) |id, samples, playing, index| { + ) |id, samples, channels, playing, index| { if (!playing) continue; - const to_read = @min(samples.len - index, mixing_buffer.items.len); - mixSamples(mixing_buffer.items[0..to_read], samples[index..][0..to_read]); - max_samples = @max(max_samples, to_read); + const channels_diff = player_channels - channels + 1; + const to_read = @min(samples.len - index, mixing_buffer.items.len) / channels_diff; + if (channels == 1 and player_channels > 1) { + // Duplicate samples for mono sounds + var i: usize = 0; + for (samples[index..][0..to_read]) |sample| { + mixSamplesDuplicate(mixing_buffer.items[i..][0..player_channels], sample); + i += player_channels; + } + } else { + mixSamples(mixing_buffer.items[0..to_read], samples[index..][0..to_read]); + } + if (index + to_read >= samples.len) { // No longer playing, we've read all samples did_state_change = true; @@ -243,9 +255,23 @@ inline fn mixSamples(a: []f32, b: []const f32) void { } } - if (i < b.len) { - for (a[i..b.len], b[i..]) |*a_sample, b_sample| { - a_sample.* += b_sample; - } + for (a[i..b.len], b[i..]) |*a_sample, b_sample| { + a_sample.* += b_sample; + } +} + +inline fn mixSamplesDuplicate(a: []f32, b: f32) void { + var i: usize = 0; + + // use SIMD when available + if (vector_length) |vec_len| { + const vec_blocks_len = a.len - (a.len % vec_len); + while (i < vec_blocks_len) : (i += vec_len) { + a[i..][0..vec_len].* += @as(@Vector(vec_len, f32), @splat(b)); + } + } + + for (a[i..]) |*a_sample| { + a_sample.* += b; } }