Audio: duplicate mono sounds to all channels

This commit is contained in:
Ali Chraghi 2024-05-06 10:10:11 +03:30 committed by Stephen Gutekanst
parent 9d95fcf0c2
commit e711f69fad
3 changed files with 49 additions and 19 deletions

View file

@ -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);

View file

@ -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);
},

View file

@ -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);
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]);
max_samples = @max(max_samples, 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;
}
}
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;
}
}