sonogram/
lib.rs

1/*
2 * Copyright (C) Simon Werner, 2022
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, see <http://www.gnu.org/licenses/>.
16 */
17
18extern crate csv;
19#[cfg(feature = "png")]
20extern crate png;
21
22mod builder;
23mod colour_gradient;
24mod errors;
25mod freq_scales;
26mod spec_core;
27mod window_fn;
28
29pub use builder::SpecOptionsBuilder;
30pub use colour_gradient::{ColourGradient, ColourTheme, RGBAColour};
31pub use errors::SonogramError;
32pub use freq_scales::{FreqScaler, FrequencyScale};
33use num_traits::NumCast;
34pub use spec_core::SpecCompute;
35pub use window_fn::*;
36
37#[cfg(feature = "png")]
38use std::fs::File;
39#[cfg(feature = "png")]
40use std::io::BufWriter;
41use std::path::Path;
42
43use resize::Pixel::GrayF32;
44use resize::Type::Lanczos3;
45use rgb::FromSlice;
46
47#[cfg(feature = "png")]
48use png::HasParameters; // To use encoder.set()
49
50#[cfg(feature = "rayon")]
51use rayon::prelude::*;
52
53pub struct Spectrogram {
54    spec: Vec<f32>,
55    width: usize,
56    height: usize,
57}
58
59impl Spectrogram {
60    ///
61    /// Create a new Spectrogram from a raw buffer.  It must of the correct size
62    /// (width * height).  But can be of any numeric type that can be converted
63    /// to f32.
64    ///
65    /// # Arguments
66    ///
67    ///  * `buf` - The raw buffer of data to create the spectrogram from, real numbers only.
68    ///  * `width` - The width of the spectrogram.
69    ///  * `height` - The height of the spectrogram.
70    ///
71    pub fn from_raw<T: NumCast + Copy>(
72        buf: &[T],
73        width: usize,
74        height: usize,
75    ) -> Result<Spectrogram, SonogramError> {
76        if buf.len() != width * height {
77            return Err(SonogramError::InvalidRawDataSize);
78        }
79
80        let spec = Spectrogram {
81            spec: buf.iter().map(|&x| NumCast::from(x).unwrap()).collect(),
82            width,
83            height,
84        };
85
86        Ok(spec)
87    }
88
89    ///
90    /// Save the calculated spectrogram as a PNG image.
91    ///
92    /// # Arguments
93    ///
94    ///  * `fname` - The path to the PNG to save to the filesystem.
95    ///  * `freq_scale` - The type of frequency scale to use for the spectrogram.
96    ///  * `gradient` - The colour gradient to use for the spectrogram.
97    ///  * `w_img` - The output image width.
98    ///  * `h_img` - The output image height.
99    ///
100    #[cfg(feature = "png")]
101    pub fn to_png(
102        &mut self,
103        fname: &Path,
104        freq_scale: FrequencyScale,
105        gradient: &mut ColourGradient,
106        w_img: usize,
107        h_img: usize,
108    ) -> Result<(), std::io::Error> {
109        let buf = self.to_buffer(freq_scale, w_img, h_img);
110
111        let mut img: Vec<u8> = vec![0u8; w_img * h_img * 4];
112        self.buf_to_img(&buf, &mut img, gradient);
113
114        let file = File::create(fname)?;
115        let w = &mut BufWriter::new(file);
116        let mut encoder = png::Encoder::new(w, w_img as u32, h_img as u32);
117        encoder.set(png::ColorType::RGBA).set(png::BitDepth::Eight);
118        let mut writer = encoder.write_header()?;
119        writer.write_image_data(&img)?; // Save
120
121        Ok(())
122    }
123
124    ///
125    /// Create the spectrogram in memory as a PNG.
126    ///
127    /// # Arguments
128    ///
129    ///  * `freq_scale` - The type of frequency scale to use for the spectrogram.
130    ///  * `gradient` - The colour gradient to use for the spectrogram.
131    ///  * `w_img` - The output image width.
132    ///  * `h_img` - The output image height.
133    ///
134    #[cfg(feature = "png")]
135    pub fn to_png_in_memory(
136        &mut self,
137        freq_scale: FrequencyScale,
138        gradient: &mut ColourGradient,
139        w_img: usize,
140        h_img: usize,
141    ) -> Result<Vec<u8>, std::io::Error> {
142        let buf = self.to_buffer(freq_scale, w_img, h_img);
143
144        let mut img: Vec<u8> = vec![0u8; w_img * h_img * 4];
145        self.buf_to_img(&buf, &mut img, gradient);
146
147        let mut pngbuf: Vec<u8> = Vec::new();
148        let mut encoder = png::Encoder::new(&mut pngbuf, w_img as u32, h_img as u32);
149        encoder.set(png::ColorType::RGBA).set(png::BitDepth::Eight);
150        let mut writer = encoder.write_header()?;
151        writer.write_image_data(&img)?;
152
153        // The png writer needs to be explicitly dropped
154        drop(writer);
155        Ok(pngbuf)
156    }
157
158    ///
159    /// Create the spectrogram in memory as raw RGBA format.
160    ///
161    /// # Arguments
162    ///
163    ///  * `freq_scale` - The type of frequency scale to use for the spectrogram.
164    ///  * `gradient` - The colour gradient to use for the spectrogram.
165    ///  * `w_img` - The output image width.
166    ///  * `h_img` - The output image height.
167    ///
168    pub fn to_rgba_in_memory(
169        &mut self,
170        freq_scale: FrequencyScale,
171        gradient: &mut ColourGradient,
172        w_img: usize,
173        h_img: usize,
174    ) -> Vec<u8> {
175        let buf = self.to_buffer(freq_scale, w_img, h_img);
176
177        let mut img: Vec<u8> = vec![0u8; w_img * h_img * 4];
178        self.buf_to_img(&buf, &mut img, gradient);
179
180        img
181    }
182
183    /// Convenience function to convert the the buffer to an image
184    fn buf_to_img(&self, buf: &[f32], img: &mut [u8], gradient: &mut ColourGradient) {
185        let (min, max) = get_min_max(buf);
186        gradient.set_min(min);
187        gradient.set_max(max);
188
189        // For each pixel, compute the RGBAColour, then assign each byte to output img
190        buf.iter()
191            .map(|val| gradient.get_colour(*val))
192            .flat_map(|c| [c.r, c.g, c.b, c.a].into_iter())
193            .zip(img.iter_mut())
194            .for_each(|(val_rgba, img_rgba)| *img_rgba = val_rgba);
195    }
196
197    ///
198    /// Save the calculated spectrogram as a CSV file.
199    ///
200    /// # Arguments
201    ///
202    ///  * `fname` - The path to the CSV to save to the filesystem.
203    ///  * `freq_scale` - The type of frequency scale to use for the spectrogram.
204    ///  * `cols` - The number of columns.
205    ///  * `rows` - The number of rows.
206    ///
207    pub fn to_csv(
208        &mut self,
209        fname: &Path,
210        freq_scale: FrequencyScale,
211        cols: usize,
212        rows: usize,
213    ) -> Result<(), std::io::Error> {
214        let result = self.to_buffer(freq_scale, cols, rows);
215
216        let mut writer = csv::Writer::from_path(fname)?;
217
218        // Create the CSV header
219        let mut csv_record: Vec<String> = (0..cols).map(|x| x.to_string()).collect();
220        writer.write_record(&csv_record)?;
221
222        let mut i = 0;
223        for _ in 0..rows {
224            for c_rec in csv_record.iter_mut().take(cols) {
225                let val = result[i];
226                i += 1;
227                *c_rec = val.to_string();
228            }
229            writer.write_record(&csv_record)?;
230        }
231
232        writer.flush()?; // Save
233
234        Ok(())
235    }
236
237    ///
238    /// Map the spectrogram to the output buffer.  Essentially scales the
239    /// frequency to map to the vertical axis (y-axis) of the output and
240    /// scale the x-axis to match the output.  It will also convert the
241    /// spectrogram to dB.
242    ///
243    /// # Arguments
244    ///
245    ///  * `freq_scale` - The type of frequency scale to use for the spectrogram.
246    ///  * `img_width` - The output image width.
247    ///  * `img_height` - The output image height.
248    ///
249    pub fn to_buffer(
250        &self,
251        freq_scale: FrequencyScale,
252        img_width: usize,
253        img_height: usize,
254    ) -> Vec<f32> {
255        let mut buf = Vec::with_capacity(self.height * self.width);
256
257        // Apply the log scale if required
258        match freq_scale {
259            FrequencyScale::Log => {
260                let scaler = FreqScaler::create(freq_scale, self.height, self.height);
261                let mut vert_slice = vec![0.0; self.height];
262                for h in 0..self.height {
263                    let (f1, f2) = scaler.scale(h);
264                    let (h1, mut h2) = (f1.floor() as usize, f2.ceil() as usize);
265                    if h2 >= self.height {
266                        h2 = self.height - 1;
267                    }
268                    for w in 0..self.width {
269                        for (hh, val) in vert_slice.iter_mut().enumerate().take(h2).skip(h1) {
270                            *val = self.spec[(hh * self.width) + w];
271                        }
272                        let value = integrate(f1, f2, &vert_slice);
273                        buf.push(value);
274                    }
275                }
276            }
277            FrequencyScale::Linear => {
278                buf.clone_from(&self.spec);
279            }
280        }
281
282        // Convert the buffer to dB
283        to_db(&mut buf);
284
285        resize(&buf, self.width, self.height, img_width, img_height)
286    }
287
288    ///
289    /// Get the minimum and maximum values from the current spectrogram.
290    ///
291    pub fn get_min_max(&self) -> (f32, f32) {
292        get_min_max(&self.spec)
293    }
294
295    ///
296    /// Get the width and height of the spectrogram.
297    ///
298    pub fn shape(&self) -> (usize, usize) {
299        (self.width, self.height)
300    }
301
302    ///
303    /// Get an iterator over the spectrogram data by row index.
304    ///
305    pub fn row_iter<'a>(&'a self, row_idx: usize) -> impl Iterator<Item = &'a f32> + 'a {
306        self.spec
307            .chunks_exact(self.width)
308            .skip(row_idx)
309            .flatten()
310            .take(self.width)
311    }
312}
313
314pub fn get_min_max(data: &[f32]) -> (f32, f32) {
315    let mut min = f32::MAX;
316    let mut max = f32::MIN;
317    for val in data {
318        min = f32::min(*val, min);
319        max = f32::max(*val, max);
320    }
321    (min, max)
322}
323
324#[cfg(feature = "rayon")]
325fn to_db(buf: &mut [f32]) {
326    let ref_db = buf
327        .par_chunks(1_000)
328        .fold(
329            || f32::MIN,
330            |acc, chunk| {
331                let v = chunk.iter().fold(f32::MIN, |acc, &v| f32::max(acc, v));
332                if acc > v {
333                    acc
334                } else {
335                    v
336                }
337            },
338        )
339        .reduce(|| f32::MIN, f32::max);
340
341    let amp_ref = ref_db * ref_db;
342    let offset = 10.0 * (f32::max(1e-10, amp_ref)).log10();
343    let log_spec_max = buf
344        .par_iter_mut()
345        .map(|val| {
346            *val = 10.0 * (f32::max(1e-10, *val * *val)).log10() - offset;
347            *val
348        })
349        .fold(|| f32::MIN, f32::max)
350        .reduce(|| f32::MIN, f32::max);
351    let log_spec_max = log_spec_max - 80.0; // Why 80?  I don't know
352
353    buf.par_chunks_mut(1_000).for_each(|chunk| {
354        for val in chunk.iter_mut() {
355            *val = f32::max(*val, log_spec_max);
356        }
357    });
358}
359
360#[cfg(not(feature = "rayon"))]
361fn to_db(buf: &mut [f32]) {
362    let mut ref_db = f32::MIN;
363    buf.iter().for_each(|v| ref_db = f32::max(ref_db, *v));
364
365    let amp_ref = ref_db * ref_db;
366    let offset = 10.0 * (f32::max(1e-10, amp_ref)).log10();
367    let mut log_spec_max = f32::MIN;
368
369    for val in buf.iter_mut() {
370        *val = 10.0 * (f32::max(1e-10, *val * *val)).log10() - offset;
371        log_spec_max = f32::max(log_spec_max, *val);
372    }
373
374    for val in buf.iter_mut() {
375        *val = f32::max(*val, log_spec_max - 80.0);
376    }
377}
378
379///
380/// Resize the image buffer
381///
382fn resize(buf: &[f32], w_in: usize, h_in: usize, w_out: usize, h_out: usize) -> Vec<f32> {
383    // Resize the buffer to match the user requirements
384    if let Ok(mut resizer) = resize::new(w_in, h_in, w_out, h_out, GrayF32, Lanczos3) {
385        let mut resized_buf = vec![0.0; w_out * h_out];
386        let result = resizer.resize(buf.as_gray(), resized_buf.as_gray_mut());
387        if result.is_ok() {
388            return resized_buf;
389        }
390    }
391
392    // If this happens there resize return an Err
393    vec![]
394}
395
396///
397/// Integrate `spec` from `x1` to `x2`, where `x1` and `x2` are
398/// floating point indicies where we take the fractional component into
399/// account as well.
400///
401/// Integration is uses simple linear interpolation.
402///
403/// # Arguments
404///
405/// * `x1` - The fractional index that points to `spec`.
406/// * `x2` - The fractional index that points to `spec`.
407/// * `spec` - The values that require integration.
408///
409/// # Returns
410///
411/// The result of the integration.
412///
413fn integrate(x1: f32, x2: f32, spec: &[f32]) -> f32 {
414    let mut i_x1 = x1.floor() as usize;
415    let i_x2 = (x2 - 0.000001).floor() as usize;
416
417    // Calculate the ratio from
418    let area = |y, frac| y * frac;
419
420    if i_x1 >= i_x2 {
421        // Sub-cell integration
422        area(spec[i_x1], x2 - x1)
423    } else {
424        // Need to integrate from x1 to x2 over multiple indicies.
425        let mut result = area(spec[i_x1], (i_x1 + 1) as f32 - x1);
426        i_x1 += 1;
427        while i_x1 < i_x2 {
428            result += spec[i_x1];
429            i_x1 += 1;
430        }
431        if i_x1 >= spec.len() {
432            i_x1 = spec.len() - 1;
433        }
434        result += area(spec[i_x1], x2 - i_x1 as f32);
435        result
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_integrate() {
445        let v = vec![1.0, 2.0, 4.0, 1.123];
446
447        // No x distance
448        let c = integrate(0.0, 0.0, &v);
449        assert!((c - 0.0).abs() < 0.0001);
450
451        // No number boundary
452        let c = integrate(0.25, 1.0, &v);
453        assert!((c - 0.75).abs() < 0.0001);
454
455        let c = integrate(0.0, 1.0, &v);
456        assert!((c - 1.0).abs() < 0.0001);
457
458        let c = integrate(3.75, 4.0, &v);
459        assert!((c - 1.123 / 4.0).abs() < 0.0001);
460
461        let c = integrate(0.5, 1.0, &v);
462        assert!((c - 0.5).abs() < 0.0001);
463
464        // Accross one boundary
465        let c = integrate(0.75, 1.25, &v);
466        assert!((c - 0.75).abs() < 0.0001);
467
468        let c = integrate(1.8, 2.6, &v);
469        assert!((c - 2.8).abs() < 0.0001);
470
471        // Full Range
472        let c = integrate(0.0, 4.0, &v);
473        assert!((c - 8.123).abs() < 0.0001);
474    }
475
476    #[test]
477    fn test_from_raw() {
478        let raw = vec![-1i16, -2, -3, 4, 5, 6];
479        let spec = Spectrogram::from_raw(&raw, 2, 3).unwrap();
480        assert_eq!(spec.width, 2);
481        assert_eq!(spec.height, 3);
482        assert_eq!(spec.spec, vec![-1.0f32, -2.0, -3.0, 4.0, 5.0, 6.0]);
483    }
484}