servo_media_audio/
analyser_node.rs

1use crate::block::{Block, Chunk, FRAMES_PER_BLOCK_USIZE};
2use crate::node::AudioNodeEngine;
3use crate::node::BlockInfo;
4use crate::node::{AudioNodeType, ChannelInfo, ChannelInterpretation};
5use std::cmp;
6use std::f32::consts::PI;
7
8#[derive(AudioNodeCommon)]
9pub(crate) struct AnalyserNode {
10    channel_info: ChannelInfo,
11    callback: Box<dyn FnMut(Block) + Send>,
12}
13
14impl AnalyserNode {
15    pub fn new(callback: Box<dyn FnMut(Block) + Send>, channel_info: ChannelInfo) -> Self {
16        Self {
17            callback,
18            channel_info,
19        }
20    }
21}
22
23impl AudioNodeEngine for AnalyserNode {
24    fn node_type(&self) -> AudioNodeType {
25        AudioNodeType::AnalyserNode
26    }
27
28    fn process(&mut self, inputs: Chunk, _: &BlockInfo) -> Chunk {
29        debug_assert!(inputs.len() == 1);
30
31        let mut push = inputs.blocks[0].clone();
32        push.mix(1, ChannelInterpretation::Speakers);
33
34        (self.callback)(push);
35
36        // analyser node doesn't modify the inputs
37        inputs
38    }
39}
40
41/// From https://webaudio.github.io/web-audio-api/#dom-analysernode-fftsize
42pub const MAX_FFT_SIZE: usize = 32768;
43pub const MAX_BLOCK_COUNT: usize = MAX_FFT_SIZE / FRAMES_PER_BLOCK_USIZE;
44
45/// The actual analysis is done on the DOM side. We provide
46/// the actual base functionality in this struct, so the DOM
47/// just has to do basic shimming
48pub struct AnalysisEngine {
49    /// The number of past sample-frames to consider in the FFT
50    fft_size: usize,
51    smoothing_constant: f64,
52    min_decibels: f64,
53    max_decibels: f64,
54    /// This is a ring buffer containing the last MAX_FFT_SIZE
55    /// sample-frames
56    data: Box<[f32; MAX_FFT_SIZE]>,
57    /// The index of the current block
58    current_block: usize,
59    /// Have we computed the FFT already?
60    fft_computed: bool,
61    /// Cached blackman window data
62    blackman_windows: Vec<f32>,
63    /// The smoothed FFT data (in frequency domain)
64    smoothed_fft_data: Vec<f32>,
65    /// The computed FFT data, in decibels
66    computed_fft_data: Vec<f32>,
67    /// The windowed time domain data
68    /// Used during FFT computation
69    windowed: Vec<f32>,
70}
71
72impl AnalysisEngine {
73    pub fn new(
74        fft_size: usize,
75        smoothing_constant: f64,
76        min_decibels: f64,
77        max_decibels: f64,
78    ) -> Self {
79        debug_assert!(fft_size >= 32 && fft_size <= 32768);
80        // must be a power of two
81        debug_assert!(fft_size & fft_size - 1 == 0);
82        debug_assert!(smoothing_constant <= 1. && smoothing_constant >= 0.);
83        debug_assert!(max_decibels > min_decibels);
84        Self {
85            fft_size,
86            smoothing_constant,
87            min_decibels,
88            max_decibels,
89            data: Box::new([0.; MAX_FFT_SIZE]),
90            current_block: MAX_BLOCK_COUNT - 1,
91            fft_computed: false,
92            blackman_windows: Vec::with_capacity(fft_size),
93            computed_fft_data: Vec::with_capacity(fft_size / 2),
94            smoothed_fft_data: Vec::with_capacity(fft_size / 2),
95            windowed: Vec::with_capacity(fft_size),
96        }
97    }
98
99    pub fn set_fft_size(&mut self, fft_size: usize) {
100        debug_assert!(fft_size >= 32 && fft_size <= 32768);
101        // must be a power of two
102        debug_assert!(fft_size & fft_size - 1 == 0);
103        self.fft_size = fft_size;
104        self.fft_computed = false;
105    }
106
107    pub fn get_fft_size(&self) -> usize {
108        self.fft_size
109    }
110
111    pub fn set_smoothing_constant(&mut self, smoothing_constant: f64) {
112        debug_assert!(smoothing_constant <= 1. && smoothing_constant >= 0.);
113        self.smoothing_constant = smoothing_constant;
114        self.fft_computed = false;
115    }
116
117    pub fn get_smoothing_constant(&self) -> f64 {
118        self.smoothing_constant
119    }
120
121    pub fn set_min_decibels(&mut self, min_decibels: f64) {
122        debug_assert!(min_decibels < self.max_decibels);
123        self.min_decibels = min_decibels;
124    }
125
126    pub fn get_min_decibels(&self) -> f64 {
127        self.min_decibels
128    }
129
130    pub fn set_max_decibels(&mut self, max_decibels: f64) {
131        debug_assert!(self.min_decibels < max_decibels);
132        self.max_decibels = max_decibels;
133    }
134
135    pub fn get_max_decibels(&self) -> f64 {
136        self.max_decibels
137    }
138
139    fn advance(&mut self) {
140        self.current_block += 1;
141        if self.current_block >= MAX_BLOCK_COUNT {
142            self.current_block = 0;
143        }
144    }
145
146    /// Get the data of the current block
147    fn curent_block_mut(&mut self) -> &mut [f32] {
148        let index = FRAMES_PER_BLOCK_USIZE * self.current_block;
149        &mut self.data[index..(index + FRAMES_PER_BLOCK_USIZE)]
150    }
151
152    /// Given an index from 0 to fft_size, convert it into an index into
153    /// the backing array
154    fn convert_index(&self, index: usize) -> usize {
155        let offset = self.fft_size - index;
156        let last_element = (1 + self.current_block) * FRAMES_PER_BLOCK_USIZE - 1;
157        if offset > last_element {
158            MAX_FFT_SIZE - offset + last_element
159        } else {
160            last_element - offset
161        }
162    }
163
164    /// Given an index into the backing array, increment it
165    fn advance_index(&self, index: &mut usize) {
166        *index += 1;
167        if *index >= MAX_FFT_SIZE {
168            *index = 0;
169        }
170    }
171
172    pub fn push(&mut self, mut block: Block) {
173        debug_assert!(block.chan_count() == 1);
174        self.advance();
175        if !block.is_silence() {
176            self.curent_block_mut().copy_from_slice(block.data_mut());
177        }
178        self.fft_computed = false;
179    }
180
181    /// https://webaudio.github.io/web-audio-api/#blackman-window
182    fn compute_blackman_windows(&mut self) {
183        if self.blackman_windows.len() == self.fft_size {
184            return;
185        }
186        const ALPHA: f32 = 0.16;
187        const ALPHA_0: f32 = (1. - ALPHA) / 2.;
188        const ALPHA_1: f32 = 1. / 2.;
189        const ALPHA_2: f32 = ALPHA / 2.;
190        self.blackman_windows.resize(self.fft_size, 0.);
191        let coeff = PI * 2. / self.fft_size as f32;
192        for n in 0..self.fft_size {
193            self.blackman_windows[n] = ALPHA_0 - ALPHA_1 * (coeff * n as f32).cos()
194                + ALPHA_2 * (2. * coeff * n as f32).cos();
195        }
196    }
197
198    fn apply_blackman_window(&mut self) {
199        self.compute_blackman_windows();
200        self.windowed.resize(self.fft_size, 0.);
201
202        let mut data_idx = self.convert_index(0);
203        for n in 0..self.fft_size {
204            self.windowed[n] = self.blackman_windows[n] * self.data[data_idx];
205            self.advance_index(&mut data_idx);
206        }
207    }
208
209    fn compute_fft(&mut self) {
210        if self.fft_computed {
211            return;
212        }
213        self.fft_computed = true;
214        self.apply_blackman_window();
215        self.computed_fft_data.resize(self.fft_size / 2, 0.);
216        self.smoothed_fft_data.resize(self.fft_size / 2, 0.);
217
218        for k in 0..(self.fft_size / 2) {
219            let mut sum_real = 0.;
220            let mut sum_imaginary = 0.;
221            let factor = -2. * PI * k as f32 / self.fft_size as f32;
222            for n in 0..(self.fft_size) {
223                sum_real += self.windowed[n] * (factor * n as f32).cos();
224                sum_imaginary += self.windowed[n] * (factor * n as f32).sin();
225            }
226            let sum_real = sum_real / self.fft_size as f32;
227            let sum_imaginary = sum_imaginary / self.fft_size as f32;
228            let magnitude = (sum_real * sum_real + sum_imaginary * sum_imaginary).sqrt();
229            self.smoothed_fft_data[k] = (self.smoothing_constant * self.smoothed_fft_data[k] as f64
230                + (1. - self.smoothing_constant) * magnitude as f64)
231                as f32;
232            self.computed_fft_data[k] = 20. * self.smoothed_fft_data[k].log(10.);
233        }
234    }
235
236    pub fn fill_time_domain_data(&self, dest: &mut [f32]) {
237        let mut data_idx = self.convert_index(0);
238        let end = cmp::min(self.fft_size, dest.len());
239        for n in 0..end {
240            dest[n] = self.data[data_idx];
241            self.advance_index(&mut data_idx);
242        }
243    }
244
245    pub fn fill_byte_time_domain_data(&self, dest: &mut [u8]) {
246        let mut data_idx = self.convert_index(0);
247        let end = cmp::min(self.fft_size, dest.len());
248        for n in 0..end {
249            let result = 128. * (1. + self.data[data_idx]);
250            dest[n] = clamp_255(result);
251            self.advance_index(&mut data_idx)
252        }
253    }
254
255    pub fn fill_frequency_data(&mut self, dest: &mut [f32]) {
256        self.compute_fft();
257        let len = cmp::min(dest.len(), self.computed_fft_data.len());
258        dest[0..len].copy_from_slice(&mut self.computed_fft_data[0..len]);
259    }
260
261    pub fn fill_byte_frequency_data(&mut self, dest: &mut [u8]) {
262        self.compute_fft();
263        let len = cmp::min(dest.len(), self.computed_fft_data.len());
264        let ratio = 255. / (self.max_decibels - self.min_decibels);
265        for freq in 0..len {
266            let result = ratio * (self.computed_fft_data[freq] as f64 - self.min_decibels);
267            dest[freq] = clamp_255(result as f32);
268        }
269    }
270}
271
272fn clamp_255(val: f32) -> u8 {
273    if val > 255. {
274        255
275    } else if val < 0. {
276        0
277    } else {
278        val as u8
279    }
280}