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}