Skip to main content

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}