test(host): harden Phase 9 live rig
This commit is contained in:
+57
-15
@@ -1020,7 +1020,8 @@ mod tests {
|
||||
const PN_CHIP_FRAMES: usize = 24;
|
||||
const PN_SEQUENCE_CHIPS: usize = 4_096;
|
||||
const PN_WINDOW_CHIPS: usize = 1_024;
|
||||
const PN_FILE_SECONDS: usize = 8;
|
||||
const PN_FILE_SECONDS: usize = 12;
|
||||
const PN_CAPTURE_SECONDS: u64 = 6;
|
||||
const PN_SKIP_FRAMES: usize = PN_SAMPLE_RATE / 2;
|
||||
const DESKTOP_PN_SEED: u64 = 0x1357_9bdf;
|
||||
const REMOTE_PN_SEED: u64 = 0x2468_ace1;
|
||||
@@ -1630,30 +1631,36 @@ mod tests {
|
||||
Ok(errors)
|
||||
}
|
||||
|
||||
fn node_ids_named(names: &[&str]) -> Result<HashSet<u64>> {
|
||||
let objects = pw_dump_objects("the Phase-9 xrun node set")?;
|
||||
fn matching_node_ids(objects: &[serde_json::Value], names: &[&str]) -> Result<HashSet<u64>> {
|
||||
let wanted = names.iter().copied().collect::<HashSet<_>>();
|
||||
let mut seen_names = HashSet::new();
|
||||
let found = objects
|
||||
.iter()
|
||||
.filter_map(|object| {
|
||||
let name = object
|
||||
.pointer("/info/props/node.name")
|
||||
.and_then(serde_json::Value::as_str)?;
|
||||
wanted
|
||||
.contains(name)
|
||||
.then(|| object.get("id").and_then(value_u64))?
|
||||
if !wanted.contains(name) {
|
||||
return None;
|
||||
}
|
||||
let id = object.get("id").and_then(value_u64)?;
|
||||
seen_names.insert(name);
|
||||
Some(id)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
if found.len() != wanted.len() {
|
||||
bail!(
|
||||
"Phase-9 xrun snapshot resolved {} of {} required nodes ({names:?})",
|
||||
found.len(),
|
||||
wanted.len()
|
||||
);
|
||||
let mut missing = wanted.difference(&seen_names).copied().collect::<Vec<_>>();
|
||||
if !missing.is_empty() {
|
||||
missing.sort_unstable();
|
||||
bail!("Phase-9 xrun snapshot did not resolve required node names: {missing:?}");
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn node_ids_named(names: &[&str]) -> Result<HashSet<u64>> {
|
||||
let objects = pw_dump_objects("the Phase-9 xrun node set")?;
|
||||
matching_node_ids(&objects, names)
|
||||
}
|
||||
|
||||
fn xrun_delta(
|
||||
before: &HashMap<u64, u64>,
|
||||
after: &HashMap<u64, u64>,
|
||||
@@ -1671,7 +1678,11 @@ mod tests {
|
||||
.sum()
|
||||
}
|
||||
|
||||
async fn record_capture(monitor_name: &str, label: &str) -> Result<Vec<u8>> {
|
||||
async fn record_capture_for(
|
||||
monitor_name: &str,
|
||||
label: &str,
|
||||
duration: Duration,
|
||||
) -> Result<Vec<u8>> {
|
||||
let raw = RawCaptureFile::new(label);
|
||||
let output_file =
|
||||
std::fs::File::create(&raw.0).with_context(|| format!("create {}", raw.0.display()))?;
|
||||
@@ -1690,12 +1701,16 @@ mod tests {
|
||||
.stdout(Stdio::from(output_file))
|
||||
.stderr(Stdio::piped());
|
||||
let mut recorder = ContainedProcess::spawn(&mut command, "Phase-6 parec recorder")?;
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
tokio::time::sleep(duration).await;
|
||||
recorder.ensure_running()?;
|
||||
recorder.stop()?;
|
||||
std::fs::read(&raw.0).with_context(|| format!("read {}", raw.0.display()))
|
||||
}
|
||||
|
||||
async fn record_capture(monitor_name: &str, label: &str) -> Result<Vec<u8>> {
|
||||
record_capture_for(monitor_name, label, Duration::from_secs(3)).await
|
||||
}
|
||||
|
||||
fn tone_dbfs(raw: &[u8], frequency: f64) -> Result<f64> {
|
||||
const SAMPLE_RATE: f64 = 48_000.0;
|
||||
const CHANNELS: usize = 2;
|
||||
@@ -1935,7 +1950,12 @@ mod tests {
|
||||
}
|
||||
let relevant_xrun_nodes = node_ids_named(&xrun_names)?;
|
||||
let xruns_before = pw_top_errors()?;
|
||||
let raw = record_capture(&monitor_name, &format!("phase9-{}", arm.label)).await?;
|
||||
let raw = record_capture_for(
|
||||
&monitor_name,
|
||||
&format!("phase9-{}", arm.label),
|
||||
Duration::from_secs(PN_CAPTURE_SECONDS),
|
||||
)
|
||||
.await?;
|
||||
let xruns_after = pw_top_errors()?;
|
||||
|
||||
// Original routes must still be intact after the measurement, not only
|
||||
@@ -2014,6 +2034,28 @@ mod tests {
|
||||
assert!(max_cyclic_correlation(&vec![0.0; PN_WINDOW_CHIPS], &desktop) == 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase9_xrun_node_resolution_accepts_duplicate_names_but_not_missing_names() {
|
||||
let objects = serde_json::json!([
|
||||
{"id": 11, "info": {"props": {"node.name": "controlled"}}},
|
||||
{"id": 12, "info": {"props": {"node.name": "controlled"}}},
|
||||
{"id": 13, "info": {"props": {"node.name": "playback"}}},
|
||||
{"id": 14, "info": {"props": {"node.name": "unrelated"}}}
|
||||
]);
|
||||
let objects = objects.as_array().expect("fixture is an array");
|
||||
assert_eq!(
|
||||
matching_node_ids(objects, &["controlled", "playback"])
|
||||
.expect("all required names are represented"),
|
||||
HashSet::from([11, 12, 13])
|
||||
);
|
||||
assert!(
|
||||
matching_node_ids(objects, &["controlled", "missing"])
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("missing")
|
||||
);
|
||||
}
|
||||
|
||||
/// Phase-9 rig upgrade: the Phase-6 production-path topology is driven by
|
||||
/// two pinned, independently generated PN probes. Every non-overlapping
|
||||
/// half-second window is normalized and correlated separately on left and
|
||||
|
||||
Reference in New Issue
Block a user