Skip to content
Merged
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
47 changes: 47 additions & 0 deletions src/audio/audio_replay_buffer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ fn replay_retention_samples(self: *Self) i64 {
return self.replay_seconds * SAMPLE_RATE;
}

const TestUtil = struct {
fn create_audio_capture_data(
allocator: Allocator,
timestamp_ns: i128,
samples_per_channel: usize,
value: f32,
) !*AudioCaptureData {
const pcm = try allocator.alloc(f32, samples_per_channel * CHANNELS);
defer allocator.free(pcm);
@memset(pcm, value);

return AudioCaptureData.init(
allocator,
"speaker",
pcm,
timestamp_ns,
SAMPLE_RATE,
CHANNELS,
);
}
};

test "addData - encodes audio before export and exposes packet timing" {
const allocator = std.testing.allocator;
const sample_rate: u32 = 48_000;
Expand Down Expand Up @@ -187,3 +209,28 @@ test "trimPackets - drops encoded packets outside replay window" {
const first_packet = iter.next() orelse return error.ExpectedEncodedPacket;
try std.testing.expect(first_packet.data.*.pts + first_packet.data.*.duration > expected_min_start);
}

test "size tracks encoded packet bytes as audio is added and trimmed" {
const allocator = std.testing.allocator;
const samples_per_chunk: usize = 48_000;
const base_ns: i128 = 3 * std.time.ns_per_s;
const trimmed_replay_seconds: u32 = 1;

var replay_buffer = try Self.init(allocator, 10);
defer replay_buffer.deinit();

const first_chunk = try TestUtil.create_audio_capture_data(allocator, base_ns, samples_per_chunk, 0.1);
try replay_buffer.add_data(first_chunk);
try std.testing.expectEqual(18_222, replay_buffer.size);
try std.testing.expectEqual(45, replay_buffer.len);

const second_chunk = try TestUtil.create_audio_capture_data(allocator, base_ns + std.time.ns_per_s, samples_per_chunk, 0.2);
try replay_buffer.add_data(second_chunk);
try replay_buffer.finalize();
try std.testing.expectEqual(49_401, replay_buffer.size);
try std.testing.expectEqual(98, replay_buffer.len);

replay_buffer.set_replay_seconds(trimmed_replay_seconds);
try std.testing.expectEqual(28_779, replay_buffer.size);
try std.testing.expectEqual(48, replay_buffer.len);
}
4 changes: 2 additions & 2 deletions src/state/actor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ pub const Actor = struct {
if (_self.state.user_settings.settings.restore_capture_source_on_startup and
_self.state.user_settings.settings.start_replay_buffer_on_startup)
{
_self.start_record() catch |err| {
_self.handle_action(.start_record) catch |err| {
log.err("[capture_startup] start_record error: {}", .{err});
};
}
Expand Down Expand Up @@ -460,7 +460,7 @@ pub const Actor = struct {
);
self.ui_mutex.lock();
defer self.ui_mutex.unlock();
self.state.replay_buffer.size = video_replay_buffer.size;
self.state.replay_buffer.video_size = video_replay_buffer.size;
self.state.replay_buffer.seconds = video_replay_buffer.get_seconds();
}
}
Expand Down
120 changes: 68 additions & 52 deletions src/state/audio_state.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ pub const AudioState = struct {
audio_capture: *AudioCapture,
// TODO: Convert devices to a ArrayHashMap.
/// This is a list of all currently available audio devices.
devices: std.ArrayList(AudioDeviceViewModel),
devices: Mutex(std.ArrayList(AudioDeviceViewModel)),
audio_replay_buffer: Mutex(?*AudioReplayBuffer) = .init(null),
capture_thread: ?std.Thread = null,

pub fn init(allocator: Allocator, audio_capture: *AudioCapture) !Self {
return .{
.allocator = allocator,
.audio_capture = audio_capture,
.devices = try .initCapacity(allocator, 0),
.devices = .init(try .initCapacity(allocator, 0)),
};
}

Expand All @@ -70,8 +70,11 @@ pub const AudioState = struct {
}
}

self.clear_devices();
self.devices.deinit(self.allocator);
var locked_devices = self.devices.lock();
defer locked_devices.unlock();
const devices = locked_devices.unwrap_ptr();
clear_devices(devices);
devices.deinit(self.allocator);
}

