Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@
.fingerprint = 0xdc2511c12abe6e80,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.win32 = .{
.url = "git+https://github.com/marlersoft/zigwin32#ec98bb4d9eea532320a8551720a9e3ec6de64994",
.hash = "win32-25.0.28-preview-mX5pFWMt5QPTVIGh3r2-OpPunpcCCjApyRbA6Zn6WALH",
},
.vulkan_headers = .{
.url = "https://github.com/KhronosGroup/Vulkan-Headers/archive/v1.4.315.tar.gz",
.hash = "N-V-__8AAPJS7wFRVAIGhMZ7cis5e-y5LZfn2KfO5O2jiHGH",
},
.vulkan_zig = .{
.url = "git+https://github.com/Snektron/vulkan-zig?ref=master#b496a6a561ffbbeb530b0f9ed4e059f88c0723a5",
.url = "git+https://github.com/Snektron/vulkan-zig?ref=zig-0.16-compat#b496a6a561ffbbeb530b0f9ed4e059f88c0723a5",
.hash = "vulkan-0.0.0-r7Ytx7N9AwD7IZt5_XNHtkJ4G9qY0pCH-cpcOsbL8wzD",
},
.imguiz = .{
Expand Down
118 changes: 86 additions & 32 deletions src/audio/audio_mixer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,43 @@ pub const AudioMixer = struct {
var node = entry.value_ptr.chunks.first;
while (node) |current| : (node = current.next) {
const chunk_node: *PendingChunkNode = @alignCast(@fieldParentPtr("node", current));
const chunk_start_sample = chunk_node.start_frame;
const chunk_end_sample = chunk_node.end_frame;
const chunk_start_sample = chunk_node.start_sample;
const chunk_end_sample = chunk_node.end_sample;

const overlap_start_sample = @max(start_sample, chunk_start_sample);
const overlap_end_sample = @min(end_sample, chunk_end_sample);
if (overlap_start_sample >= overlap_end_sample) {
continue;
}

const sample_positions_to_mix: usize = @intCast(overlap_end_sample - overlap_start_sample);
const input_start_sample: usize = @intCast(overlap_start_sample - chunk_start_sample);
const output_start_sample: usize = @intCast(overlap_start_sample - start_sample);
const data_ptr = chunk_node.data.as_ptr();
// No need to iterate if the device gain is 0.
if (data_ptr.gain == 0.0) {
continue;
}
const src_channels: usize = @intCast(data_ptr.channels);

for (0..sample_positions_to_mix) |sample_idx| {
const output_pcm_offset = (output_start_sample + sample_idx) * channels;
const input_pcm_offset = (input_start_sample + sample_idx) * src_channels;
for (0..channels) |channel_idx| {
mixed_pcm.items[output_pcm_offset + channel_idx] +=
data_ptr.pcm_data[input_pcm_offset + channel_idx] * data_ptr.gain;
}

var i: usize = 0;
const vector_len = std.simd.suggestVectorLength(f32) orelse 1;
const Vector = @Vector(vector_len, f32);
const gain_vector: Vector = @splat(data_ptr.gain);

const output_start: usize = @intCast((overlap_start_sample - start_sample) * channels);
const input_start: usize = @intCast((overlap_start_sample - chunk_start_sample) * channels);
const samples_to_mix: usize = @intCast((overlap_end_sample - overlap_start_sample) * channels);

const output = mixed_pcm.items[output_start..][0..samples_to_mix];
const input = data_ptr.pcm_data[input_start..][0..samples_to_mix];

// Mix with SIMD.
while (i + vector_len <= output.len) : (i += vector_len) {
const output_vec: Vector = output[i..][0..vector_len].*;
const input_vec: Vector = input[i..][0..vector_len].*;
output[i..][0..vector_len].* = output_vec + (gain_vector * input_vec);
}

// Mix the remaining scalers.
while (i < output.len) : (i += 1) {
output[i] += input[i] * data_ptr.gain;
}
}
}
Expand All @@ -61,6 +72,9 @@ pub const AudioMixer = struct {
};

const TestUtil = struct {
const SAMPLE_RATE = 48_000;
const vector_len = std.simd.suggestVectorLength(f32) orelse 1;

fn add_test_chunk(
allocator: Allocator,
device_map: *std.StringHashMap(DeviceState),
Expand All @@ -81,7 +95,7 @@ const TestUtil = struct {
id,
pcm,
0,
48_000,
SAMPLE_RATE,
channels,
);
errdefer audio_capture_data.deinit();
Expand Down Expand Up @@ -112,15 +126,19 @@ test "AudioMixer - mix mixes a single aligned mono chunk" {
var device_map = std.StringHashMap(DeviceState).init(allocator);
defer TestUtil.deinit_test_device_map(allocator, &device_map);

const pcm = [_]f32{ 0.1, 0.2, 0.3, 0.4, 0.5 };
const sample_count = (TestUtil.vector_len * 100) + 1;
var pcm: [sample_count]f32 = undefined;
for (&pcm, 0..) |*sample, i| {
sample.* = @floatFromInt((i % 100) / 100);
}
try TestUtil.add_test_chunk(allocator, &device_map, "mic", &pcm, 0, 1, 1.0);

var mixed = try AudioMixer.mix(allocator, &device_map, 1, 0, 5);
var mixed = try AudioMixer.mix(allocator, &device_map, 1, 0, pcm.len);
defer mixed.deinit(allocator);

try std.testing.expectEqual(@as(usize, pcm.len), mixed.items.len);
for (pcm, 0..) |expected, idx| {
try std.testing.expectEqual(expected, mixed.items[idx]);
try std.testing.expectEqual(pcm.len, mixed.items.len);
for (pcm, 0..) |expected, i| {
try std.testing.expectEqual(expected, mixed.items[i]);
}
}

Expand All @@ -129,15 +147,19 @@ test "AudioMixer - mix mixes a single aligned stereo chunk" {
var device_map = std.StringHashMap(DeviceState).init(allocator);
defer TestUtil.deinit_test_device_map(allocator, &device_map);

const pcm = [_]f32{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 };
const sample_count = (TestUtil.vector_len * 100) + 1;
var pcm: [sample_count * 2]f32 = undefined;
for (&pcm, 0..) |*sample, i| {
sample.* = @floatFromInt((i % 100) / 100);
}
try TestUtil.add_test_chunk(allocator, &device_map, "stereo", &pcm, 0, 2, 1.0);

var mixed = try AudioMixer.mix(allocator, &device_map, 2, 0, 3);
var mixed = try AudioMixer.mix(allocator, &device_map, 2, 0, sample_count);
defer mixed.deinit(allocator);

try std.testing.expectEqual(@as(usize, pcm.len), mixed.items.len);
for (pcm, 0..) |expected, idx| {
try std.testing.expectEqual(expected, mixed.items[idx]);
try std.testing.expectEqual(pcm.len, mixed.items.len);
for (pcm, 0..) |expected, i| {
try std.testing.expectEqual(expected, mixed.items[i]);
}
}

Expand All @@ -146,15 +168,47 @@ test "AudioMixer - mix applies capture gain" {
var device_map = std.StringHashMap(DeviceState).init(allocator);
defer TestUtil.deinit_test_device_map(allocator, &device_map);

const pcm = [_]f32{ 1.0, 0.5, 0.25 };
try TestUtil.add_test_chunk(allocator, &device_map, "mic", &pcm, 0, 1, 0.5);
const sample_count = (TestUtil.vector_len * 100) + 1;
var pcm: [sample_count]f32 = undefined;
for (&pcm, 0..) |*sample, i| {
sample.* = @floatFromInt((i % 100) / 100);
}
const gain: f32 = 0.5;
try TestUtil.add_test_chunk(allocator, &device_map, "mic", &pcm, 0, 1, gain);

var mixed = try AudioMixer.mix(allocator, &device_map, 1, 0, pcm.len);
defer mixed.deinit(allocator);

try std.testing.expectEqual(pcm.len, mixed.items.len);
for (pcm, 0..) |sample, i| {
try std.testing.expectApproxEqAbs(sample * gain, mixed.items[i], 0.0001);
}
}

test "AudioMixer - mix accumulates overlapping chunks with capture gain" {
const allocator = std.testing.allocator;
var device_map = std.StringHashMap(DeviceState).init(allocator);
defer TestUtil.deinit_test_device_map(allocator, &device_map);

const sample_count = (TestUtil.vector_len * 100) + 1;
var first_pcm: [sample_count]f32 = undefined;
var second_pcm: [sample_count]f32 = undefined;
for (&first_pcm, &second_pcm, 0..) |*first_sample, *second_sample, i| {
first_sample.* = @floatFromInt((i % 100) / 100);
second_sample.* = @floatFromInt((i % 25) / 25);
}

const first_gain: f32 = 0.5;
const second_gain: f32 = 0.25;
try TestUtil.add_test_chunk(allocator, &device_map, "mic", &first_pcm, 0, 1, first_gain);
try TestUtil.add_test_chunk(allocator, &device_map, "desktop", &second_pcm, 0, 1, second_gain);

var mixed = try AudioMixer.mix(allocator, &device_map, 1, 0, 3);
var mixed = try AudioMixer.mix(allocator, &device_map, 1, 0, sample_count);
defer mixed.deinit(allocator);

const expected = [_]f32{ 0.5, 0.25, 0.125 };
try std.testing.expectEqual(@as(usize, expected.len), mixed.items.len);
for (expected, 0..) |sample, idx| {
try std.testing.expectApproxEqAbs(sample, mixed.items[idx], 0.0001);
try std.testing.expectEqual(sample_count, mixed.items.len);
for (first_pcm, second_pcm, 0..) |first_sample, second_sample, i| {
const expected = first_sample * first_gain + second_sample * second_gain;
try std.testing.expectApproxEqAbs(expected, mixed.items[i], 0.0001);
}
}
12 changes: 6 additions & 6 deletions src/audio/audio_timeline.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const Arc = @import("../arc.zig").Arc;
/// finalized audio timeline.
pub const PendingChunkNode = struct {
data: Arc(AudioCaptureData),
start_frame: i64,
end_frame: i64,
start_sample: i64,
end_sample: i64,
node: std.DoublyLinkedList.Node = .{},
allocator: Allocator,

Expand All @@ -25,8 +25,8 @@ pub const PendingChunkNode = struct {
const self = try allocator.create(@This());
self.* = .{
.data = data,
.start_frame = start_sample,
.end_frame = end_sample,
.start_sample = start_sample,
.end_sample = end_sample,
.allocator = allocator,
};
return self;
Expand Down Expand Up @@ -106,7 +106,7 @@ pub const AudioTimeline = struct {
self.encoder.deinit(self.allocator);
}

/// Takes ownership of data.
/// Clone data before calling.
pub fn add_data(self: *Self, data: Arc(AudioCaptureData)) !void {
defer data.deinit();
const audio_capture_data = data.as_ptr();
Expand Down Expand Up @@ -248,7 +248,7 @@ pub const AudioTimeline = struct {
while (node) |current| {
const next = current.next;
const chunk_node: *PendingChunkNode = @alignCast(@fieldParentPtr("node", current));
if (chunk_node.end_frame <= self.encoded_until_sample) {
if (chunk_node.end_sample <= self.encoded_until_sample) {
entry.value_ptr.chunks.remove(current);
chunk_node.deinit();
} else {
Expand Down
25 changes: 22 additions & 3 deletions src/capture/audio/audio_capture_data.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,27 @@ pub fn init(
const pcm_copy = try allocator.alloc(f32, pcm_data.len);
errdefer allocator.free(pcm_copy);

var peak_level: f32 = 0.0;
for (pcm_data, pcm_copy) |sample, *dest| {
// Use SIMD to find the peak audo level, while copying the data.
const vector_len = std.simd.suggestVectorLength(f32) orelse 1;
const Vector = @Vector(vector_len, f32);
const zero: Vector = @splat(0.0);
const infinity: Vector = @splat(std.math.inf(f32));

var peak_level_vector: Vector = zero;
var i: usize = 0;
while (i + vector_len <= pcm_data.len) : (i += vector_len) {
// Convert the sample window to a vector.
const samples: Vector = pcm_data[i..][0..vector_len].*;
// Copy the data into the output buffer.
pcm_copy[i..][0..vector_len].* = samples;

const magnitudes = @abs(samples);
const finite_magnitudes = @select(f32, magnitudes < infinity, magnitudes, zero);
peak_level_vector = @max(peak_level_vector, finite_magnitudes);
}

var peak_level = @reduce(.Max, peak_level_vector);
for (pcm_data[i..], pcm_copy[i..]) |sample, *dest| {
dest.* = sample;
if (!std.math.isFinite(sample)) {
continue;
Expand Down Expand Up @@ -124,7 +143,7 @@ test "AudioCaptureData - init stores peak level" {
const data = try @This().init(allocator, "mic", &pcm, 0, 48_000, 1);
defer data.deinit();

try std.testing.expectEqual(@as(f32, 0.75), data.peak_level);
try std.testing.expectEqual(0.75, data.peak_level);
}

test "AudioCaptureData - clone preserves peak level" {
Expand Down