audio_processor_testing_helpers/
charts.rs

1// Augmented Audio: Audio libraries and applications
2// Copyright (c) 2022 Pedro Tacla Yamada
3//
4// The MIT License (MIT)
5//
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to deal
8// in the Software without restriction, including without limitation the rights
9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10// copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12//
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15//
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22// THE SOFTWARE.
23
24//! Draw charts with `plotters` during unit-testing.
25
26use std::time::Duration;
27
28use crate::generators::sine_buffer;
29use crate::util::rms_level;
30use audio_processor_traits::{AudioBuffer, AudioContext, AudioProcessor, AudioProcessorSettings};
31pub use plotters::prelude::*;
32
33struct FrequencyResponseResult {
34    frequency: f32,
35    relative_output_level: f32,
36}
37
38fn test_frequency_response<Processor>(
39    sample_rate: f32,
40    frequency: f32,
41    audio_processor: &mut Processor,
42) -> FrequencyResponseResult
43where
44    Processor: AudioProcessor<SampleType = f32>,
45{
46    let input_buffer = sine_buffer(sample_rate, frequency, Duration::from_millis(50));
47    let mut input_buffer = AudioBuffer::from_interleaved(1, &input_buffer);
48
49    let input_rms = rms_level(input_buffer.channel(0));
50    let mut context = AudioContext::from(AudioProcessorSettings::new(sample_rate, 1, 1, 512));
51    audio_processor.process(&mut context, &mut input_buffer);
52    let output_rms = rms_level(input_buffer.channel(0));
53
54    let relative_output_level = output_rms / input_rms;
55
56    FrequencyResponseResult {
57        frequency,
58        relative_output_level,
59    }
60}
61
62fn get_test_frequencies() -> Vec<f32> {
63    let mut frequencies = vec![];
64    let mut start_frequency = 20.0;
65    for _ in 0..200 {
66        frequencies.push(start_frequency);
67        start_frequency += 20.0;
68    }
69    frequencies
70}
71
72struct FrequencyResponseChartModel {
73    x_range: (f64, f64),
74    y_range: (f64, f64),
75    values: Vec<(f64, f64)>,
76}
77
78fn build_frequency_response_chart_model(
79    responses: Vec<FrequencyResponseResult>,
80) -> FrequencyResponseChartModel {
81    let min_x = 0.0;
82    let max_x = responses
83        .iter()
84        .map(|r| r.frequency as f64)
85        .fold(-1. / 0., f64::max);
86    let min_y = 0.0;
87    let max_y = responses
88        .iter()
89        .map(|r| r.relative_output_level as f64)
90        .fold(-1. / 0., f64::max);
91
92    let values = responses
93        .iter()
94        .map(|r| (r.frequency as f64, r.relative_output_level as f64))
95        .collect();
96
97    FrequencyResponseChartModel {
98        x_range: (min_x, max_x),
99        y_range: (min_y, max_y),
100        values,
101    }
102}
103
104/// Generates a frequency response plot for a given audio processor
105pub fn generate_frequency_response_plot<Processor>(
106    filename: &str,
107    plot_name: &str,
108    audio_processor: &mut Processor,
109) where
110    Processor: AudioProcessor<SampleType = f32>,
111{
112    let mut settings = AudioProcessorSettings::default();
113    settings.sample_rate = 22050.0;
114    settings.input_channels = 1;
115    settings.output_channels = 1;
116    let mut context = AudioContext::from(settings);
117    audio_processor.prepare(&mut context);
118    let sample_rate = settings.sample_rate;
119
120    let frequencies = get_test_frequencies();
121    let responses = frequencies
122        .iter()
123        .map(|frequency| test_frequency_response(sample_rate, *frequency, audio_processor))
124        .collect();
125    let chart_model = build_frequency_response_chart_model(responses);
126
127    let filename = std::path::Path::new(filename);
128    let chart_filename = filename.with_file_name(format!(
129        "{}--{}.svg",
130        filename.file_name().unwrap().to_str().unwrap(),
131        plot_name
132    ));
133
134    let svg_backend = SVGBackend::new(&chart_filename, (1000, 1000));
135    let drawing_area = svg_backend.into_drawing_area();
136    drawing_area.fill(&WHITE).unwrap();
137
138    let mut chart = ChartBuilder::on(&drawing_area)
139        .caption(plot_name, ("sans-serif", 20))
140        .set_label_area_size(LabelAreaPosition::Left, 40)
141        .set_label_area_size(LabelAreaPosition::Bottom, 40)
142        .build_cartesian_2d(
143            chart_model.x_range.0..chart_model.x_range.1,
144            chart_model.y_range.0..chart_model.y_range.1,
145        )
146        .unwrap();
147
148    chart.configure_mesh().draw().unwrap();
149
150    chart
151        .draw_series(LineSeries::new(chart_model.values, &RED))
152        .unwrap();
153    println!(">>> Wrote {} chart to {:?}", plot_name, chart_filename);
154}
155
156type SeriesDef = (RGBColor, Vec<f32>);
157type MultiSeries = Vec<SeriesDef>;
158
159/// Draw a `Vec<f32>` as a line chart under `filename`.
160pub fn draw_vec_chart(filename: &str, plot_name: &str, vec: Vec<f32>) {
161    draw_multi_vec_charts(filename, plot_name, vec![(RED, vec)])
162}
163
164/// Draw multiple vectors as line charts.
165pub fn draw_multi_vec_charts(filename: &str, plot_name: &str, vecs: MultiSeries) {
166    let (_, vec) = &vecs[0];
167    let filename = std::path::Path::new(filename);
168    let chart_filename = filename.with_file_name(format!(
169        "{}--{}.png",
170        filename.file_name().unwrap().to_str().unwrap(),
171        plot_name
172    ));
173
174    let backend = BitMapBackend::new(&chart_filename, (1000, 200));
175    let drawing_area = backend.into_drawing_area();
176    drawing_area.fill(&WHITE).unwrap();
177
178    let x_range = (0, vec.len());
179    let y_range = (
180        vec.iter().cloned().fold(-1. / 0., f32::max) as f64,
181        vec.iter().cloned().fold(1. / 0., f32::min) as f64,
182    );
183
184    let mut chart = ChartBuilder::on(&drawing_area)
185        .caption(plot_name, ("sans-serif", 20))
186        .set_label_area_size(LabelAreaPosition::Left, 40)
187        .set_label_area_size(LabelAreaPosition::Bottom, 40)
188        .build_cartesian_2d(x_range.0..x_range.1, y_range.1..y_range.0)
189        .unwrap();
190
191    chart.configure_mesh().draw().unwrap();
192
193    for (color, vec) in vecs {
194        let values: Vec<(usize, f64)> = vec
195            .iter()
196            .enumerate()
197            .map(|(i, s)| (i, *s as f64))
198            .collect();
199        chart.draw_series(LineSeries::new(values, color)).unwrap();
200    }
201}
202
203#[cfg(test)]
204mod test {
205    #[test]
206    fn it_compiles() {}
207}