Audio: duplicate mono sounds to all channels
This commit is contained in:
parent
9d95fcf0c2
commit
e711f69fad
3 changed files with 49 additions and 19 deletions
|
|
@ -77,6 +77,7 @@ fn audioStateChange(
|
||||||
// Play a new sound
|
// Play a new sound
|
||||||
const entity = try audio.newEntity();
|
const entity = try audio.newEntity();
|
||||||
try audio.set(entity, .samples, try fillTone(audio, frequency));
|
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, .playing, true);
|
||||||
try audio.set(entity, .index, 0);
|
try audio.set(entity, .index, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +117,7 @@ fn tick(
|
||||||
// Play a new sound
|
// Play a new sound
|
||||||
const entity = try audio.newEntity();
|
const entity = try audio.newEntity();
|
||||||
try audio.set(entity, .samples, try fillTone(audio, keyToFrequency(ev.key)));
|
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, .playing, true);
|
||||||
try audio.set(entity, .index, 0);
|
try audio.set(entity, .index, 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const builtin = @import("builtin");
|
||||||
|
|
||||||
const mach = @import("mach");
|
const mach = @import("mach");
|
||||||
const assets = @import("assets");
|
const assets = @import("assets");
|
||||||
const opus = @import("opus");
|
const Opus = @import("opus");
|
||||||
const gpu = mach.gpu;
|
const gpu = mach.gpu;
|
||||||
const math = mach.math;
|
const math = mach.math;
|
||||||
const sysaudio = mach.sysaudio;
|
const sysaudio = mach.sysaudio;
|
||||||
|
|
@ -29,7 +29,7 @@ pub const components = .{
|
||||||
.is_bgm = .{ .type = void },
|
.is_bgm = .{ .type = void },
|
||||||
};
|
};
|
||||||
|
|
||||||
sfx: []const f32,
|
sfx: Opus,
|
||||||
|
|
||||||
fn init(core: *mach.Core.Mod, audio: *mach.Audio.Mod, app: *Mod) !void {
|
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
|
// 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);
|
const sfx_fbs = std.io.fixedBufferStream(assets.sfx.death);
|
||||||
|
|
||||||
var sound_stream = std.io.StreamSource{ .const_buffer = bgm_fbs };
|
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 };
|
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
|
// Initialize module state
|
||||||
app.init(.{ .sfx = sfx.samples });
|
app.init(.{ .sfx = sfx });
|
||||||
|
|
||||||
const bgm_entity = try audio.newEntity();
|
const bgm_entity = try audio.newEntity();
|
||||||
try app.set(bgm_entity, .is_bgm, {});
|
try app.set(bgm_entity, .is_bgm, {});
|
||||||
try audio.set(bgm_entity, .samples, bgm.samples);
|
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, .playing, true);
|
||||||
try audio.set(bgm_entity, .index, 0);
|
try audio.set(bgm_entity, .index, 0);
|
||||||
|
|
||||||
|
|
@ -115,7 +116,8 @@ fn tick(
|
||||||
else => {
|
else => {
|
||||||
// Play a new SFX
|
// Play a new SFX
|
||||||
const entity = try audio.newEntity();
|
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, .index, 0);
|
||||||
try audio.set(entity, .playing, true);
|
try audio.set(entity, .playing, true);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
pub const components = .{
|
pub const components = .{
|
||||||
.samples = .{ .type = []const f32 },
|
.samples = .{ .type = []const f32 },
|
||||||
|
.channels = .{ .type = u8 },
|
||||||
.playing = .{ .type = bool },
|
.playing = .{ .type = bool },
|
||||||
.index = .{ .type = usize },
|
.index = .{ .type = usize },
|
||||||
};
|
};
|
||||||
|
|
@ -35,7 +36,7 @@ on_state_change: mach.AnyEvent,
|
||||||
output_mu: std.Thread.Mutex = .{},
|
output_mu: std.Thread.Mutex = .{},
|
||||||
output: SampleBuffer,
|
output: SampleBuffer,
|
||||||
mixing_buffer: ?std.ArrayListUnmanaged(f32) = null,
|
mixing_buffer: ?std.ArrayListUnmanaged(f32) = null,
|
||||||
render_num_samples: usize = undefined,
|
render_num_samples: usize = 0,
|
||||||
debug: bool = false,
|
debug: bool = false,
|
||||||
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
|
@ -99,14 +100,15 @@ fn deinit(audio: *Mod) void {
|
||||||
/// ahead is rather small and imperceivable to most humans.
|
/// ahead is rather small and imperceivable to most humans.
|
||||||
fn audioTick(audio: *Mod) !void {
|
fn audioTick(audio: *Mod) !void {
|
||||||
const allocator = audio.state().allocator;
|
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.
|
// How many samples the driver last expected us to produce.
|
||||||
const driver_expects = audio.state().render_num_samples;
|
const driver_expects = audio.state().render_num_samples;
|
||||||
|
|
||||||
// How many audio samples we will render ahead by
|
// How many audio samples we will render ahead by
|
||||||
const samples_per_ms = @as(f32, @floatFromInt(player.sampleRate())) / 1000.0;
|
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
|
// 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.
|
// expected, expects, plus the play ahead amount.
|
||||||
|
|
@ -133,22 +135,32 @@ fn audioTick(audio: *Mod) !void {
|
||||||
@memset(mixing_buffer.items, 0);
|
@memset(mixing_buffer.items, 0);
|
||||||
|
|
||||||
var did_state_change = false;
|
var did_state_change = false;
|
||||||
var max_samples: usize = 0;
|
|
||||||
var archetypes_iter = audio.entities.query(.{ .all = &.{
|
var archetypes_iter = audio.entities.query(.{ .all = &.{
|
||||||
.{ .mach_audio = &.{ .samples, .playing, .index } },
|
.{ .mach_audio = &.{ .samples, .channels, .playing, .index } },
|
||||||
} });
|
} });
|
||||||
while (archetypes_iter.next()) |archetype| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
for (
|
for (
|
||||||
archetype.slice(.entity, .id),
|
archetype.slice(.entity, .id),
|
||||||
archetype.slice(.mach_audio, .samples),
|
archetype.slice(.mach_audio, .samples),
|
||||||
|
archetype.slice(.mach_audio, .channels),
|
||||||
archetype.slice(.mach_audio, .playing),
|
archetype.slice(.mach_audio, .playing),
|
||||||
archetype.slice(.mach_audio, .index),
|
archetype.slice(.mach_audio, .index),
|
||||||
) |id, samples, playing, index| {
|
) |id, samples, channels, playing, index| {
|
||||||
if (!playing) continue;
|
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]);
|
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) {
|
if (index + to_read >= samples.len) {
|
||||||
// No longer playing, we've read all samples
|
// No longer playing, we've read all samples
|
||||||
did_state_change = true;
|
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| {
|
for (a[i..b.len], b[i..]) |*a_sample, b_sample| {
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue