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
6 changes: 6 additions & 0 deletions ac-rs/ZMQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ Every CTRL reply contains at minimum:

On failure: `"ok": false, "error": "<human-readable string>"`.

Commands that use calibration refuse synchronously when `cal.json` exists
but cannot be read or parsed. No worker starts and no measurement frames are
published. The error string names the store and cause and confirms that the
existing file was preserved; transfer refusals also state that the failure
applies to all requested pairs.

---

## DATA frame envelope
Expand Down
15 changes: 7 additions & 8 deletions ac-rs/crates/ac-core/src/shared/calibration/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ fn read_all_entries(path: &Path) -> Result<HashMap<String, CalibrationEntry>> {
}
let raw =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
serde_json::from_str(&raw).with_context(|| {
format!(
"parsing {} — refusing to treat it as empty, because saving over it would \
discard every calibration it holds",
path.display()
)
})
serde_json::from_str(&raw).with_context(|| format!("parsing {}", path.display()))
}

/// Serialize `all` to `path` atomically: write a sibling temporary, then
Expand Down Expand Up @@ -90,7 +84,12 @@ impl Calibration {
/// Existing entries for other channel pairs are preserved.
pub fn save(&self, path: Option<&Path>) -> Result<()> {
let path = resolve_path(path);
let mut all = read_all_entries(&path)?;
let mut all = read_all_entries(&path).with_context(|| {
format!(
"refusing to save over unreadable calibration store {} — existing file preserved",
path.display()
)
})?;
all.insert(self.key(), self.to_entry());
write_all_entries(&path, &all)?;
eprintln!(
Expand Down
3 changes: 1 addition & 2 deletions ac-rs/crates/ac-daemon/src/handlers/audio/monitor/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,12 +368,11 @@ impl ChannelState {
pub(super) fn new(
channel: u32,
in_port: String,
out_ch: u32,
cal: Option<Calibration>,
sr: u32,
freq_hz: f64,
caps: &RingCaps,
) -> Self {
let cal = Calibration::load(out_ch, channel, None).ok().flatten();
let spl_offset = cal.as_ref().and_then(Calibration::spl_offset_db);
let mic_curve = cal.as_ref().and_then(|c| c.mic_response.clone());
let loudness_fir = mic_curve
Expand Down
43 changes: 26 additions & 17 deletions ac-rs/crates/ac-daemon/src/handlers/audio/monitor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ use ac_core::visualize::weighting_curves::WeightingCurve;
use crate::server::{MonitorParams, ServerState};

use super::super::{
busy_guard, cfg_guard, make_engine_for_state, resolve_input, selected_backend_is_fake,
send_pub, spawn_worker,
busy_guard, cfg_guard, load_calibration_or_refuse, make_engine_for_state, resolve_input,
selected_backend_is_fake, send_pub, spawn_worker,
};

use self::capture::{
Expand Down Expand Up @@ -103,18 +103,6 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value {

let lf_fft_n = defaults.lf_fft_n;
let crossover_hz = defaults.crossover_hz;
{
let mut mp = state.monitor_params.lock().unwrap();
*mp = MonitorParams {
interval,
fft_n,
lf_fft_n,
crossover_hz,
active: true,
};
}
let monitor_params_shared = state.monitor_params.clone();

let cfg = state.cfg.lock().unwrap().clone();

let channels: Vec<u32> = cmd
Expand Down Expand Up @@ -145,6 +133,15 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value {
Err(e) => return json!({"ok": false, "error": e}),
};
let primary_in_port = in_ports.first().cloned().unwrap_or_default();
let out_ch = cfg.output_channel;

let mut channel_cals = Vec::with_capacity(channels.len());
for &channel in &channels {
match load_calibration_or_refuse(out_ch, channel, "measurement", None) {
Ok(cal) => channel_cals.push(cal),
Err(msg) => return json!({"ok": false, "error": msg}),
}
}

let pub_tx = state.pub_tx.clone();
let mut eng = match make_engine_for_state(state) {
Expand All @@ -153,7 +150,6 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value {
};
let fake = selected_backend_is_fake(eng.as_ref());
let backend = eng.backend_name();
let out_ch = cfg.output_channel;
let n_channels = channels.len() as u32;
let channels_worker = channels.clone();
let in_ports_worker = in_ports.clone();
Expand All @@ -167,6 +163,18 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value {
let loudness_reset_shared = state.loudness_reset_request.clone();
let band_weighting_shared = state.band_weighting.clone();

{
let mut mp = state.monitor_params.lock().unwrap();
*mp = MonitorParams {
interval,
fft_n,
lf_fft_n,
crossover_hz,
active: true,
};
}
let monitor_params_shared = state.monitor_params.clone();

let worker = spawn_worker(state, "monitor_spectrum", move |stop| {
let start_port = in_ports_worker.first().map(String::as_str);
if let Err(e) = eng.start(&[], start_port) {
Expand Down Expand Up @@ -286,8 +294,9 @@ pub fn monitor_spectrum(state: &ServerState, cmd: &Value) -> Value {
let mut channel_states: Vec<ChannelState> = channels_worker
.iter()
.zip(in_ports_worker.iter())
.map(|(&channel, in_port)| {
ChannelState::new(channel, in_port.clone(), out_ch, sr, freq_hz, &ring_caps)
.zip(channel_cals)
.map(|((&channel, in_port), cal)| {
ChannelState::new(channel, in_port.clone(), cal, sr, freq_hz, &ring_caps)
})
.collect();
let single_channel = channel_states.len() == 1;
Expand Down
8 changes: 4 additions & 4 deletions ac-rs/crates/ac-daemon/src/handlers/audio/plot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use ac_core::shared::calibration::{Calibration, TauConditions};
use crate::server::ServerState;

use super::super::{
apply_drive_ceiling, busy_guard, cfg_guard, make_engine_for_state, resolve_input,
apply_drive_ceiling, busy_guard, cal_guard, cfg_guard, make_engine_for_state, resolve_input,
resolve_output, send_pub, snapshot_from_cal, spawn_worker, sweep_point_frame, Tier1Ctx,
};
use crate::handlers::mic;
Expand Down Expand Up @@ -70,6 +70,7 @@ pub fn plot(state: &ServerState, cmd: &Value) -> Value {
let backend = eng.backend_name();
let out_ch = cfg.output_channel;
let in_ch = cfg.input_channel;
let cal = cal_guard!(out_ch, in_ch);
// Processing-context shared state — same Arc clones the monitor
// worker uses so #97 + #98 wire the same envelope onto Tier 1.
let mic_corr_enabled = state.mic_correction_enabled.clone();
Expand All @@ -84,7 +85,6 @@ pub fn plot(state: &ServerState, cmd: &Value) -> Value {
let temperature_c = cfg.temperature_c;

let worker = spawn_worker(state, "plot", move |stop| {
let cal = Calibration::load(out_ch, in_ch, None).ok().flatten();
let mic_curve_opt = cal.as_ref().and_then(|c| c.mic_response.clone());
let spl_offset = cal.as_ref().and_then(Calibration::spl_offset_db);
let freqs = super::super::log_freq_points(start_hz, stop_hz, ppd);
Expand Down Expand Up @@ -336,6 +336,7 @@ pub fn plot_level(state: &ServerState, cmd: &Value) -> Value {
let pub_tx = state.pub_tx.clone();
let out_ch = cfg.output_channel;
let in_ch = cfg.input_channel;
let cal = cal_guard!(out_ch, in_ch);
let mic_corr_enabled = state.mic_correction_enabled.clone();
let band_weighting_shared = state.band_weighting.clone();
let time_integration_shared = state.time_integration_mode.clone();
Expand All @@ -346,7 +347,6 @@ pub fn plot_level(state: &ServerState, cmd: &Value) -> Value {
};
let backend = eng.backend_name();
let worker = spawn_worker(state, "plot_level", move |stop| {
let cal = Calibration::load(out_ch, in_ch, None).ok().flatten();
let mic_curve_opt = cal.as_ref().and_then(|c| c.mic_response.clone());
let spl_offset = cal.as_ref().and_then(Calibration::spl_offset_db);
// Raw request shape — each computed level is clamped individually
Expand Down Expand Up @@ -655,6 +655,7 @@ pub fn plot_ir(state: &ServerState, cmd: &Value) -> Value {
let out_port_reply = out_port.clone();
let out_ch = cfg.output_channel;
let in_ch = cfg.input_channel;
let cal = cal_guard!(out_ch, in_ch);
let report_dir = cfg.report_dir.clone();
let temperature_c = cfg.temperature_c;
let device = cfg.device;
Expand All @@ -678,7 +679,6 @@ pub fn plot_ir(state: &ServerState, cmd: &Value) -> Value {
// `gated_points` correction step below for why: `MicCurveFir`'s
// linear-phase group delay would otherwise move the IR peak the
// gate is anchored to.
let cal = Calibration::load(out_ch, in_ch, None).ok().flatten();
let mic_curve_opt = cal.as_ref().and_then(|c| c.mic_response.clone());

if let Err(e) = eng.start(&[out_port], Some(&in_port)) {
Expand Down
44 changes: 43 additions & 1 deletion ac-rs/crates/ac-daemon/src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::thread;
use serde_json::{json, Value};

use ac_core::config::Config;
use ac_core::shared::calibration::Calibration;
use ac_core::shared::calibration::{default_cal_path, Calibration};

use crate::audio::AudioEngine;
use crate::server::ServerState;
Expand Down Expand Up @@ -115,6 +115,48 @@ macro_rules! cfg_guard {
}
pub(super) use cfg_guard;

// ---------------------------------------------------------------------------
// Calibration-freshness guard (#425)
// ---------------------------------------------------------------------------

/// Load one calibration entry, refusing the operation if the shared store
/// exists but cannot be read or parsed. A read error is not "no calibration":
/// continuing would silently relabel an uncalibrated result as successful.
pub(super) fn load_calibration_or_refuse(
output_channel: u32,
input_channel: u32,
operation: &str,
scope: Option<&str>,
) -> Result<Option<Calibration>, String> {
Calibration::load(output_channel, input_channel, None).map_err(|e| {
let scope = scope
.map(|value| format!("\n scope {value}"))
.unwrap_or_default();
format!(
"calibration unreadable — {operation} not started{scope}\n\
\x20 store {}\n\
\x20 cause {e:#}\n\
\x20 data existing file preserved",
default_cal_path().display()
)
})
}

macro_rules! cal_guard {
($output_channel:expr, $input_channel:expr) => {
match $crate::handlers::load_calibration_or_refuse(
$output_channel,
$input_channel,
"measurement",
None,
) {
Ok(cal) => cal,
Err(msg) => return ::serde_json::json!({"ok": false, "error": msg}),
}
};
}
pub(super) use cal_guard;

// ---------------------------------------------------------------------------
// Worker spawn
// ---------------------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions ac-rs/crates/ac-daemon/src/handlers/test_dut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use crate::handlers::mic;
use crate::server::ServerState;

use super::{
busy_guard, cal_dbu_str, cal_out_dbu_str, capture_rms, cfg_guard, make_engine_for_state,
median, ref_output_migration_warning, resolve_input, resolve_output, resolve_ref_input,
resolve_ref_output, rms_to_dbfs, send_pub, spawn_worker, TestResult,
busy_guard, cal_dbu_str, cal_guard, cal_out_dbu_str, capture_rms, cfg_guard,
make_engine_for_state, median, ref_output_migration_warning, resolve_input, resolve_output,
resolve_ref_input, resolve_ref_output, rms_to_dbfs, send_pub, spawn_worker, TestResult,
};

pub fn test_dut(state: &ServerState, cmd: &Value) -> Value {
Expand Down Expand Up @@ -55,6 +55,7 @@ pub fn test_dut(state: &ServerState, cmd: &Value) -> Value {
};
let out_ch = cfg.output_channel;
let in_ch = cfg.input_channel;
let cal = cal_guard!(out_ch, in_ch);

let pub_tx = state.pub_tx.clone();
let mut eng = match make_engine_for_state(state) {
Expand Down Expand Up @@ -101,7 +102,6 @@ pub fn test_dut(state: &ServerState, cmd: &Value) -> Value {
}

let sr = eng.sample_rate();
let cal = Calibration::load(out_ch, in_ch, None).ok().flatten();
let mic_curve_loaded = cal
.as_ref()
.map(|c| c.mic_response.is_some())
Expand Down
12 changes: 5 additions & 7 deletions ac-rs/crates/ac-daemon/src/handlers/test_hw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::handlers::mic;
use crate::server::ServerState;

use super::{
analyze_mono, busy_guard, capture_rms, cfg_guard, make_engine_for_state, read_dmm_vrms,
ref_output_migration_warning, resolve_input, resolve_output, resolve_ref_input,
analyze_mono, busy_guard, cal_guard, capture_rms, cfg_guard, make_engine_for_state,
read_dmm_vrms, ref_output_migration_warning, resolve_input, resolve_output, resolve_ref_input,
resolve_ref_output, rms_to_dbfs, send_pub, spawn_worker, std_dev, TestResult,
};

Expand Down Expand Up @@ -59,6 +59,7 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value {
let dmm_host = cfg.dmm_host.clone();
let out_ch = cfg.output_channel;
let in_ch = cfg.input_channel;
let cal_ctx = cal_guard!(out_ch, in_ch);
let mic_corr_enabled = state.mic_correction_enabled.clone();

let out_port_r = out_port.clone();
Expand Down Expand Up @@ -101,7 +102,6 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value {
// once per worker, stamped on every emitted `test_result` so
// downstream readers can tell whether the test ran on a
// mic-curve'd channel and at what SPL offset (#103).
let cal_ctx = Calibration::load(out_ch, in_ch, None).ok().flatten();
let mic_curve_loaded = cal_ctx
.as_ref()
.map(|c| c.mic_response.is_some())
Expand Down Expand Up @@ -151,8 +151,6 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value {
let mut dmm_pass = 0usize;
if dmm_mode {
if let Some(ref host) = dmm_host {
let cal = Calibration::load(out_ch, in_ch, None).ok().flatten();

macro_rules! emit_dmm {
($r:expr) => {{
if $r.pass { dmm_pass += 1; }
Expand All @@ -167,10 +165,10 @@ pub fn test_hardware(state: &ServerState, cmd: &Value) -> Value {
}

if !stop.load(Ordering::Relaxed) {
emit_dmm!(hw_dmm_absolute(&mut *eng, host, cal.as_ref()));
emit_dmm!(hw_dmm_absolute(&mut *eng, host, cal_ctx.as_ref()));
}
if !stop.load(Ordering::Relaxed) {
emit_dmm!(hw_dmm_tracking(&mut *eng, host, cal.as_ref()));
emit_dmm!(hw_dmm_tracking(&mut *eng, host, cal_ctx.as_ref()));
}
if !stop.load(Ordering::Relaxed) {
emit_dmm!(hw_dmm_freq_response(&mut *eng, host));
Expand Down
11 changes: 7 additions & 4 deletions ac-rs/crates/ac-daemon/src/handlers/transfer/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ use ac_core::shared::calibration::Calibration;
use ac_core::visualize::weighting_curves::WeightingCurve;

use crate::handlers::{
apply_drive_ceiling, make_engine_for_state, ref_output_migration_warning, resolve_output,
resolve_ref_output, selected_backend_is_fake,
apply_drive_ceiling, load_calibration_or_refuse, make_engine_for_state,
ref_output_migration_warning, resolve_output, resolve_ref_output, selected_backend_is_fake,
};
use crate::server::ServerState;

Expand Down Expand Up @@ -162,8 +162,11 @@ impl SessionPlan {
// the exact split this check exists to prevent.
let unique_cals: Vec<Option<Calibration>> = unique_chans
.iter()
.map(|&ch| Calibration::load(out_ch, ch, None).ok().flatten())
.collect();
.map(|&ch| {
load_calibration_or_refuse(out_ch, ch, "transfer", Some("all requested pairs"))
})
.collect::<Result<_, _>>()
.map_err(|msg| json!({"ok": false, "error": msg}))?;
// Every channel named in `pairs` is in `unique_chans` by
// construction, so this never misses: `None` means "no calibration
// stored for this channel", never "channel not found".
Expand Down
Loading