Skip to main content

tuner/
lib.rs

1// Copyright (c) 2025 Worik Turei Stanton
2// License: GPL-3.0
3
4use clap::Parser;
5use qzn3t_pitch_detection::note_detection_result::NoteDetectionResult;
6use qzn3t_pitch_detection::note_detection_result::NoteName;
7use qzn3t_pitch_detection::runner::{Detector, DetectorCfg, pitch_detection_run};
8use std::io::Write;
9use std::sync::{Arc, atomic::AtomicBool, atomic::Ordering, mpsc};
10use std::thread::spawn;
11use std::{
12    io::{self},
13    thread::JoinHandle,
14};
15
16#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
17pub enum TunerNote {
18    A,
19    ASharp,
20    B,
21    C,
22    CSharp,
23    D,
24    DSharp,
25    E,
26    F,
27    FSharp,
28    G,
29    GSharp,
30}
31
32impl From<NoteName> for TunerNote {
33    fn from(n: NoteName) -> Self {
34        match n {
35            NoteName::A => TunerNote::A,
36            NoteName::ASharp => TunerNote::ASharp,
37            NoteName::B => TunerNote::B,
38            NoteName::C => TunerNote::C,
39            NoteName::CSharp => TunerNote::CSharp,
40            NoteName::D => TunerNote::D,
41            NoteName::DSharp => TunerNote::DSharp,
42            NoteName::E => TunerNote::E,
43            NoteName::F => TunerNote::F,
44            NoteName::FSharp => TunerNote::FSharp,
45            NoteName::G => TunerNote::G,
46            NoteName::GSharp => TunerNote::GSharp,
47        }
48    }
49}
50/// Data to return from tuner::get_results
51#[derive(Debug)]
52pub struct TunerData {
53    pub note: TunerNote,
54    pub octave: i32,
55    pub cents_offset: f32,
56}
57
58// Custom ProcessHandler for capturing audio using ringbuf
59#[derive(Parser, Debug)]
60#[command(version, about, long_about = None)]
61pub struct TunerArgs {
62    #[arg(short = 'p', long)]
63    pub connect_port: Option<String>, // If specified connct this port to the tuner
64}
65
66pub fn get_results(
67    args: &TunerArgs,
68    sender: mpsc::Sender<TunerData>,
69    kill_switch: Arc<AtomicBool>,
70) -> JoinHandle<()> {
71    let port = match &args.connect_port {
72        Some(p) => p.clone(),
73        None => "system:capture_1".to_string(),
74    };
75    // Channel for note data from pitech detector
76    let (tx, rx) = mpsc::channel::<NoteDetectionResult>();
77    //  Channel for audio data from Jack to pitch detector
78    let (tx_f32, rx_f32) = mpsc::channel::<f32>();
79
80    // Set up the pitch detection Jack client
81    let audio_dst_client = match qzn3t_pitch_detection::runner::start_jack(tx_f32, &port) {
82        Ok(ac) => ac,
83        Err(err) => panic!(
84            "Error pitch_detectiopn tester: Cannot create Jack clent to receive audio: {err}"
85        ),
86    };
87    let detector_cfg = DetectorCfg {
88        sample_rate: audio_dst_client
89            .as_client()
90            .sample_rate()
91            .try_into()
92            .unwrap(),
93        size: 16384,
94        padding: 1024,
95        power_threshold: 0.1,
96        clarity_threshold: 0.5,
97        detector: Detector::McLeod,
98    };
99
100    let pd_handle = pitch_detection_run(tx, rx_f32, &detector_cfg, Some(kill_switch.clone()));
101    spawn(move || {
102        // Move the client into the thread so it is not shut down
103        let _audio_dst_client = audio_dst_client;
104        let sender = sender.clone();
105        loop {
106            if kill_switch.load(Ordering::SeqCst) {
107                // Tuner disabled
108                _ = pd_handle.join();
109                break;
110            }
111
112            match rx.recv() {
113                Ok(ndr) => {
114                    let td = TunerData {
115                        octave: ndr.octave,
116                        note: ndr.note_name.into(),
117                        cents_offset: ndr.cents,
118                    };
119                    if let Err(err) = sender.send(td) {
120                        eprintln!(
121                            "Error qzn3t/tuner: Note detection loop failed sending results: {err}"
122                        );
123                        break;
124                    }
125                }
126                Err(err) => {
127                    eprintln!(
128                        "Error qzn3t/tuner: Note detection loop failed receiving results: {err}"
129                    );
130                    break;
131                }
132            }
133        }
134    })
135}
136
137pub fn inner_main(args: &TunerArgs) {
138    let kill_switch = Arc::new(AtomicBool::new(false));
139    let (sender, receiver) = mpsc::channel::<TunerData>();
140    _ = get_results(args, sender, kill_switch.clone());
141    loop {
142        let ndr = match receiver.recv() {
143            Ok(r) => r,
144            Err(err) => {
145                eprintln!("DBG tuner: get_results send error: {err}");
146                break;
147            }
148        };
149        let report = format!(
150            "Tuner> {:?}/{} {: >-6.2}\n",
151            ndr.note, ndr.octave, ndr.cents_offset
152        );
153        if let Err(err) = my_write(report) {
154            eprintln!("Error tuner: inner main. {err}");
155        }
156    }
157}
158
159fn my_write(report: String) -> io::Result<()> {
160    let mut v = io::stdout().lock();
161    v.write_all(report.as_bytes())?;
162    v.flush()?;
163    Ok(())
164}