1mod decibels;
4pub mod experimental;
5mod frame;
6mod json;
7pub mod stable;
8
9use sva_samples::{EnvelopeFrame, StereoImage};
10
11pub use decibels::to_db;
12
13pub const ANALYSES: [&str; 4] = ["onsets", "trajectory", "masking", "gain-reduction"];
15
16#[derive(Debug, PartialEq)]
17pub struct AnalysisError(pub String);
18
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct TempoGrid {
21 pub seconds_per_bar: f64,
22 pub beats_per_bar: f64,
23}
24
25pub struct Request<'a> {
28 pub samples: &'a [f32],
29 pub sample_rate: f64,
30 pub start_secs: f64,
31 pub frame_secs: f64,
32 pub tempo: Option<TempoGrid>,
33 pub envelope: &'a [EnvelopeFrame],
34 pub stereo: Option<&'a StereoImage>,
35 pub against: Option<&'a [f32]>,
36 pub gated: bool,
37 pub input_envelope: Option<&'a [EnvelopeFrame]>,
38}
39
40pub fn run(name: &str, req: &Request) -> Result<String, AnalysisError> {
42 match name {
43 "onsets" => {
44 let o =
45 stable::onsets::detect(req.samples, req.sample_rate, req.start_secs, req.tempo)?;
46 Ok(json::onsets(&o))
47 }
48 "trajectory" => {
49 let t = stable::trajectory::analyze(
50 req.envelope,
51 req.stereo,
52 req.samples,
53 req.sample_rate,
54 req.frame_secs,
55 );
56 Ok(json::trajectory(&t))
57 }
58 "masking" => {
59 let against = req.against.ok_or_else(|| {
60 AnalysisError("`masking` needs a second, `--against` node".to_string())
61 })?;
62 let gate = req
63 .gated
64 .then(|| {
65 stable::onsets::detect(req.samples, req.sample_rate, req.start_secs, req.tempo)
66 })
67 .transpose()?;
68 experimental::masking::analyze(
69 req.samples,
70 against,
71 req.sample_rate,
72 req.start_secs,
73 gate.as_ref().map(|o| o.onsets.as_slice()),
74 )
75 .map(|m| json::masking(&m))
76 }
77 "gain-reduction" => {
78 let g = experimental::gain_reduction::analyze(req.envelope, req.input_envelope);
79 Ok(json::gain_reduction(&g))
80 }
81 other => Err(AnalysisError(format!("unknown representation `{other}`"))),
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn req(samples: &[f32]) -> Request<'_> {
90 Request {
91 samples,
92 sample_rate: 44100.0,
93 start_secs: 0.0,
94 frame_secs: 0.01,
95 tempo: None,
96 envelope: &[],
97 stereo: None,
98 against: None,
99 gated: false,
100 input_envelope: None,
101 }
102 }
103
104 #[test]
105 fn every_listed_analysis_is_reachable_by_name() {
106 assert!(run("onsets", &req(&[0.0; 100])).is_ok());
107 assert!(run("trajectory", &req(&[0.0; 100])).is_ok());
108 assert!(run("gain-reduction", &req(&[])).is_ok());
109 }
110
111 #[test]
112 fn masking_without_against_refuses_with_a_named_flag() {
113 let err = run("masking", &req(&[0.0; 100])).unwrap_err();
114 assert!(err.0.contains("--against"));
115 }
116
117 #[test]
118 fn an_unknown_name_refuses() {
119 assert!(run("vibes", &req(&[])).is_err());
120 }
121}