forge_capture/lib.rs
1//! Microphone capture: the input device, the input plan, and the mono
2//! model-rate audio a host reads from it.
3//!
4//! A leaf crate. It depends on no other forge crate and knows nothing about
5//! a host, a take or a wire: [`open`] hands a caller the device's audio and
6//! the decision about what a frame is stays with the caller. A client's own
7//! shell reads frames straight off it; `forge-dictate` re-exports the device
8//! list.
9//!
10//! The stream is `!Send`, so it lives on a thread of its own from the moment
11//! the device opens; [`Capture`] is the handle that stops it.
12
13mod convert;
14mod device;
15mod error;
16mod stream;
17
18pub use convert::resample_mono;
19pub use device::{Device, devices};
20pub use error::Error;
21pub use stream::{Block, Capture, open};
22
23/// Sample rate every model in forge reads, and the rate [`open`] delivers.
24pub const SAMPLE_RATE: u32 = 16_000;
25
26/// A panic payload's own message, or a phrase where it carried none.
27pub(crate) fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
28 panic
29 .downcast_ref::<&str>()
30 .map(|said| (*said).to_owned())
31 .or_else(|| panic.downcast_ref::<String>().cloned())
32 .unwrap_or_else(|| "no message".to_owned())
33}
34
35#[cfg(test)]
36mod tests_leaf_invariant {
37 /// This crate is a leaf, and depending on a forge-* crate is the one
38 /// change that would quietly end that. Nothing catches it at compile
39 /// time, because such an edge closes no cycle and the workspace
40 /// builds fine.
41 ///
42 /// Asked of cargo's resolver rather than read out of the manifest,
43 /// because a rename is exactly what a manifest hides and what the
44 /// resolver has already undone. `--manifest-path` is a compile-time
45 /// constant, so the answer does not depend on where this runs from.
46 ///
47 /// Checks direct dependencies only: a forge crate reached through a
48 /// workspace crate not itself named `forge-*` is not caught.
49 #[test]
50 fn depends_on_no_forge_crate() {
51 let output = std::process::Command::new(option_env!("CARGO").unwrap_or("cargo"))
52 .args([
53 "metadata",
54 "--no-deps",
55 "--offline",
56 "--format-version",
57 "1",
58 "--manifest-path",
59 concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"),
60 ])
61 .output()
62 .expect("cargo metadata must run: a guard that cannot run is not a guard");
63 assert!(
64 output.status.success(),
65 "cargo metadata failed, so the invariant is unchecked: {}",
66 String::from_utf8_lossy(&output.stderr)
67 );
68
69 let metadata: serde_json::Value =
70 serde_json::from_slice(&output.stdout).expect("cargo metadata emits json");
71 let package = metadata["packages"]
72 .as_array()
73 .into_iter()
74 .flatten()
75 .find(|p| p["name"] == "forge-capture")
76 .expect("forge-capture must appear in its own metadata");
77
78 let forge: Vec<&str> = package["dependencies"]
79 .as_array()
80 .into_iter()
81 .flatten()
82 .filter_map(|dep| dep["name"].as_str())
83 .filter(|name| name.starts_with("forge-"))
84 .collect();
85 assert!(forge.is_empty(), "forge-capture must depend on no forge-* crate, found {forge:?}");
86 }
87}