Skip to main content

sound_to_patch/cli/
analyze.rs

1//! The arguments of `analyze`.
2
3use std::path::PathBuf;
4
5use clap::Args;
6
7use crate::notes::Profile;
8use crate::synth::{Synth, SynthArg, SynthError};
9use crate::units::Bpm;
10
11/// Arguments of `analyze`.
12#[derive(Debug, Args)]
13pub struct AnalyzeArgs {
14    /// The audio stem: WAV, AIFF, FLAC, MP3, AAC or M4A.
15    pub stem: PathBuf,
16    /// The tempo of the stem in beats per minute, 4 to 1000. It is refined
17    /// from the stem, 2.91 % below to 3.09 % above it, unless --bpm-exact
18    /// is given
19    #[arg(long, value_name = "BPM", value_parser = parse_bpm, allow_negative_numbers = true)]
20    pub bpm: Bpm,
21    /// Use --bpm as it is, without refining it from the stem
22    #[arg(long)]
23    pub bpm_exact: bool,
24    /// A MIDI file with the stem's notes, instead of transcribing the stem;
25    /// the f0 track still reads the stem unless the notes play chords. Its
26    /// tempo must match --bpm or the refined tempo
27    #[arg(long, value_name = "FILE")]
28    pub notes: Option<PathBuf>,
29    /// Decode with the settings of this kind of stem instead of the
30    /// detected ones
31    #[arg(long, value_enum, value_name = "PROFILE", conflicts_with = "notes")]
32    pub profile: Option<Profile>,
33    /// The output folder [default: a folder named after the stem, next to
34    /// it]
35    #[arg(short, long, value_name = "DIR")]
36    pub out: Option<PathBuf>,
37    /// Replace files in the output folder that this stem's report does not
38    /// own: a report or notes of another stem, a report of the same name
39    /// with other content, a file this tool did not write, or a
40    /// spectrogram.png without a report
41    #[arg(long)]
42    pub force: bool,
43    /// The target synth that the report names [default: vital]
44    #[arg(long, value_enum, value_name = "SYNTH")]
45    pub synth: Option<Synth>,
46}
47
48impl AnalyzeArgs {
49    /// Resolves the target synth: the flag, else Vital, and only a synth
50    /// with an adapter.
51    ///
52    /// # Errors
53    ///
54    /// [`SynthError::NotBuilt`] for a synth without an adapter.
55    pub fn synth(&self) -> Result<Synth, SynthError> {
56        SynthArg { synth: self.synth }.resolve(None)
57    }
58}
59
60/// Parses `--bpm`: a number from 4 to 1000.
61pub(crate) fn parse_bpm(text: &str) -> Result<Bpm, String> {
62    let value: f64 = text
63        .trim()
64        .parse()
65        .map_err(|_| format!("`{text}` is not a number"))?;
66    Bpm::new(value).map_err(|error| error.to_string())
67}