style: apply cargo fmt across the crate (A20)
The repo never enforced rustfmt, so formatting had drifted broadly. This is a single mechanical `cargo fmt` pass over the whole crate (no behavioral change; lib suite green, 493 passed). Going forward fmt should be enforced (planned CI fmt --check step). Part of the 0.6.1 hygiene pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+53
-12
@@ -578,7 +578,9 @@ fn choose_config(device: &Device, output: bool) -> Result<cpal::SupportedStreamC
|
||||
let pick = |channels: Option<u16>| {
|
||||
ranges
|
||||
.iter()
|
||||
.find(|r| usable_range(r) && supports_48k(r) && channels.is_none_or(|c| r.channels() == c))
|
||||
.find(|r| {
|
||||
usable_range(r) && supports_48k(r) && channels.is_none_or(|c| r.channels() == c)
|
||||
})
|
||||
.cloned()
|
||||
};
|
||||
|
||||
@@ -666,15 +668,30 @@ fn run_capture(
|
||||
let device_rate = config.sample_rate.0;
|
||||
let stream = match sample_format {
|
||||
SampleFormat::F32 => build_input::<f32, _>(
|
||||
&device, &config, producer, channels, overrun.clone(), callbacks.clone(),
|
||||
&device,
|
||||
&config,
|
||||
producer,
|
||||
channels,
|
||||
overrun.clone(),
|
||||
callbacks.clone(),
|
||||
err_code.clone(),
|
||||
),
|
||||
SampleFormat::I16 => build_input::<i16, _>(
|
||||
&device, &config, producer, channels, overrun.clone(), callbacks.clone(),
|
||||
&device,
|
||||
&config,
|
||||
producer,
|
||||
channels,
|
||||
overrun.clone(),
|
||||
callbacks.clone(),
|
||||
err_code.clone(),
|
||||
),
|
||||
SampleFormat::U16 => build_input::<u16, _>(
|
||||
&device, &config, producer, channels, overrun.clone(), callbacks.clone(),
|
||||
&device,
|
||||
&config,
|
||||
producer,
|
||||
channels,
|
||||
overrun.clone(),
|
||||
callbacks.clone(),
|
||||
err_code.clone(),
|
||||
),
|
||||
other => Err(AudioError::Stream(format!(
|
||||
@@ -764,7 +781,10 @@ fn run_capture(
|
||||
// Surface a stream error the RT callback flagged (it can't log itself).
|
||||
let ec = err_code.load(Ordering::Relaxed);
|
||||
if ec != STREAM_ERR_NONE && ec != last_err {
|
||||
crate::log_msg(&format!("cpal capture stream error: {}", stream_err_text(ec)));
|
||||
crate::log_msg(&format!(
|
||||
"cpal capture stream error: {}",
|
||||
stream_err_text(ec)
|
||||
));
|
||||
last_err = ec;
|
||||
}
|
||||
if !drained {
|
||||
@@ -914,16 +934,34 @@ fn run_playback(
|
||||
let device_rate = config.sample_rate.0;
|
||||
let stream = match sample_format {
|
||||
SampleFormat::F32 => build_output::<f32, _>(
|
||||
&device, &config, consumer, ring_fill.clone(), underrun.clone(),
|
||||
max_cb.clone(), callbacks.clone(), err_code.clone(),
|
||||
&device,
|
||||
&config,
|
||||
consumer,
|
||||
ring_fill.clone(),
|
||||
underrun.clone(),
|
||||
max_cb.clone(),
|
||||
callbacks.clone(),
|
||||
err_code.clone(),
|
||||
),
|
||||
SampleFormat::I16 => build_output::<i16, _>(
|
||||
&device, &config, consumer, ring_fill.clone(), underrun.clone(),
|
||||
max_cb.clone(), callbacks.clone(), err_code.clone(),
|
||||
&device,
|
||||
&config,
|
||||
consumer,
|
||||
ring_fill.clone(),
|
||||
underrun.clone(),
|
||||
max_cb.clone(),
|
||||
callbacks.clone(),
|
||||
err_code.clone(),
|
||||
),
|
||||
SampleFormat::U16 => build_output::<u16, _>(
|
||||
&device, &config, consumer, ring_fill.clone(), underrun.clone(),
|
||||
max_cb.clone(), callbacks.clone(), err_code.clone(),
|
||||
&device,
|
||||
&config,
|
||||
consumer,
|
||||
ring_fill.clone(),
|
||||
underrun.clone(),
|
||||
max_cb.clone(),
|
||||
callbacks.clone(),
|
||||
err_code.clone(),
|
||||
),
|
||||
other => Err(AudioError::Stream(format!(
|
||||
"unsupported playback sample format: {other:?}"
|
||||
@@ -1199,7 +1237,10 @@ fn spawn_health_logger(
|
||||
// Surface a stream error the RT callback flagged (it can't log itself).
|
||||
let ec = err_code.load(Ordering::Relaxed);
|
||||
if ec != STREAM_ERR_NONE && ec != last_err {
|
||||
crate::log_msg(&format!("cpal playback stream error: {}", stream_err_text(ec)));
|
||||
crate::log_msg(&format!(
|
||||
"cpal playback stream error: {}",
|
||||
stream_err_text(ec)
|
||||
));
|
||||
last_err = ec;
|
||||
}
|
||||
// Report the device's per-cycle demand (in internal 48 kHz-stereo
|
||||
|
||||
+49
-11
@@ -54,7 +54,10 @@ impl Drop for EchoCancelGuard {
|
||||
.arg("unload-module")
|
||||
.arg(&self.module_index)
|
||||
.output();
|
||||
crate::log_msg(&format!("Echo cancel: unloaded module {}", self.module_index));
|
||||
crate::log_msg(&format!(
|
||||
"Echo cancel: unloaded module {}",
|
||||
self.module_index
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +68,10 @@ impl Drop for EchoCancelGuard {
|
||||
/// `None` (or an empty string) to bind to the system defaults. Returns `Err` with
|
||||
/// a human-readable reason if `pactl` is missing, the load fails, or the nodes
|
||||
/// don't appear — the caller should fall back to the direct devices.
|
||||
pub fn enable(real_source: Option<&str>, real_sink: Option<&str>) -> Result<EchoCancelGuard, String> {
|
||||
pub fn enable(
|
||||
real_source: Option<&str>,
|
||||
real_sink: Option<&str>,
|
||||
) -> Result<EchoCancelGuard, String> {
|
||||
// Best-effort: clear any stale instance left by a crashed prior run so we
|
||||
// don't stack duplicate modules / fight over the virtual node names.
|
||||
unload_stale();
|
||||
@@ -101,7 +107,11 @@ pub fn enable(real_source: Option<&str>, real_sink: Option<&str>) -> Result<Echo
|
||||
if module_index.parse::<u64>().is_err() {
|
||||
return Err(format!("unexpected pactl output: {module_index:?}"));
|
||||
}
|
||||
let guard = EchoCancelGuard { module_index, source_name, sink_name };
|
||||
let guard = EchoCancelGuard {
|
||||
module_index,
|
||||
source_name,
|
||||
sink_name,
|
||||
};
|
||||
|
||||
// The virtual nodes appear shortly after the module loads; wait for both so
|
||||
// the subsequent capture/playback streams can actually target them. If they
|
||||
@@ -134,7 +144,12 @@ fn wait_for_nodes(source_name: &str, sink_name: &str) -> bool {
|
||||
/// Whether `pactl list <kind> short` lists a node named `name`.
|
||||
/// `kind` is "sources" or "sinks".
|
||||
fn node_present(kind: &str, name: &str) -> bool {
|
||||
let Ok(out) = Command::new("pactl").arg("list").arg(kind).arg("short").output() else {
|
||||
let Ok(out) = Command::new("pactl")
|
||||
.arg("list")
|
||||
.arg(kind)
|
||||
.arg("short")
|
||||
.output()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
@@ -167,7 +182,12 @@ fn process_is_alive(_pid: u32) -> bool {
|
||||
/// Unloads leftover PeerSpeak `module-echo-cancel` instances only when their
|
||||
/// owning process is gone. Best-effort and conservative on non-Linux platforms.
|
||||
fn unload_stale() {
|
||||
let Ok(out) = Command::new("pactl").arg("list").arg("modules").arg("short").output() else {
|
||||
let Ok(out) = Command::new("pactl")
|
||||
.arg("list")
|
||||
.arg("modules")
|
||||
.arg("short")
|
||||
.output()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
||||
@@ -179,7 +199,10 @@ fn unload_stale() {
|
||||
&& ec_module_is_stale(args, process_is_alive)
|
||||
&& index.parse::<u64>().is_ok()
|
||||
{
|
||||
let _ = Command::new("pactl").arg("unload-module").arg(index).output();
|
||||
let _ = Command::new("pactl")
|
||||
.arg("unload-module")
|
||||
.arg(index)
|
||||
.output();
|
||||
crate::log_msg(&format!("Echo cancel: cleaned up stale module {index}"));
|
||||
}
|
||||
}
|
||||
@@ -198,13 +221,25 @@ mod tests {
|
||||
let guard = enable(None, None).expect("module-echo-cancel should load");
|
||||
let source_name = guard.source_name().to_string();
|
||||
let sink_name = guard.sink_name().to_string();
|
||||
assert!(node_present("sources", &source_name), "cleaned source must exist");
|
||||
assert!(node_present("sinks", &sink_name), "reference sink must exist");
|
||||
assert!(
|
||||
node_present("sources", &source_name),
|
||||
"cleaned source must exist"
|
||||
);
|
||||
assert!(
|
||||
node_present("sinks", &sink_name),
|
||||
"reference sink must exist"
|
||||
);
|
||||
drop(guard);
|
||||
// Give pactl a moment to tear the nodes down.
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
assert!(!node_present("sources", &source_name), "source must be gone after unload");
|
||||
assert!(!node_present("sinks", &sink_name), "sink must be gone after unload");
|
||||
assert!(
|
||||
!node_present("sources", &source_name),
|
||||
"source must be gone after unload"
|
||||
);
|
||||
assert!(
|
||||
!node_present("sinks", &sink_name),
|
||||
"sink must be gone after unload"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -220,7 +255,10 @@ mod tests {
|
||||
pid_from_ec_args("source_name=peerspeak_echocancel_source.not-a-pid"),
|
||||
None
|
||||
);
|
||||
assert_eq!(pid_from_ec_args("source_name=someone_elses_source.4242"), None);
|
||||
assert_eq!(
|
||||
pid_from_ec_args("source_name=someone_elses_source.4242"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+17
-9
@@ -251,7 +251,10 @@ mod tests {
|
||||
let before = rms(&low);
|
||||
eq.process_frame(&mut low);
|
||||
let after = rms(&low);
|
||||
assert!(after > before * 1.6, "low shelf should boost low RMS: {before} -> {after}");
|
||||
assert!(
|
||||
after > before * 1.6,
|
||||
"low shelf should boost low RMS: {before} -> {after}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -264,7 +267,10 @@ mod tests {
|
||||
let before = rms(&high);
|
||||
eq.process_frame(&mut high);
|
||||
let after = rms(&high);
|
||||
assert!(after > before * 1.6, "high shelf should boost high RMS: {before} -> {after}");
|
||||
assert!(
|
||||
after > before * 1.6,
|
||||
"high shelf should boost high RMS: {before} -> {after}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +281,10 @@ mod tests {
|
||||
Biquad::peaking(DEFAULT_SAMPLE_RATE, MID_PEAK_HZ, gain, MID_Q),
|
||||
Biquad::high_shelf(DEFAULT_SAMPLE_RATE, HIGH_SHELF_HZ, gain, SHELF_Q),
|
||||
] {
|
||||
assert!(b.coeffs.all_finite(), "coefficients must be finite at {gain} dB");
|
||||
assert!(
|
||||
b.coeffs.all_finite(),
|
||||
"coefficients must be finite at {gain} dB"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,12 +298,11 @@ mod tests {
|
||||
});
|
||||
let mut frame = sine(1_000.0, 48_000, 30_000.0);
|
||||
eq.process_frame(&mut frame);
|
||||
let peak = frame
|
||||
.iter()
|
||||
.map(|&s| i32::from(s).abs())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
assert!(peak > 1_000, "processed signal should retain audible energy");
|
||||
let peak = frame.iter().map(|&s| i32::from(s).abs()).max().unwrap_or(0);
|
||||
assert!(
|
||||
peak > 1_000,
|
||||
"processed signal should retain audible energy"
|
||||
);
|
||||
assert!(
|
||||
frame.iter().any(|&s| s > 0) && frame.iter().any(|&s| s < 0),
|
||||
"a boosted sine should retain both polarities"
|
||||
|
||||
+51
-12
@@ -169,7 +169,10 @@ mod tests {
|
||||
assert!(g.process(&mut f, 0.05), "loud frame must transmit");
|
||||
last = peak(&f);
|
||||
}
|
||||
assert!(last >= 9900, "gain should reach ~1.0 on sustained loud input, got peak {last}");
|
||||
assert!(
|
||||
last >= 9900,
|
||||
"gain should reach ~1.0 on sustained loud input, got peak {last}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -179,8 +182,15 @@ mod tests {
|
||||
g.process(&mut f, 0.05);
|
||||
// 5ms attack @48k = 240 samples; across a 960-sample frame the gain ramps
|
||||
// 0->1, so the early samples are well below full scale (no instant click).
|
||||
assert!(f[0].abs() < 5000, "attack should start near zero, got {}", f[0]);
|
||||
assert!(f[FRAME - 1].abs() > 9000, "attack should complete within the frame");
|
||||
assert!(
|
||||
f[0].abs() < 5000,
|
||||
"attack should start near zero, got {}",
|
||||
f[0]
|
||||
);
|
||||
assert!(
|
||||
f[FRAME - 1].abs() > 9000,
|
||||
"attack should complete within the frame"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -193,8 +203,14 @@ mod tests {
|
||||
}
|
||||
// First quiet frame right after speech: hold keeps it open (not chopped).
|
||||
let mut q = frame(50); // rms ~0.0015, below close (0.03)
|
||||
assert!(g.process(&mut q, 0.05), "first quiet frame must stay open (hangover)");
|
||||
assert!(peak(&q) > 0, "held-open frame must not be silenced immediately");
|
||||
assert!(
|
||||
g.process(&mut q, 0.05),
|
||||
"first quiet frame must stay open (hangover)"
|
||||
);
|
||||
assert!(
|
||||
peak(&q) > 0,
|
||||
"held-open frame must not be silenced immediately"
|
||||
);
|
||||
|
||||
// Hold is 200ms = 10 frames; keep feeding quiet until it fully closes.
|
||||
let mut closed = false;
|
||||
@@ -205,7 +221,10 @@ mod tests {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(closed, "gate must eventually close and stop transmitting after sustained silence");
|
||||
assert!(
|
||||
closed,
|
||||
"gate must eventually close and stop transmitting after sustained silence"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -216,8 +235,14 @@ mod tests {
|
||||
g.process(&mut f, 0.05); // open=0.05, close=0.03
|
||||
// A frame between close and open thresholds: rms ~0.04 (amp ~1310).
|
||||
let mut mid = frame(1310);
|
||||
assert!(g.process(&mut mid, 0.05), "between-threshold frame must keep an open gate open");
|
||||
assert!(g.open, "hysteresis: gate stays open above the close threshold");
|
||||
assert!(
|
||||
g.process(&mut mid, 0.05),
|
||||
"between-threshold frame must keep an open gate open"
|
||||
);
|
||||
assert!(
|
||||
g.open,
|
||||
"hysteresis: gate stays open above the close threshold"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -225,7 +250,10 @@ mod tests {
|
||||
let mut g = NoiseGate::new(SR);
|
||||
// Never opened; feed silence — should report don't-transmit promptly.
|
||||
let mut f = frame(0);
|
||||
assert!(!g.process(&mut f, 0.05), "an unopened gate on silence must not transmit");
|
||||
assert!(
|
||||
!g.process(&mut f, 0.05),
|
||||
"an unopened gate on silence must not transmit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -266,7 +294,11 @@ mod tests {
|
||||
|
||||
let mut f2 = frame(10000);
|
||||
assert!(g.process(&mut f2, 0.05)); // enabled
|
||||
assert!(f2[0].abs() > 9000, "expected first sample of enabled frame to have no fade-in, got {}", f2[0]);
|
||||
assert!(
|
||||
f2[0].abs() > 9000,
|
||||
"expected first sample of enabled frame to have no fade-in, got {}",
|
||||
f2[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -302,7 +334,10 @@ mod tests {
|
||||
let mut f = frame(1310);
|
||||
assert!(g.process(&mut f, 0.05));
|
||||
}
|
||||
assert!(g.open, "gate must stay open (hold refreshed by mid-level input)");
|
||||
assert!(
|
||||
g.open,
|
||||
"gate must stay open (hold refreshed by mid-level input)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -333,6 +368,10 @@ mod tests {
|
||||
last_peak = peak(&f);
|
||||
}
|
||||
assert!(g.open);
|
||||
assert!(last_peak >= 9900, "peak of the 3rd reopened frame must be >= 9900, got {}", last_peak);
|
||||
assert!(
|
||||
last_peak >= 9900,
|
||||
"peak of the 3rd reopened frame must be >= 9900, got {}",
|
||||
last_peak
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+69
-15
@@ -123,7 +123,10 @@ mod tests {
|
||||
let out = lim.process(&loud, 1.0);
|
||||
let ceiling = lim.ceiling().ceil() as i16;
|
||||
for &s in &out {
|
||||
assert!(s > 0, "positive loud input stays positive (no wrap), got {s}");
|
||||
assert!(
|
||||
s > 0,
|
||||
"positive loud input stays positive (no wrap), got {s}"
|
||||
);
|
||||
assert!(s <= ceiling, "sample {s} exceeded ceiling {ceiling}");
|
||||
}
|
||||
}
|
||||
@@ -175,7 +178,10 @@ mod tests {
|
||||
let out_pos = lim.process(&pos_loud, 1.0);
|
||||
for &s in &out_pos {
|
||||
assert!(s > 0, "positive input stays positive, got {s}");
|
||||
assert!(s <= ceiling_ceil, "positive sample {s} exceeded ceiling {ceiling_ceil}");
|
||||
assert!(
|
||||
s <= ceiling_ceil,
|
||||
"positive sample {s} exceeded ceiling {ceiling_ceil}"
|
||||
);
|
||||
}
|
||||
|
||||
// Sustained negative loud sum
|
||||
@@ -185,7 +191,10 @@ mod tests {
|
||||
let neg_ceiling = -ceiling_ceil;
|
||||
for &s in &out_neg {
|
||||
assert!(s < 0, "negative input stays negative, got {s}");
|
||||
assert!(s >= neg_ceiling, "negative sample {s} exceeded negative ceiling {neg_ceiling}");
|
||||
assert!(
|
||||
s >= neg_ceiling,
|
||||
"negative sample {s} exceeded negative ceiling {neg_ceiling}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +209,14 @@ mod tests {
|
||||
let out = lim.process(&input, 8.0);
|
||||
for &s in &out {
|
||||
assert!(s > 0, "positive stays positive");
|
||||
assert!(s <= ceiling_ceil, "sample {s} must be limited to ceiling {ceiling_ceil}");
|
||||
assert!((s - ceiling_ceil).abs() <= 2, "sample {s} should ride the ceiling {ceiling_ceil}");
|
||||
assert!(
|
||||
s <= ceiling_ceil,
|
||||
"sample {s} must be limited to ceiling {ceiling_ceil}"
|
||||
);
|
||||
assert!(
|
||||
(s - ceiling_ceil).abs() <= 2,
|
||||
"sample {s} should ride the ceiling {ceiling_ceil}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +228,10 @@ mod tests {
|
||||
let out = lim.process(&input, 0.5);
|
||||
for (i, &s) in out.iter().enumerate() {
|
||||
let expected = (input[i] as f32 * 0.5).round() as i16;
|
||||
assert!((s - expected).abs() <= 1, "sample {s} should be close to expected {expected}");
|
||||
assert!(
|
||||
(s - expected).abs() <= 1,
|
||||
"sample {s} should be close to expected {expected}"
|
||||
);
|
||||
}
|
||||
|
||||
// Subsequently feed a new sample at unity gain. It must be transparent,
|
||||
@@ -230,7 +248,12 @@ mod tests {
|
||||
|
||||
let loud = vec![200_000i32; 10];
|
||||
let out = lim.process(&loud, 1.0);
|
||||
assert!(out[0] <= ceiling_ceil, "first sample {} must not overshoot ceiling {}", out[0], ceiling_ceil);
|
||||
assert!(
|
||||
out[0] <= ceiling_ceil,
|
||||
"first sample {} must not overshoot ceiling {}",
|
||||
out[0],
|
||||
ceiling_ceil
|
||||
);
|
||||
}
|
||||
|
||||
/// 5. Release direction & monotonicity.
|
||||
@@ -247,13 +270,23 @@ mod tests {
|
||||
|
||||
// Output should be monotonic (non-decreasing)
|
||||
for i in 1..out.len() {
|
||||
assert!(out[i] >= out[i - 1], "output must be monotonic; index {} was {}, index {} was {}", i - 1, out[i - 1], i, out[i]);
|
||||
assert!(
|
||||
out[i] >= out[i - 1],
|
||||
"output must be monotonic; index {} was {}, index {} was {}",
|
||||
i - 1,
|
||||
out[i - 1],
|
||||
i,
|
||||
out[i]
|
||||
);
|
||||
}
|
||||
|
||||
// The end sample should be closer to the original input than the start sample
|
||||
let start_diff = (mid_val as i16 - out[0]).abs();
|
||||
let end_diff = (mid_val as i16 - *out.last().unwrap()).abs();
|
||||
assert!(end_diff < start_diff, "end diff {end_diff} should be smaller than start diff {start_diff}");
|
||||
assert!(
|
||||
end_diff < start_diff,
|
||||
"end diff {end_diff} should be smaller than start diff {start_diff}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 6. Release is gradual, not instantaneous.
|
||||
@@ -265,7 +298,11 @@ mod tests {
|
||||
|
||||
// Immediately follow with a sub-ceiling sample
|
||||
let out = lim.process(&[10_000i32], 1.0);
|
||||
assert!(out[0] < 10_000, "first quiet sample should still be attenuated (got {})", out[0]);
|
||||
assert!(
|
||||
out[0] < 10_000,
|
||||
"first quiet sample should still be attenuated (got {})",
|
||||
out[0]
|
||||
);
|
||||
}
|
||||
|
||||
/// 7. State carries across process calls.
|
||||
@@ -287,7 +324,10 @@ mod tests {
|
||||
let mut out_split = out_split1;
|
||||
out_split.extend(&out_split2);
|
||||
|
||||
assert_eq!(out_single, out_split, "splitting process calls must produce identical output to a single call");
|
||||
assert_eq!(
|
||||
out_single, out_split,
|
||||
"splitting process calls must produce identical output to a single call"
|
||||
);
|
||||
|
||||
// Test 2: Pre-loaded limiter vs fresh limiter on the same input
|
||||
let mut lim_preloaded = SoftLimiter::new(SR);
|
||||
@@ -299,8 +339,16 @@ mod tests {
|
||||
let out_preloaded = lim_preloaded.process(&test_input, 1.0);
|
||||
let out_fresh = lim_fresh.process(&test_input, 1.0);
|
||||
|
||||
assert_ne!(out_preloaded, out_fresh, "pre-loaded and fresh limiter outputs should differ");
|
||||
assert!(out_preloaded[0] < out_fresh[0], "pre-loaded limiter first sample {} should be smaller than fresh limiter first sample {}", out_preloaded[0], out_fresh[0]);
|
||||
assert_ne!(
|
||||
out_preloaded, out_fresh,
|
||||
"pre-loaded and fresh limiter outputs should differ"
|
||||
);
|
||||
assert!(
|
||||
out_preloaded[0] < out_fresh[0],
|
||||
"pre-loaded limiter first sample {} should be smaller than fresh limiter first sample {}",
|
||||
out_preloaded[0],
|
||||
out_fresh[0]
|
||||
);
|
||||
}
|
||||
|
||||
/// 8. Empty input.
|
||||
@@ -320,7 +368,10 @@ mod tests {
|
||||
// Gain 0.0
|
||||
let out_zero = lim.process(&input, 0.0);
|
||||
assert_eq!(out_zero.len(), input.len());
|
||||
assert!(out_zero.iter().all(|&s| s == 0), "0.0 gain should result in all zeros");
|
||||
assert!(
|
||||
out_zero.iter().all(|&s| s == 0),
|
||||
"0.0 gain should result in all zeros"
|
||||
);
|
||||
|
||||
// Gain 1.0
|
||||
let out_unity = lim.process(&input, 1.0);
|
||||
@@ -354,6 +405,9 @@ mod tests {
|
||||
|
||||
let out = lim.process(&input, 1.0);
|
||||
let expected: Vec<i16> = input.iter().map(|&s| s as i16).collect();
|
||||
assert_eq!(out, expected, "below ceiling input must be bit-exact at unity gain");
|
||||
assert_eq!(
|
||||
out, expected,
|
||||
"below ceiling input must be bit-exact at unity gain"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -1,6 +1,6 @@
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Playback output channel count. Capture/encode/network remain mono; only the
|
||||
@@ -35,7 +35,11 @@ pub enum AudioError {
|
||||
pub trait AudioBackend: Send + Sync {
|
||||
/// Starts capturing raw PCM audio from the input device (microphone),
|
||||
/// sending chunks of samples (e.g. `Vec<i16>`) to the provided Sender.
|
||||
fn start_capture(&self, tx: Sender<Vec<i16>>, target_node: Option<String>) -> Result<(), AudioError>;
|
||||
fn start_capture(
|
||||
&self,
|
||||
tx: Sender<Vec<i16>>,
|
||||
target_node: Option<String>,
|
||||
) -> Result<(), AudioError>;
|
||||
|
||||
/// Starts playing back raw PCM audio to the output device (speaker),
|
||||
/// reading mixed/incoming chunks of samples from the provided Receiver.
|
||||
@@ -65,16 +69,16 @@ pub mod pan;
|
||||
// Linear resamplers used by the Windows/cpal backend (W4). Platform-neutral and
|
||||
// pure, so it builds (and its tests run) everywhere even though only the cpal
|
||||
// backend wires it in.
|
||||
pub mod resample;
|
||||
#[cfg(windows)]
|
||||
pub mod cpal_impl;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod echo_cancel;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod pipewire_impl;
|
||||
#[cfg(windows)]
|
||||
pub mod cpal_impl;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod pw_cli;
|
||||
pub mod recorder;
|
||||
pub mod resample;
|
||||
|
||||
/// A selectable audio device for the input/output pickers. `name` is the stable
|
||||
/// identifier the backend uses to request the device (`target_node`);
|
||||
@@ -96,10 +100,10 @@ impl std::fmt::Display for AudioDevice {
|
||||
// Enumerate audio input/output devices for the pickers (sorted by description),
|
||||
// returning the same `AudioDevice` shape regardless of platform: PipeWire
|
||||
// (`pw-cli`) on Linux, cpal/WASAPI on Windows.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use pw_cli::enumerate_audio_devices;
|
||||
#[cfg(windows)]
|
||||
pub use cpal_impl::enumerate_audio_devices;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use pw_cli::enumerate_audio_devices;
|
||||
|
||||
/// The audio backend implementation for the current platform.
|
||||
///
|
||||
|
||||
+33
-7
@@ -108,7 +108,13 @@ pub fn track_filename(name: &str, id: &EndpointId) -> String {
|
||||
let clean = crate::sanitize::sanitize_name(name);
|
||||
let mut slug: String = clean
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '-' })
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
c.to_ascii_lowercase()
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Collapse runs of '-' and trim them off the ends.
|
||||
while slug.contains("--") {
|
||||
@@ -284,7 +290,10 @@ mod tests {
|
||||
let short: String = id.to_string().chars().take(8).collect();
|
||||
assert_eq!(track_filename("Alice", &id), format!("alice-{short}.wav"));
|
||||
// Spaces / punctuation collapse to single dashes, trimmed.
|
||||
assert_eq!(track_filename(" Bob the Builder! ", &id), format!("bob-the-builder-{short}.wav"));
|
||||
assert_eq!(
|
||||
track_filename(" Bob the Builder! ", &id),
|
||||
format!("bob-the-builder-{short}.wav")
|
||||
);
|
||||
// A name that sanitizes/slugs to nothing falls back to "peer".
|
||||
assert_eq!(track_filename("!!!", &id), format!("peer-{short}.wav"));
|
||||
}
|
||||
@@ -327,10 +336,18 @@ mod tests {
|
||||
rec.finalize().unwrap();
|
||||
|
||||
let expected = 3 * frame;
|
||||
assert_eq!(wav_samples(&dir.join("me.wav")), expected, "mic padded to full length");
|
||||
assert_eq!(
|
||||
wav_samples(&dir.join("me.wav")),
|
||||
expected,
|
||||
"mic padded to full length"
|
||||
);
|
||||
assert_eq!(wav_samples(&dir.join("mix.wav")), expected);
|
||||
assert_eq!(wav_samples(&dir.join(track_filename("p1", &p1))), expected);
|
||||
assert_eq!(wav_samples(&dir.join(track_filename("p2", &p2))), expected, "silent peer still full length");
|
||||
assert_eq!(
|
||||
wav_samples(&dir.join(track_filename("p2", &p2))),
|
||||
expected,
|
||||
"silent peer still full length"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -357,8 +374,14 @@ mod tests {
|
||||
rec.finalize().unwrap();
|
||||
|
||||
// Both tracks are the full 5 cycles long (late one was back-padded).
|
||||
assert_eq!(wav_samples(&dir.join(track_filename("early", &early))), 5 * frame);
|
||||
assert_eq!(wav_samples(&dir.join(track_filename("late", &late))), 5 * frame);
|
||||
assert_eq!(
|
||||
wav_samples(&dir.join(track_filename("early", &early))),
|
||||
5 * frame
|
||||
);
|
||||
assert_eq!(
|
||||
wav_samples(&dir.join(track_filename("late", &late))),
|
||||
5 * frame
|
||||
);
|
||||
|
||||
// The late track's first 2 cycles are silence, then the real audio.
|
||||
let bytes = std::fs::read(dir.join(track_filename("late", &late))).unwrap();
|
||||
@@ -378,6 +401,9 @@ mod tests {
|
||||
rec.end_cycle().unwrap();
|
||||
rec.finalize().unwrap();
|
||||
assert!(dir.join("me.wav").exists());
|
||||
assert!(!dir.join("mix.wav").exists(), "no mix track in stems-only mode");
|
||||
assert!(
|
||||
!dir.join("mix.wav").exists(),
|
||||
"no mix track in stems-only mode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-5
@@ -23,7 +23,10 @@ pub fn pan_gains(pan: f32) -> (f32, f32) {
|
||||
/// still following the same equal-power curve as a peer is moved away from center.
|
||||
pub fn playback_pan_gains(pan: f32) -> (f32, f32) {
|
||||
let (left, right) = pan_gains(pan);
|
||||
(left * std::f32::consts::SQRT_2, right * std::f32::consts::SQRT_2)
|
||||
(
|
||||
left * std::f32::consts::SQRT_2,
|
||||
right * std::f32::consts::SQRT_2,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -36,8 +39,14 @@ mod tests {
|
||||
fn hard_left_and_right_are_endpoints() {
|
||||
assert_eq!(pan_gains(-1.0), (1.0, 0.0));
|
||||
let (l, r) = pan_gains(1.0);
|
||||
assert!(l.abs() < EPS, "left at hard-right should be zero-ish, got {l}");
|
||||
assert!((r - 1.0).abs() < EPS, "right at hard-right should be one, got {r}");
|
||||
assert!(
|
||||
l.abs() < EPS,
|
||||
"left at hard-right should be zero-ish, got {l}"
|
||||
);
|
||||
assert!(
|
||||
(r - 1.0).abs() < EPS,
|
||||
"right at hard-right should be one, got {r}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -55,8 +64,14 @@ mod tests {
|
||||
let mut prev_r = f32::NEG_INFINITY;
|
||||
for pan in pans {
|
||||
let (l, r) = pan_gains(pan);
|
||||
assert!(l <= prev_l + EPS, "left gain must not rise as pan moves right");
|
||||
assert!(r >= prev_r - EPS, "right gain must not fall as pan moves right");
|
||||
assert!(
|
||||
l <= prev_l + EPS,
|
||||
"left gain must not rise as pan moves right"
|
||||
);
|
||||
assert!(
|
||||
r >= prev_r - EPS,
|
||||
"right gain must not fall as pan moves right"
|
||||
);
|
||||
prev_l = l;
|
||||
prev_r = r;
|
||||
}
|
||||
|
||||
+66
-44
@@ -1,13 +1,16 @@
|
||||
use crate::audio::{AudioBackend, AudioError};
|
||||
use std::sync::mpsc::{Sender, Receiver, RecvTimeoutError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
use pipewire as pw;
|
||||
use pw::{properties::properties, spa};
|
||||
use ringbuf::{
|
||||
HeapRb,
|
||||
traits::{Consumer, Producer, Split},
|
||||
};
|
||||
use spa::pod::Pod;
|
||||
use ringbuf::{HeapRb, traits::{Consumer, Producer, Split}};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct PipeWireBackend {
|
||||
capture_state: Mutex<Option<CaptureState>>,
|
||||
@@ -41,7 +44,11 @@ impl PipeWireBackend {
|
||||
}
|
||||
|
||||
impl AudioBackend for PipeWireBackend {
|
||||
fn start_capture(&self, tx: Sender<Vec<i16>>, target_node: Option<String>) -> Result<(), AudioError> {
|
||||
fn start_capture(
|
||||
&self,
|
||||
tx: Sender<Vec<i16>>,
|
||||
target_node: Option<String>,
|
||||
) -> Result<(), AudioError> {
|
||||
let mut capture_guard = self.capture_state.lock().unwrap();
|
||||
if capture_guard.is_some() {
|
||||
return Err(AudioError::Stream("Capture already started".to_string()));
|
||||
@@ -108,12 +115,17 @@ impl AudioBackend for PipeWireBackend {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_capture(cmd_rx: pw::channel::Receiver<()>, tx: Sender<Vec<i16>>, target_node: Option<String>) -> Result<(), AudioError> {
|
||||
let mainloop = pw::main_loop::MainLoopRc::new(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
fn run_capture(
|
||||
cmd_rx: pw::channel::Receiver<()>,
|
||||
tx: Sender<Vec<i16>>,
|
||||
target_node: Option<String>,
|
||||
) -> Result<(), AudioError> {
|
||||
let mainloop =
|
||||
pw::main_loop::MainLoopRc::new(None).map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let context = pw::context::ContextRc::new(&mainloop, None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let core = context.connect_rc(None)
|
||||
let core = context
|
||||
.connect_rc(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
// Ring buffer setup: 9600 samples (200ms capacity for mono 48kHz)
|
||||
@@ -181,15 +193,16 @@ fn run_capture(cmd_rx: pw::channel::Receiver<()>, tx: Sender<Vec<i16>>, target_n
|
||||
|
||||
let mut params = [Pod::from_bytes(&values).unwrap()];
|
||||
|
||||
stream.connect(
|
||||
spa::utils::Direction::Input,
|
||||
None,
|
||||
pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||
| pw::stream::StreamFlags::RT_PROCESS,
|
||||
&mut params,
|
||||
)
|
||||
.map_err(|e| AudioError::Stream(e.to_string()))?;
|
||||
stream
|
||||
.connect(
|
||||
spa::utils::Direction::Input,
|
||||
None,
|
||||
pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||
| pw::stream::StreamFlags::RT_PROCESS,
|
||||
&mut params,
|
||||
)
|
||||
.map_err(|e| AudioError::Stream(e.to_string()))?;
|
||||
|
||||
// Spawn the worker thread to pop from consumer and send Vec<i16> frames
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
@@ -257,11 +270,7 @@ const WORKER_POLL: Duration = Duration::from_millis(100);
|
||||
/// every `WORKER_POLL` even when no frames arrive — this is what lets `stop()`
|
||||
/// join the worker promptly instead of hanging on a parked blocking `recv()`
|
||||
/// (bug A7). Pure w.r.t. its inputs (no PipeWire), so it's unit-testable.
|
||||
fn drain_loop(
|
||||
rx: &Receiver<Vec<i16>>,
|
||||
running: &AtomicBool,
|
||||
mut on_frame: impl FnMut(Vec<i16>),
|
||||
) {
|
||||
fn drain_loop(rx: &Receiver<Vec<i16>>, running: &AtomicBool, mut on_frame: impl FnMut(Vec<i16>)) {
|
||||
while running.load(Ordering::Relaxed) {
|
||||
match rx.recv_timeout(WORKER_POLL) {
|
||||
Ok(frame) => on_frame(frame),
|
||||
@@ -293,7 +302,11 @@ fn publish_frame<P: Producer<Item = i16>>(
|
||||
fn frames_to_produce(requested: usize, mapped_frames: usize) -> usize {
|
||||
/// Safe per-cycle fallback when the graph doesn't report a quantum.
|
||||
const FALLBACK_FRAMES: usize = 1024;
|
||||
let want = if requested > 0 { requested } else { FALLBACK_FRAMES };
|
||||
let want = if requested > 0 {
|
||||
requested
|
||||
} else {
|
||||
FALLBACK_FRAMES
|
||||
};
|
||||
want.min(mapped_frames)
|
||||
}
|
||||
|
||||
@@ -303,11 +316,12 @@ fn run_playback(
|
||||
target_node: Option<String>,
|
||||
fill_gauge: Arc<AtomicUsize>,
|
||||
) -> Result<(), AudioError> {
|
||||
let mainloop = pw::main_loop::MainLoopRc::new(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let mainloop =
|
||||
pw::main_loop::MainLoopRc::new(None).map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let context = pw::context::ContextRc::new(&mainloop, None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
let core = context.connect_rc(None)
|
||||
let core = context
|
||||
.connect_rc(None)
|
||||
.map_err(|e| AudioError::Init(e.to_string()))?;
|
||||
|
||||
// Ring buffer setup: 19200 interleaved samples (200ms capacity for stereo
|
||||
@@ -428,7 +442,9 @@ fn run_playback(
|
||||
}
|
||||
if starved > 0 {
|
||||
// One wait-free atomic add per quantum — RT-safe.
|
||||
user_data.underrun_samples.fetch_add(starved, Ordering::Relaxed);
|
||||
user_data
|
||||
.underrun_samples
|
||||
.fetch_add(starved, Ordering::Relaxed);
|
||||
}
|
||||
// Decrement the exact occupancy counter by the samples we
|
||||
// actually pulled (excluding underruns, which removed
|
||||
@@ -493,7 +509,11 @@ fn run_playback(
|
||||
pw::spa::pod::Value::Choice(pw::spa::pod::ChoiceValue::Int(
|
||||
pw::spa::utils::Choice(
|
||||
pw::spa::utils::ChoiceFlags::empty(),
|
||||
pw::spa::utils::ChoiceEnum::Range { default: 8, min: 2, max: 64 },
|
||||
pw::spa::utils::ChoiceEnum::Range {
|
||||
default: 8,
|
||||
min: 2,
|
||||
max: 64,
|
||||
},
|
||||
),
|
||||
)),
|
||||
),
|
||||
@@ -524,15 +544,16 @@ fn run_playback(
|
||||
Pod::from_bytes(&buffers_values).unwrap(),
|
||||
];
|
||||
|
||||
stream.connect(
|
||||
spa::utils::Direction::Output,
|
||||
None,
|
||||
pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||
| pw::stream::StreamFlags::RT_PROCESS,
|
||||
&mut params,
|
||||
)
|
||||
.map_err(|e| AudioError::Stream(e.to_string()))?;
|
||||
stream
|
||||
.connect(
|
||||
spa::utils::Direction::Output,
|
||||
None,
|
||||
pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS
|
||||
| pw::stream::StreamFlags::RT_PROCESS,
|
||||
&mut params,
|
||||
)
|
||||
.map_err(|e| AudioError::Stream(e.to_string()))?;
|
||||
|
||||
// Spawn a worker thread to read from rx and push to producer
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
@@ -607,7 +628,10 @@ fn run_playback(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{drain_loop, for_each_capture_sample, frames_to_produce, publish_frame};
|
||||
use ringbuf::{HeapRb, traits::{Consumer, Producer, Split}};
|
||||
use ringbuf::{
|
||||
HeapRb,
|
||||
traits::{Consumer, Producer, Split},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
@@ -647,9 +671,7 @@ mod tests {
|
||||
#[test]
|
||||
fn capture_size_larger_than_mapping_is_clamped() {
|
||||
let mut samples = Vec::new();
|
||||
for_each_capture_sample(&[1, 0, 2, 0, 3], usize::MAX, |sample| {
|
||||
samples.push(sample)
|
||||
});
|
||||
for_each_capture_sample(&[1, 0, 2, 0, 3], usize::MAX, |sample| samples.push(sample));
|
||||
assert_eq!(samples, vec![1, 2]);
|
||||
}
|
||||
|
||||
|
||||
+31
-6
@@ -16,11 +16,20 @@ pub fn enumerate_audio_devices() -> Vec<AudioDevice> {
|
||||
/// Emits the in-progress node as an `AudioDevice` if it's a complete Audio/*
|
||||
/// node, then resets the accumulators for the next block. Non-audio or
|
||||
/// incomplete blocks are dropped (but still reset).
|
||||
fn push_device(name: &mut String, desc: &mut String, class: &mut String, out: &mut Vec<AudioDevice>) {
|
||||
fn push_device(
|
||||
name: &mut String,
|
||||
desc: &mut String,
|
||||
class: &mut String,
|
||||
out: &mut Vec<AudioDevice>,
|
||||
) {
|
||||
if !name.is_empty() && class.starts_with("Audio/") {
|
||||
out.push(AudioDevice {
|
||||
name: name.clone(),
|
||||
description: if desc.is_empty() { name.clone() } else { desc.clone() },
|
||||
description: if desc.is_empty() {
|
||||
name.clone()
|
||||
} else {
|
||||
desc.clone()
|
||||
},
|
||||
is_input: class == "Audio/Source",
|
||||
});
|
||||
}
|
||||
@@ -44,7 +53,12 @@ fn parse_pw_nodes(text: &str) -> Vec<AudioDevice> {
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("id ") {
|
||||
push_device(&mut current_name, &mut current_desc, &mut current_class, &mut devices);
|
||||
push_device(
|
||||
&mut current_name,
|
||||
&mut current_desc,
|
||||
&mut current_class,
|
||||
&mut devices,
|
||||
);
|
||||
} else if let Some(val) = line.strip_prefix("node.name = \"") {
|
||||
current_name = val.trim_end_matches('"').to_string();
|
||||
} else if let Some(val) = line.strip_prefix("node.description = \"") {
|
||||
@@ -53,7 +67,12 @@ fn parse_pw_nodes(text: &str) -> Vec<AudioDevice> {
|
||||
current_class = val.trim_end_matches('"').to_string();
|
||||
}
|
||||
}
|
||||
push_device(&mut current_name, &mut current_desc, &mut current_class, &mut devices);
|
||||
push_device(
|
||||
&mut current_name,
|
||||
&mut current_desc,
|
||||
&mut current_class,
|
||||
&mut devices,
|
||||
);
|
||||
|
||||
devices.sort_by(|a, b| a.description.cmp(&b.description));
|
||||
devices
|
||||
@@ -108,8 +127,14 @@ mod tests {
|
||||
fn source_is_input_sink_is_output() {
|
||||
let devices = parse_pw_nodes(SAMPLE_NODES);
|
||||
// Find devices by name or description to verify is_input
|
||||
let mic = devices.iter().find(|d| d.name == "alsa_input.builtin").unwrap();
|
||||
let speakers = devices.iter().find(|d| d.name == "alsa_output.builtin").unwrap();
|
||||
let mic = devices
|
||||
.iter()
|
||||
.find(|d| d.name == "alsa_input.builtin")
|
||||
.unwrap();
|
||||
let speakers = devices
|
||||
.iter()
|
||||
.find(|d| d.name == "alsa_output.builtin")
|
||||
.unwrap();
|
||||
let bare = devices.iter().find(|d| d.name == "bare.sink").unwrap();
|
||||
|
||||
assert!(mic.is_input);
|
||||
|
||||
@@ -145,7 +145,10 @@ impl StereoPullResampler {
|
||||
self.frac -= 1.0;
|
||||
}
|
||||
let f = self.frac as f32;
|
||||
let out = (lerp(self.prev.0, self.cur.0, f), lerp(self.prev.1, self.cur.1, f));
|
||||
let out = (
|
||||
lerp(self.prev.0, self.cur.0, f),
|
||||
lerp(self.prev.1, self.cur.1, f),
|
||||
);
|
||||
self.frac += self.step;
|
||||
Some(out)
|
||||
}
|
||||
@@ -273,7 +276,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
// At step 2.0 we consume ~2 input frames per output frame.
|
||||
assert!(idx > emitted, "consumed {idx} input, emitted {emitted} output");
|
||||
assert!(
|
||||
idx > emitted,
|
||||
"consumed {idx} input, emitted {emitted} output"
|
||||
);
|
||||
}
|
||||
|
||||
/// A zero rate must not produce a zero `step` (which would spin `push`'s inner
|
||||
|
||||
Reference in New Issue
Block a user