From 07308a548a0d13ef6814ba171e0a40e2c5429431 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Wed, 29 Oct 2025 12:53:41 -0700 Subject: [PATCH] Add level measurement to gauge background noise --- src/main.zig | 84 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/stt.zig | 2 +- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/main.zig b/src/main.zig index 3e32ecd..5e932de 100644 --- a/src/main.zig +++ b/src/main.zig @@ -286,6 +286,70 @@ fn signalHandler(sig: i32) callconv(.c) void { } } +fn runMeasureLevels(allocator: std.mem.Allocator) !void { + const stdout = std.fs.File.stdout(); + const is_tty = stdout.isTty(); + + var capture = try stt.AlsaCapture.init(allocator, "default", 16000, 1024); + defer capture.deinit(); + try capture.open(); + + _ = try stdout.writeAll("Measuring audio levels... Press Ctrl+C to exit\n"); + if (is_tty) { + _ = try stdout.writeAll("Histogram (0-10000):\n"); + } + + var buffer: [4096]i16 = undefined; + var write_buffer: [256]u8 = undefined; + var second_max: u16 = 0; + var last_print = std.time.milliTimestamp(); + + while (!should_exit.load(.acquire)) { + _ = try capture.readAudio(); + const samples_read = capture.getAudioSamples(&buffer); + if (samples_read == 0) { + std.Thread.sleep(10 * std.time.ns_per_ms); + continue; + } + + var max_amp: u16 = 0; + for (buffer[0..samples_read]) |sample| { + const abs_sample = @abs(sample); + if (abs_sample > max_amp) max_amp = abs_sample; + } + + if (max_amp > second_max) second_max = max_amp; + + const now = std.time.milliTimestamp(); + if (now - last_print >= 1000) { + if (is_tty) { + const bar_width = (@as(u32, second_max) * 60) / 10000; + var writer = stdout.writer(&write_buffer); + const w = &writer.interface; + try w.print("{d:5} |", .{second_max}); + try w.flush(); + for (0..bar_width) |_| { + _ = try stdout.writeAll("█"); + } + _ = try stdout.writeAll("\n"); + } else { + var writer = stdout.writer(&write_buffer); + const w = &writer.interface; + try w.print("{d}\n", .{second_max}); + try w.flush(); + } + second_max = 0; + last_print = now; + } + + std.Thread.sleep(50 * std.time.ns_per_ms); + } + + if (is_tty) { + _ = try stdout.writeAll("\n"); + } +} + fn signalAction(sig: i32, info: *const std.posix.siginfo_t, _: ?*anyopaque) callconv(.c) void { // NOTE: info only works correctly if std.posix.SA.SIGINFO is in the flags // std.log.debug("signal action. sig {d}", .{sig}); @@ -353,6 +417,7 @@ pub fn main() !void { var model_path: ?[]const u8 = null; var exec_program: ?[]const u8 = null; + var measure_levels = false; // Parse arguments for (args[1..]) |arg| { @@ -361,13 +426,15 @@ pub fn main() !void { _ = try stdout.writeAll("USAGE:\n"); _ = try stdout.writeAll(" stt [OPTIONS]\n\n"); _ = try stdout.writeAll("OPTIONS:\n"); - _ = try stdout.writeAll(" --model= Path to Vosk model directory\n"); - _ = try stdout.writeAll(" --exec= Program to execute with recognized text\n"); - _ = try stdout.writeAll(" --help, -h Show this help message\n\n"); + _ = try stdout.writeAll(" --model= Path to Vosk model directory\n"); + _ = try stdout.writeAll(" --exec= Program to execute with recognized text\n"); + _ = try stdout.writeAll(" --measure-levels Display real-time audio level histogram\n"); + _ = try stdout.writeAll(" --help, -h Show this help message\n\n"); _ = try stdout.writeAll("EXAMPLES:\n"); _ = try stdout.writeAll(" stt\n"); _ = try stdout.writeAll(" stt --model=../share/vosk/models/vosk-model-small-en-us-0.15\n"); - _ = try stdout.writeAll(" stt --exec=echo\n\n"); + _ = try stdout.writeAll(" stt --exec=echo\n"); + _ = try stdout.writeAll(" stt --measure-levels\n\n"); _ = try stdout.writeAll("The application will search for models in these locations:\n"); _ = try stdout.writeAll(" vosk-model-small-en-us-0.15\n"); _ = try stdout.writeAll(" /../share/vosk/models/vosk-model-small-en-us-0.15\n"); @@ -376,6 +443,8 @@ pub fn main() !void { model_path = arg[8..]; // Skip "--model=" } else if (std.mem.startsWith(u8, arg, "--exec=")) { exec_program = arg[7..]; // Skip "--exec=" + } else if (std.mem.eql(u8, arg, "--measure-levels")) { + measure_levels = true; } } @@ -385,6 +454,13 @@ pub fn main() !void { .exec_program = exec_program, }; defer handler.deinit(); + + // If measure-levels mode, run that instead of normal STT + if (measure_levels) { + try runMeasureLevels(allocator); + return; + } + const speech_handler = stt.SpeechEventHandler{ .onSpeechFn = SpeechHandler.onSpeech, .onErrorFn = SpeechHandler.onError, diff --git a/src/stt.zig b/src/stt.zig index ff7bdf8..48e744d 100644 --- a/src/stt.zig +++ b/src/stt.zig @@ -531,7 +531,7 @@ pub const AlsaCapture = struct { } /// Read audio data from ALSA device and process it - fn readAudio(self: *Self) !usize { + pub fn readAudio(self: *Self) !usize { if (self.pcm_handle == null) return Error.AudioDeviceError;