pub fn handle_action(self: *Self, actor: *Actor, action: Actions) !void {
Expand All @@ -87,13 +90,21 @@ pub const AudioState = struct {
var available_devices = try self.audio_capture.get_available_devices(self.allocator);
defer available_devices.deinit();

actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
self.clear_devices();
errdefer self.clear_devices();
var user_settings = blk: {
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
break :blk try actor.state.user_settings.settings.clone(self.allocator);
};
defer user_settings.deinit(self.allocator);

var locked_devices = self.devices.lock();
defer locked_devices.unlock();
const devices = locked_devices.unwrap_ptr();
clear_devices(devices);
errdefer clear_devices(devices);

for (available_devices.devices.items) |device| {
const persisted_settings = actor.state.user_settings.settings.audio_devices.map.get(device.id);
const persisted_settings = user_settings.audio_devices.map.get(device.id);
const device_copy = try AudioDeviceViewModel.init(self.allocator, .{
.id = device.id,
.name = device.name,
Expand All @@ -103,22 +114,23 @@ pub const AudioState = struct {
.gain = if (persisted_settings) |settings| settings.gain else 1.0,
});
errdefer device_copy.deinit();
try self.devices.append(self.allocator, device_copy);
try devices.append(self.allocator, device_copy);
}

try self.update_selected_devices();
try self.update_selected_devices(devices);
},
.toggle_audio_device => |device_id| {
defer self.allocator.free(device_id);

{
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
var locked_devices = self.devices.lock();
defer locked_devices.unlock();
const devices = locked_devices.unwrap_ptr();

for (self.devices.items) |*device| {
for (devices.items) |*device| {
if (!std.mem.eql(u8, device.id, device_id)) continue;
device.selected = !device.selected;
try self.update_selected_devices();
try self.update_selected_devices(devices);

try actor.dispatch(.{
.user_settings = .{
Expand All @@ -137,10 +149,11 @@ pub const AudioState = struct {
const payload = _action.payload;
defer _action.deinit();
{
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
var locked_devices = self.devices.lock();
defer locked_devices.unlock();
const devices = locked_devices.unwrap_ptr();

for (self.devices.items) |*device| {
for (devices.items) |*device| {
if (!std.mem.eql(u8, device.id, payload.device_id)) continue;
device.gain = std.math.clamp(payload.gain, AUDIO_GAIN_MIN, AUDIO_GAIN_MAX);
try actor.dispatch(.{
Expand Down Expand Up @@ -170,6 +183,30 @@ pub const AudioState = struct {
else => {},
}
},
.start_record => {
const replay_seconds = blk: {
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
break :blk actor.state.user_settings.settings.replay_seconds;
};
var replay_buffer_locked = self.audio_replay_buffer.lock();
defer replay_buffer_locked.unlock();
const ptr = replay_buffer_locked.unwrap_ptr();
if (ptr.* != null) {
log.warn("[handle_action] start_record - previous buffer was not destroyed", .{});
ptr.*.?.deinit();
}
ptr.* = try AudioReplayBuffer.init(self.allocator, replay_seconds);
},
.stop_record => {
var locked = self.audio_replay_buffer.lock();
defer locked.unlock();

if (locked.unwrap()) |audio_replay_buffer| {
audio_replay_buffer.deinit();
locked.set(null);
}
},
else => {},
}
}
Expand Down Expand Up @@ -201,11 +238,11 @@ pub const AudioState = struct {

/// Communicates with the audio capture interface and tells it which audio devices
/// were selected.
fn update_selected_devices(self: *Self) !void {
fn update_selected_devices(self: *Self, devices: *std.ArrayList(AudioDeviceViewModel)) !void {
var selected_devices = try std.ArrayList(SelectedAudioDevice).initCapacity(self.allocator, 0);
defer selected_devices.deinit(self.allocator);

for (self.devices.items) |device| {
for (devices.items) |device| {
if (!device.selected) continue;
try selected_devices.append(self.allocator, .{
.id = device.id,
Expand All @@ -216,28 +253,14 @@ pub const AudioState = struct {
try self.audio_capture.update_selected_devices(selected_devices.items);
}

pub fn clear_devices(self: *Self) void {
for (self.devices.items) |device| {
pub fn clear_devices(devices: *std.ArrayList(AudioDeviceViewModel)) void {
for (devices.items) |device| {
device.deinit();
}
self.devices.clearRetainingCapacity();
devices.clearRetainingCapacity();
}

fn capture_thread_handler(self: *Self, actor: *Actor) !void {
const replay_seconds = blk: {
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
break :blk actor.state.user_settings.settings.replay_seconds;
};

{
var replay_buffer_locked = self.audio_replay_buffer.lock();
defer replay_buffer_locked.unlock();
const ptr = replay_buffer_locked.unwrap_ptr();
assert(ptr.* == null);
ptr.* = try AudioReplayBuffer.init(self.allocator, replay_seconds);
}

while (true) {
const data = self.audio_capture.receive_data() catch |err| {
if (err == ChanError.Closed) {
Expand All @@ -248,15 +271,12 @@ pub const AudioState = struct {
return err;
};

const gain = blk: {
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
data.gain = blk: {
var locked_devices = self.devices.lock();
defer locked_devices.unlock();
const devices = locked_devices.unwrap_ptr();

if (!actor.state.is_recording_video) {
break :blk null;
}

for (self.devices.items) |device| {
for (devices.items) |device| {
if (std.mem.eql(u8, device.id, data.id)) {
break :blk device.gain;
}
Expand All @@ -269,20 +289,16 @@ pub const AudioState = struct {
break :blk 1.0;
};

if (gain) |device_gain| {
data.gain = device_gain;
} else {
data.deinit();
continue;
}

var replay_buffer_locked = self.audio_replay_buffer.lock();
defer replay_buffer_locked.unlock();
if (replay_buffer_locked.unwrap()) |replay_buffer| {
replay_buffer.add_data(data) catch |err| {
data.deinit();
return err;
};
actor.ui_mutex.lock();
defer actor.ui_mutex.unlock();
actor.state.replay_buffer.audio_size = replay_buffer.size;
} else {
data.deinit();
}
Expand Down
16 changes: 12 additions & 4 deletions src/state/state.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ const UserSettingsState = @import("./user_settings_state.zig").UserSettingsState
const AudioDeviceType = @import("../capture/audio/audio_capture.zig").AudioDeviceType;
const AudioState = @import("./audio_state.zig").AudioState;

// TODO: add audio size
const ReplayBufferViewModel = struct {
size: u64 = 0,
video_size: u64 = 0,
audio_size: u64 = 0,
seconds: u64 = 0,

pub fn size_in_mb(self: *const @This()) f64 {
const mb = @as(f64, @floatFromInt(self.size)) / (1024.0 * 1024.0);
pub fn size_in_mb(self: *const @This(), size_type: enum { total, audio, video }) f64 {
return switch (size_type) {
.total => _size_in_mb(self.audio_size + self.video_size),
.audio => _size_in_mb(self.audio_size),
.video => _size_in_mb(self.video_size),
};
}

fn _size_in_mb(size: u64) f64 {
const mb = @as(f64, @floatFromInt(size)) / (1024.0 * 1024.0);
return mb;
}
};
Expand Down
1 change: 1 addition & 0 deletions src/state/user_settings_state.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub const UserSettingsState = struct {
const Self = @This();

allocator: Allocator,
// TODO: Use mutex here instead of ui_mutex.
settings: UserSettings = .{},

pub fn init(allocator: Allocator) !Self {
Expand Down
Loading
Loading