audio_visualizer/dynamic/window_top_btm/
mod.rs

1/*
2MIT License
3
4Copyright (c) 2021 Philipp Schuster
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24//! This module provides the functionality to display a GUI window, where the upper
25//! half shows the real-time recorded audio data whereas the lower half shows a
26//! diagram of transformed data, such as a lowpass filter or a a frequency spectrum.
27//!
28//! It uses the [`minifb`] crate to display GUI windows.
29use crate::dynamic::live_input::{setup_audio_input_loop, AudioDevAndCfg};
30use crate::dynamic::window_top_btm::visualize_minifb::{
31    get_drawing_areas, setup_window, DEFAULT_H, DEFAULT_W,
32};
33use cpal::traits::StreamTrait;
34
35use minifb::Key;
36use plotters::chart::ChartContext;
37use plotters::coord::cartesian::Cartesian2d;
38use plotters::coord::types::RangedCoordf64;
39use plotters::prelude::BitMapBackend;
40use plotters::series::LineSeries;
41use plotters::style::{BLACK, CYAN};
42use plotters_bitmap::bitmap_pixel::BGRXPixel;
43use ringbuffer::{AllocRingBuffer, RingBuffer};
44use std::borrow::{Borrow, BorrowMut};
45use std::ops::Range;
46use std::sync::{Arc, Mutex};
47use std::time::Duration;
48
49/// Smooth refresh rate on 144 Hz displays.
50const REFRESH_RATE: f64 = 144.0;
51const REFRESH_S: f64 = 1.0 / REFRESH_RATE;
52
53pub mod pixel_buf;
54pub mod visualize_minifb;
55
56/// Parameter type for [`open_window_connect_audio`]. Describes how the audio data shall
57/// be transformed, and thus, how it should be displayed in the lower part of the window.
58///
59/// The function is called every x milliseconds (refresh rate of window).
60///
61/// This works cross-platform (Windows, MacOS, Linux).
62#[allow(missing_debug_implementations)]
63pub enum TransformFn<'a> {
64    /// Synchronized x-axis with the original data. Useful for transformations on the
65    /// waveform, such as a (lowpass) filter.
66    ///
67    /// Functions takes amplitude values and transforms them to a new amplitude value.
68    /// It gets the sampling rate as second argument.
69    Basic(fn(&[f32], f32) -> Vec<f32>),
70    /// Use this, when the x-axis is different than for the original data. For example,
71    /// if you want to display a spectrum.
72    ///
73    /// Functions takes amplitude values (and their index) and transforms them to a new
74    /// (x,y)-pair. Takes a closure instead of a function, so that it can capture state.
75    /// It gets the sampling rate as second argument.
76    #[allow(clippy::complexity)]
77    Complex(&'a dyn Fn(&[f32], f32) -> Vec<(f64, f64)>),
78}
79
80/// Starts the audio recording via `cpal` on the given audio device (or the default input device),
81/// opens a GUI window and displays two graphs. The upper graph is the latest audio input as
82/// wave form (live/real time). The lower graph can be customized, to show for example a
83/// spectrum or the lowpassed data.
84///
85/// This operation is blocking. It returns, when the GUI window is closed.
86///
87/// **This operation is expensive and will be very laggy in "Debug" builds!**
88///
89/// # Parameters
90/// - `name` Name of the GUI window
91/// - `preferred_height` Preferred height of GUI window. Default is [`DEFAULT_H`].
92/// - `preferred_width` Preferred height of GUI window. Default is [`DEFAULT_W`].
93/// - `preferred_x_range` Preferred range for the x-axis of the lower (=custom) diagram.
94///                       If no value is present, the same value as for the upper diagram is used.
95/// - `preferred_y_range` Preferred range for the y-axis of the lower (=custom) diagram.
96///                       If no value is present, the same value as for the upper diagram is used.
97/// - `x_desc` Description for the x-axis of the lower (=custom) diagram.
98/// - `y_desc` Description for the y-axis of the lower (=custom) diagram.
99/// - `preferred_input_dev` See [`AudioDevAndCfg`].
100/// - `audio_data_transform_fn` See [`open_window_connect_audio`].
101#[allow(clippy::too_many_arguments)]
102pub fn open_window_connect_audio(
103    name: &str,
104    preferred_height: Option<usize>,
105    preferred_width: Option<usize>,
106    preferred_x_range: Option<Range<f64>>,
107    preferred_y_range: Option<Range<f64>>,
108    x_desc: &str,
109    y_desc: &str,
110    input_dev_and_cfg: AudioDevAndCfg,
111    audio_data_transform_fn: TransformFn,
112) {
113    let sample_rate = input_dev_and_cfg.cfg().sample_rate.0 as f32;
114    let latest_audio_data = init_ringbuffer(sample_rate as usize);
115    let audio_buffer_len = latest_audio_data.lock().unwrap().len();
116    let stream = setup_audio_input_loop(latest_audio_data.clone(), input_dev_and_cfg);
117    // This will be 1/44100 or 1/48000; the two most common sampling rates.
118    let time_per_sample = 1.0 / sample_rate as f64;
119
120    // start recording; audio will be continuously stored in "latest_audio_data"
121    stream.play().unwrap();
122    let (mut window, top_cs, btm_cs, mut pixel_buf) = setup_window(
123        name,
124        preferred_height,
125        preferred_width,
126        preferred_x_range,
127        preferred_y_range,
128        x_desc,
129        y_desc,
130        audio_buffer_len,
131        time_per_sample,
132    );
133    window.limit_update_rate(Some(Duration::from_secs_f64(REFRESH_S)));
134
135    // GUI refresh loop; CPU-limited by "window.limit_update_rate"
136    while window.is_open() {
137        if window.is_key_down(Key::Escape) {
138            break;
139        }
140
141        let (top_drawing_area, btm_drawing_area) = get_drawing_areas(
142            pixel_buf.borrow_mut(),
143            preferred_width.unwrap_or(DEFAULT_W),
144            preferred_height.unwrap_or(DEFAULT_H),
145        );
146
147        let top_chart = top_cs.clone().restore(&top_drawing_area);
148        let btm_chart = btm_cs.clone().restore(&btm_drawing_area);
149
150        // remove drawings from previous iteration (but keep axis etc)
151        top_chart.plotting_area().fill(&BLACK).borrow();
152        btm_chart.plotting_area().fill(&BLACK).borrow();
153
154        // lock released immediately after oneliner
155        let latest_audio_data = latest_audio_data.clone().lock().unwrap().to_vec();
156        fill_chart_waveform_over_time(
157            top_chart,
158            &latest_audio_data,
159            time_per_sample,
160            audio_buffer_len,
161        );
162        if let TransformFn::Basic(fnc) = audio_data_transform_fn {
163            let data = fnc(&latest_audio_data, sample_rate);
164            fill_chart_waveform_over_time(btm_chart, &data, time_per_sample, audio_buffer_len);
165        } else if let TransformFn::Complex(fnc) = audio_data_transform_fn {
166            let data = fnc(&latest_audio_data, sample_rate);
167            fill_chart_complex_fnc(btm_chart, data);
168        } else {
169            // required for compilation
170            drop(btm_chart);
171            panic!("invalid transform fn variant");
172        }
173
174        // make sure that "pixel_buf" is not borrowed longer
175        drop(top_drawing_area);
176        drop(btm_drawing_area);
177
178        // REQUIRED to call on of the .update*()-methods, otherwise mouse and keyboard events
179        // are not updated
180        //
181        // Update() also does the rate limiting/set the thread to sleep if not enough time
182        //  sine the last refresh happened
183        window
184            .update_with_buffer(
185                pixel_buf.borrow(),
186                preferred_width.unwrap_or(DEFAULT_W),
187                preferred_height.unwrap_or(DEFAULT_H),
188            )
189            .unwrap();
190    }
191    stream.pause().unwrap();
192}
193
194/// Inits a ringbuffer on the heap and fills it with zeroes.
195fn init_ringbuffer(sampling_rate: usize) -> Arc<Mutex<AllocRingBuffer<f32>>> {
196    // Must be a power (ringbuffer requirement).
197    let mut buf = AllocRingBuffer::new((5 * sampling_rate).next_power_of_two());
198    buf.fill(0.0);
199    Arc::new(Mutex::new(buf))
200}
201
202/// Fills the given chart with the waveform over time, from the past (left) to now/realtime (right).
203fn fill_chart_complex_fnc(
204    mut chart: ChartContext<BitMapBackend<BGRXPixel>, Cartesian2d<RangedCoordf64, RangedCoordf64>>,
205    audio_data: Vec<(f64, f64)>,
206) {
207    // dedicated function; otherwise lifetime problems/compiler errors
208    chart
209        .draw_series(LineSeries::new(audio_data, &CYAN))
210        .unwrap();
211}
212
213/// Fills the given chart with the waveform over time, from the past (left) to now/realtime (right).
214fn fill_chart_waveform_over_time(
215    mut chart: ChartContext<BitMapBackend<BGRXPixel>, Cartesian2d<RangedCoordf64, RangedCoordf64>>,
216    audio_data: &[f32],
217    time_per_sample: f64,
218    audio_history_buf_len: usize,
219) {
220    debug_assert_eq!(audio_data.len(), audio_history_buf_len);
221    let timeshift = audio_history_buf_len as f64 * time_per_sample;
222
223    // calculate timestamp of each index (x coordinate)
224    let data_iter = audio_data
225        .iter()
226        .enumerate()
227        // Important to reduce the calculation complexity by reducing the number of elements,
228        // because drawing tens of thousands of points into the diagram is very expensive.
229        //
230        // If we skip too many elements, animation becomes un-smooth.... 4 seems to be sensible
231        // due to tests by me.
232        .filter(|(i, _)| *i % 4 == 0)
233        .map(|(i, amplitude)| {
234            let timestamp = time_per_sample * (i as f64) - timeshift;
235            // Values for amplitude in interval [-1.0; 1.0]
236            (timestamp, (*amplitude) as f64)
237        });
238
239    // Draws all points as a line of connected points.
240    // LineSeries is reasonable efficient for the big workload, but still very expensive..
241    // (4-6ms in release mode on my intel i5 10th generation)
242    chart
243        .draw_series(LineSeries::new(data_iter, &CYAN))
244        .unwrap();
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[ignore]
252    #[test]
253    fn test_record_live_audio_and_visualize() {
254        open_window_connect_audio(
255            "Test",
256            None,
257            None,
258            None,
259            None,
260            "x-axis",
261            "y-axis",
262            AudioDevAndCfg::new(None, None),
263            TransformFn::Basic(|vals, _| vals.to_vec()),
264        );
265    }
266}