From d9b02a5c20d358f312cd77543b272f4490e9077a Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 12:25:05 +0100 Subject: [PATCH 01/18] feat: refactor analyser node - use shared buffer to move audio data to control thread - implement full API --- src/analysis.rs | 937 ++++++++++++++++++++++++++++++++----------- src/node/analyser.rs | 205 ++++------ 2 files changed, 789 insertions(+), 353 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 27a1f8a3..0bb21c58 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -2,17 +2,36 @@ //! //! These are used in the [`AnalyserNode`](crate::node::AnalyserNode) -use crate::render::AudioRenderQuantumChannel; -use crate::RENDER_QUANTUM_SIZE; +use std::f32::consts::PI; +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use realfft::{num_complex::Complex, RealFftPlanner}; -use std::f32::consts::PI; +use crate::RENDER_QUANTUM_SIZE; -/// FFT size is max 32768 samples, mandated in spec -const MAX_SAMPLES: usize = 32768; -/// Max FFT size corresponds to 256 render quanta -const MAX_QUANTA: usize = MAX_SAMPLES / RENDER_QUANTUM_SIZE; + +// @todo - modify AtomicF32 in `lib.rs` to expose Ordering +#[derive(Debug)] +struct AtomicF32 { + inner: AtomicU32, +} + +impl AtomicF32 { + pub fn new(v: f32) -> Self { + Self { + inner: AtomicU32::new(u32::from_ne_bytes(v.to_ne_bytes())), + } + } + + pub fn load(&self, ordering: Ordering) -> f32 { + f32::from_ne_bytes(self.inner.load(ordering).to_ne_bytes()) + } + + pub fn store(&self, v: f32, ordering: Ordering) { + self.inner.store(u32::from_ne_bytes(v.to_ne_bytes()), ordering); + } +} /// Blackman window values iterator with alpha = 0.16 pub fn generate_blackman(size: usize) -> impl Iterator { @@ -27,358 +46,812 @@ pub fn generate_blackman(size: usize) -> impl Iterator { }) } -/// Ring buffer for time domain analysis -struct TimeAnalyser { - buffer: Vec, - index: u8, - previous_cycle_index: u8, + +/// FFT size is max 32768 samples, mandated in spec +// const MAX_SAMPLES: usize = 32768; +/// Max FFT size corresponds to 256 render quanta +// const MAX_QUANTA: usize = MAX_SAMPLES / RENDER_QUANTUM_SIZE; + +const DEFAULT_SMOOTHING_TIME_CONSTANT: f64 = 0.8; +const DEFAULT_MIN_DECIBELS: f64 = -100.; +const DEFAULT_MAX_DECIBELS: f64 = -30.; + +const MIN_FFT_SIZE: usize = 32; +const MAX_FFT_SIZE: usize = 32768; +const DEFAULT_FFT_SIZE: usize = 2048; + +// [spec] This MUST be a power of two in the range 32 to 32768, otherwise an +// IndexSizeError exception MUST be thrown. +fn assert_valid_fft_size(fft_size: usize) { + if !fft_size.is_power_of_two() { + panic!( + "IndexSizeError - Invalid fft size: {:?} is not a power of two", + fft_size + ); + } + + if fft_size < MIN_FFT_SIZE || fft_size > MAX_FFT_SIZE { + panic!( + "IndexSizeError - Invalid fft size: {:?} is outside range [{:?}, {:?}]", + fft_size, MIN_FFT_SIZE, MAX_FFT_SIZE + ); + } } -impl TimeAnalyser { - /// Create a new TimeAnalyser - fn new() -> Self { - Self { - buffer: Vec::with_capacity(MAX_QUANTA), - index: 0, - previous_cycle_index: 0, - } +// [spec] If the value of this attribute is set to a value less than 0 or more +// than 1, an IndexSizeError exception MUST be thrown. +fn assert_valid_smoothing_time_constant(smoothing_time_constant: f64) { + if smoothing_time_constant < 0. || smoothing_time_constant > 1. { + panic!( + "IndexSizeError - Invalid smoothing time constant: {:?} is outside range [0, 1]", + smoothing_time_constant + ); + } +} + +// [spec] If the value of this attribute is set to a value more than or equal +// to maxDecibels, an IndexSizeError exception MUST be thrown. +fn assert_valid_min_decibels(min_decibels: f64, max_decibels: f64) { + if min_decibels >= max_decibels { + panic!( + "IndexSizeError - Invalid min decibels: {:?} is greater than or equals to max decibels {:?}", + min_decibels, max_decibels + ); + } +} + +// [spec] If the value of this attribute is set to a value less than or equal to +// minDecibels, an IndexSizeError exception MUST be thrown. +fn assert_valid_max_decibels(max_decibels: f64, min_decibels: f64) { + if max_decibels <= min_decibels { + panic!( + "IndexSizeError - Invalid max decibels: {:?} is lower than or equals to min decibels {:?}", + max_decibels, min_decibels + ); } +} + + +// as the queue is composed of AtomicF32 having only 1 render quantum of room should be enough +const RING_BUFFER_SIZE: usize = MAX_FFT_SIZE + RENDER_QUANTUM_SIZE; - /// Add samples to the ring buffer - fn add_data(&mut self, data: AudioRenderQuantumChannel) { - if self.buffer.len() < 256 { - self.buffer.push(data); - } else { - self.buffer[self.index as usize] = data; +// single producer / single consumer ring buffer +pub(crate) struct AnalyserRingBuffer { + buffer: Arc>, + write_index: AtomicUsize, +} + +impl AnalyserRingBuffer { + pub fn new() -> Self { + let mut buffer = Vec::with_capacity(RING_BUFFER_SIZE); + buffer.resize_with(RING_BUFFER_SIZE, || AtomicF32::new(0.)); + + Self { + buffer: Arc::new(buffer), + write_index: AtomicUsize::new(0), } - self.index = self.index.wrapping_add(1); } - /// Check if we have completed a full round of `fft_size` samples - fn check_complete_cycle(&mut self, fft_size: usize) -> bool { - // number of buffers processed since last complete cycle - let processed = self.index.wrapping_sub(self.previous_cycle_index); - let processed_samples = processed as usize * RENDER_QUANTUM_SIZE; + pub fn write(&self, src: &[f32]) { + let mut write_index = self.write_index.load(Ordering::SeqCst); + let len = src.len(); - // cycle is complete when divisible by fft_size - if processed_samples % fft_size == 0 { - self.previous_cycle_index = self.index; - return true; + src.iter().enumerate().for_each(|(index, value)| { + let position = (write_index + index) % RING_BUFFER_SIZE; + self.buffer[position].store(*value, Ordering::Relaxed); + }); + + write_index += len; + + if write_index >= RING_BUFFER_SIZE { + write_index -= RING_BUFFER_SIZE; } - false + self.write_index.store(write_index, Ordering::SeqCst); } - /// Read out the ring buffer (max `fft_size` samples) - fn get_float_time(&self, buffer: &mut [f32], fft_size: usize) { - // buffer is never empty when this call is made - debug_assert!(!self.buffer.is_empty()); + pub fn read(&self, dst: &mut [f32], max_len: usize) { + let write_index = self.write_index.load(Ordering::SeqCst); + // let fft_size = self.fft_size.load(Ordering::SeqCst); + let len = dst.len().min(max_len); - // get a reference to the 'silence buffer' - let silence = self.buffer[0].silence(); + dst.iter_mut().take(len).enumerate().for_each(|(index, value)| { + // offset calculation by RING_BUFFER_SIZE so we cant negative values + let position = (RING_BUFFER_SIZE + write_index - len + index) % RING_BUFFER_SIZE; + *value = self.buffer[position].load(Ordering::Relaxed); + }); + } - // order the ring buffer, and pad with silence - let data_chunks = self.buffer[self.index as usize..] - .iter() - .chain(self.buffer[..self.index as usize].iter()) - .rev() - .chain(std::iter::repeat(&silence)); + // so that we can easily share the tests with the unsafe version + #[cfg(test)] + fn raw(&self) -> Vec { + let mut slice = vec![0.; RING_BUFFER_SIZE]; - // split the output buffer in same sized chunks - let true_size = fft_size.min(buffer.len()); - let buf_chunks = buffer[0..true_size].chunks_mut(RENDER_QUANTUM_SIZE).rev(); + self.buffer.iter().zip(slice.iter_mut()).for_each(|(a, b)| { + *b = a.load(Ordering::SeqCst); + }); - // copy data from internal buffer to output buffer - buf_chunks - .zip(data_chunks) - .for_each(|(b, d)| b.copy_from_slice(&d[..b.len()])); + slice } } -/// Analyser kernel for time domain and frequency data +// this cannot be made thread safe because RealFftPlanner does not support it pub(crate) struct Analyser { - time: TimeAnalyser, + // ring buffer informations + ring_buffer: Arc, + fft_size: usize, + smoothing_time_constant: f64, + min_decibels: f64, + max_decibels: f64, fft_planner: RealFftPlanner, fft_input: Vec, fft_scratch: Vec>, fft_output: Vec>, - - current_fft_size: usize, - previous_block: Vec, + last_fft_output: Vec, blackman: Vec, + } impl Analyser { - /// Create a new analyser kernel - pub fn new(initial_fft_size: usize) -> Self { + pub fn new() -> Self { + let ring_buffer = Arc::new(AnalyserRingBuffer::new()); + // FFT utils let mut fft_planner = RealFftPlanner::::new(); - let max_fft = fft_planner.plan_fft_forward(MAX_SAMPLES); + let max_fft = fft_planner.plan_fft_forward(MAX_FFT_SIZE); let fft_input = max_fft.make_input_vec(); let fft_scratch = max_fft.make_scratch_vec(); let fft_output = max_fft.make_output_vec(); - let previous_block = vec![0.; fft_output.len()]; + let last_fft_output = vec![0.; fft_output.len()]; // precalculate Blackman window values, reserve enough space for all input sizes let mut blackman = Vec::with_capacity(fft_input.len()); - generate_blackman(initial_fft_size).for_each(|v| blackman.push(v)); + generate_blackman(DEFAULT_FFT_SIZE).for_each(|v| blackman.push(v)); Self { - time: TimeAnalyser::new(), + ring_buffer, + fft_size: DEFAULT_FFT_SIZE, + smoothing_time_constant: DEFAULT_SMOOTHING_TIME_CONSTANT, + min_decibels: DEFAULT_MIN_DECIBELS, + max_decibels: DEFAULT_MAX_DECIBELS, fft_planner, fft_input, fft_scratch, fft_output, - current_fft_size: initial_fft_size, - previous_block, + last_fft_output, blackman, } } - pub fn current_fft_size(&self) -> usize { - self.current_fft_size + pub fn get_ring_buffer_clone(&self) -> Arc { + self.ring_buffer.clone() } - /// Add samples to the ring buffer - pub fn add_data(&mut self, data: AudioRenderQuantumChannel) { - self.time.add_data(data); + pub fn fft_size(&self) -> usize { + self.fft_size } - /// Read out the time domain ring buffer (max `fft_size samples) - pub fn get_float_time(&self, buffer: &mut [f32], fft_size: usize) { - self.time.get_float_time(buffer, fft_size); + pub fn set_fft_size(&mut self, fft_size: usize) { + assert_valid_fft_size(fft_size); + + let current_fft_size = self.fft_size; + + if current_fft_size != fft_size { + // reset last fft buffer + self.last_fft_output.iter_mut().for_each(|v| *v = 0.); + // generate blackman window + self.blackman.clear(); + generate_blackman(fft_size).for_each(|v| self.blackman.push(v)); + + self.fft_size = fft_size; + } } - /// Check if we have completed a full round of `fft_size` samples - pub fn check_complete_cycle(&mut self, fft_size: usize) -> bool { - self.time.check_complete_cycle(fft_size) + pub fn smoothing_time_constant(&self) -> f64 { + self.smoothing_time_constant } - /// Copy the frequency data - pub fn get_float_frequency(&mut self, buffer: &mut [f32]) { - let previous_block = &mut self.previous_block[..self.current_fft_size / 2 + 1]; + pub fn set_smoothing_time_constant(&mut self, value: f64) { + assert_valid_smoothing_time_constant(value); + self.smoothing_time_constant = value; + } - // nomalizing, conversion to dB and fill buffer - let norm = 20. * (self.current_fft_size as f32).sqrt().log10(); - buffer - .iter_mut() - .zip(previous_block.iter()) - .for_each(|(b, o)| *b = 20. * o.log10() - norm); + pub fn min_decibels(&self) -> f64 { + self.min_decibels } - /// Calculate the frequency data - pub fn calculate_float_frequency(&mut self, fft_size: usize, smoothing_time_constant: f32) { - // reset state after resizing - if self.current_fft_size != fft_size { - // previous block data - self.previous_block[0..fft_size / 2 + 1] - .iter_mut() - .for_each(|v| *v = 0.); + pub fn set_min_decibels(&mut self, value: f64) { + assert_valid_min_decibels(value, self.max_decibels); + self.min_decibels = value; + } - // blackman window - self.blackman.clear(); - generate_blackman(fft_size).for_each(|v| self.blackman.push(v)); + pub fn max_decibels(&self) -> f64 { + self.max_decibels + } - self.current_fft_size = fft_size; - } + pub fn set_max_decibels(&mut self, value: f64) { + assert_valid_max_decibels(value, self.min_decibels); + self.max_decibels = value; + } - let r2c = self.fft_planner.plan_fft_forward(fft_size); + pub fn frequency_bin_count(&self) -> usize { + self.fft_size / 2 + } + + // @note: `add_input`, `get_float_time_domain_data`, `get_byte_time_domain_data` + // are the methods that should be adapted to review the buffering strategy + + // [spec] Write the current time-domain data (waveform data) into array. + // If array has fewer elements than the value of fftSize, the excess elements + // will be dropped. If array has more elements than the value of fftSize, + // the excess elements will be ignored. The most recent fftSize frames are + // written (after downmixing) + pub fn get_float_time_domain_data(&self, dst: &mut [f32]) { + let fft_size = self.fft_size; + self.ring_buffer.read(dst, fft_size); + } + + // we need to duplicate the `get_float_time_domain_data` to avoid creating + // an intermediate vector of floats + pub fn get_byte_time_domain_data(&self, dst: &mut [u8]) { + let fft_size = self.fft_size; + let mut tmp = vec![0.; dst.len()]; + self.ring_buffer.read(&mut tmp, fft_size); + + dst.iter_mut().zip(tmp.iter()).for_each(|(o, i)| { + *o = (128. * (1. + i)) as u8; + }); + } - // setup proper sized buffers + fn compute_fft(&mut self, fft_size: usize) { + let smoothing_time_constant = self.smoothing_time_constant as f32; + // setup FFT planner and properly sized buffers + let r2c = self.fft_planner.plan_fft_forward(fft_size); let input = &mut self.fft_input[..fft_size]; let output = &mut self.fft_output[..fft_size / 2 + 1]; let scratch = &mut self.fft_scratch[..r2c.get_scratch_len()]; - let previous_block = &mut self.previous_block[..fft_size / 2 + 1]; + // we ignore the Nyquist bin in output, see comment below + let last_fft_output = &mut self.last_fft_output[..fft_size / 2]; - // put time domain data in fft_input - self.time.get_float_time(input, fft_size); + // Compute the current time-domain data. + // The most recent fftSize frames are used in computing the frequency data. + self.ring_buffer.read(input, fft_size); - // blackman window + // Apply a Blackman window to the time domain input data. input .iter_mut() .zip(self.blackman.iter()) .for_each(|(i, b)| *i *= *b); - // calculate frequency data + // Apply a Fourier transform to the windowed time domain input data to + // get real and imaginary frequency data. r2c.process_with_scratch(input, output, scratch).unwrap(); - // smoothing over time - previous_block + // Notes from chromium source code (tbc) + // + // cf. third_party/blink/renderer/platform/audio/fft_frame.h" + // ``` + // Since x[n] is assumed to be real, X[k] has complex conjugate symmetry with + // X[N-k] = conj(X[k]). Thus, we only need to keep X[k] for k = 0 to N/2. + // But since X[0] is purely real and X[N/2] is also purely real, so we could + // place the real part of X[N/2] in the imaginary part of X[0]. Thus + // for k = 1 to N/2: + // + // real_data[k] = Re(X[k]) + // imag_data[k] = Im(X[k]) + // + // and + // + // real_data[0] = Re(X[0]); + // imag_data[0] = Re(X[N/2]) + // ``` + // + // It seems to be why their FFT return only `fft_size / 2` components + // instead `fft_size * 2 + 1`, they pack DC and Nyquist bins together. + // + // However in their `realtime_analyser` they then remove the packed nyquist + // imaginary component: + // cf. third_party/blink/renderer/modules/webaudio/realtime_analyser.h + // ``` + // // Blow away the packed nyquist component. + // imag[0] = 0; + // ``` + // In our case, it seems we can thus just ignore the Nyquist information + // and take the DC bin as it is + + let normalize_factor = 1. / fft_size as f32; + + last_fft_output .iter_mut() .zip(output.iter()) .for_each(|(p, c)| { - *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * c.norm() + let norm = c.norm() * normalize_factor; + *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * norm; + + }); + + // Smooth over time the frequency domain data. + last_fft_output + .iter_mut() + .zip(output.iter().skip(1)) // skip first bin, i.e. DC Offset + .for_each(|(p, c)| { + let norm = c.norm() / fft_size as f32; + *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * norm; + }); + } + + pub fn get_float_frequency_data(&mut self, dst: &mut [f32]) { + let fft_size = self.fft_size; + let frequency_bin_count = self.frequency_bin_count(); + + self.compute_fft(fft_size); + + // [spec] Write the current frequency data into array. If array’s byte + // length is less than frequencyBinCount, the excess elements will be + // dropped. If array’s byte length is greater than the frequencyBinCount + let len = dst.len().min(frequency_bin_count); + + // Convert to dB. + dst.iter_mut() + .take(len) + .zip(self.last_fft_output.iter()) + .for_each(|(v, b)| *v = 20. * b.log10()); + } + + pub fn get_byte_frequency_data(&mut self, dst: &mut [u8]) { + let fft_size = self.fft_size; + let frequency_bin_count = self.frequency_bin_count(); + let min_decibels = self.min_decibels() as f32; + let max_decibels = self.max_decibels() as f32; + + self.compute_fft(fft_size); + + // [spec] Write the current frequency data into array. If array’s byte + // length is less than frequencyBinCount, the excess elements will be + // dropped. If array’s byte length is greater than the frequencyBinCount + let len = dst.len().min(frequency_bin_count); + + // Convert to dB and convert / scale to u8 + dst.iter_mut() + .take(len) + .zip(self.last_fft_output.iter()) + .for_each(|(v, b)| { + let db = 20. * b.log10(); + // 𝑏[π‘˜]=⌊255 / dBπ‘šπ‘Žπ‘₯βˆ’dBπ‘šπ‘–π‘› * (π‘Œ[π‘˜]βˆ’dBπ‘šπ‘–π‘›)βŒ‹ + let scaled = 255. / (max_decibels - min_decibels) * (db - min_decibels); + let clamped = scaled.max(0.).min(255.); + *v = clamped as u8; }); } } + #[cfg(test)] mod tests { + use std::thread; + use float_eq::{assert_float_eq, float_eq}; + use rand::Rng; use super::*; - use crate::render::Alloc; + #[test] + fn test_blackman() { + let values: Vec = generate_blackman(2048).collect(); + + let min = values + .iter() + .fold(1000., |min, &val| if val < min { val } else { min }); + let max = values + .iter() + .fold(0., |max, &val| if val > max { val } else { max }); + assert!(min < 0.01 && min > 0.); + assert!(max > 0.99 && max <= 1.); + + let min_pos = values + .iter() + .position(|&v| float_eq!(v, min, abs_all <= 0.)) + .unwrap(); + let max_pos = values + .iter() + .position(|&v| float_eq!(v, max, abs_all <= 0.)) + .unwrap(); + assert_eq!(min_pos, 0); + assert_eq!(max_pos, 1024); + } + #[test] - fn assert_index_size() { - // silly test to remind us MAX_QUANTA should wrap around a u8, - // otherwise the ring buffer index breaks - assert_eq!(u8::MAX as usize + 1, MAX_QUANTA); + fn test_ring_buffer_write_simple() { + let ring_buffer = AnalyserRingBuffer::new(); + + // check index update + { + // fill the buffer twice so we check the buffer wrap + for i in 1..3 { + for j in 0..(RING_BUFFER_SIZE / RENDER_QUANTUM_SIZE) { + let data = [i as f32; RENDER_QUANTUM_SIZE]; + ring_buffer.write(&data); + + // check write index is properly updated + let write_index = ring_buffer.write_index.load(Ordering::SeqCst); + let expected = (j * RENDER_QUANTUM_SIZE + RENDER_QUANTUM_SIZE) % RING_BUFFER_SIZE; + + assert_eq!(write_index, expected); + } + + // for each loop check the ring buffer is properly filled + let expected = [i as f32; RING_BUFFER_SIZE]; + + assert_float_eq!(&ring_buffer.raw()[..], &expected[..], abs_all <= 1e-12); + } + } } #[test] - fn test_time_domain() { - let alloc = Alloc::with_capacity(256); + fn test_ring_buffer_write_wrap() { + // check values are written in right place + { + let ring_buffer = AnalyserRingBuffer::new(); - let mut analyser = TimeAnalyser::new(); - let mut buffer = vec![-1.; RENDER_QUANTUM_SIZE * 5]; + let offset = 10; + ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); - // feed single data buffer - analyser.add_data(alloc.silence()); + let data = [1.; RENDER_QUANTUM_SIZE]; + ring_buffer.write(&data); - // get data, should be padded with zeroes - analyser.get_float_time(&mut buffer[..], RENDER_QUANTUM_SIZE * 5); - assert_float_eq!( - &buffer[..], - &[0.; 5 * RENDER_QUANTUM_SIZE][..], - abs_all <= 0. - ); + let mut expected = [0.; RING_BUFFER_SIZE]; - // feed data for more than 256 times (the ring buffer size) - for i in 0..258 { - let mut signal = alloc.silence(); - // signal = i - signal.copy_from_slice(&[i as f32; RENDER_QUANTUM_SIZE]); - analyser.add_data(signal); + expected.iter_mut().enumerate().for_each(|(index, v)| { + if index < RENDER_QUANTUM_SIZE - offset || index >= RING_BUFFER_SIZE - offset { + *v = 1. + } else { + *v = 0. + } + }); + + assert_float_eq!(&ring_buffer.raw()[..], &expected[..], abs_all <= 1e-12); } - // this should return non-zero data now - analyser.get_float_time(&mut buffer[..], RENDER_QUANTUM_SIZE * 4); + // check values are written in right order + { + let ring_buffer = AnalyserRingBuffer::new(); + let offset = 2; + ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); - // taken from the end of the ring buffer - assert_float_eq!( - &buffer[0..RENDER_QUANTUM_SIZE], - &[254.; RENDER_QUANTUM_SIZE][..], - abs_all <= 0. - ); - assert_float_eq!( - &buffer[RENDER_QUANTUM_SIZE..2 * RENDER_QUANTUM_SIZE], - &[255.; RENDER_QUANTUM_SIZE][..], - abs_all <= 0. - ); - // taken from the start of the ring buffer - assert_float_eq!( - &buffer[2 * RENDER_QUANTUM_SIZE..3 * RENDER_QUANTUM_SIZE], - &[256.; RENDER_QUANTUM_SIZE][..], - abs_all <= 0. - ); - assert_float_eq!( - &buffer[3 * RENDER_QUANTUM_SIZE..4 * RENDER_QUANTUM_SIZE], - &[257.; RENDER_QUANTUM_SIZE][..], - abs_all <= 0. - ); - // excess capacity should be left unaltered - assert_float_eq!( - &buffer[4 * RENDER_QUANTUM_SIZE..5 * RENDER_QUANTUM_SIZE], - &[0.; RENDER_QUANTUM_SIZE][..], - abs_all <= 0. - ); + let data = [1., 2., 3., 4.]; + ring_buffer.write(&data); + + let mut expected = [0.; RING_BUFFER_SIZE]; + expected[RING_BUFFER_SIZE - 2] = 1.; + expected[RING_BUFFER_SIZE - 1] = 2.; + expected[0] = 3.; + expected[1] = 4.; + + assert_float_eq!(&ring_buffer.raw()[..], &expected[..], abs_all <= 1e-12); + } + } + + #[test] + fn test_ring_buffer_read_simple() { + let ring_buffer = Arc::new(AnalyserRingBuffer::new()); + + // first pass + let data = [1.; RENDER_QUANTUM_SIZE]; + ring_buffer.write(&data); + + // index is where it should be + let index = ring_buffer.write_index.load(Ordering::SeqCst); + assert_eq!(index, RENDER_QUANTUM_SIZE); + + let mut read_buffer = [0.; RENDER_QUANTUM_SIZE]; + ring_buffer.read(&mut read_buffer, RENDER_QUANTUM_SIZE); + // data is good + let expected = [1.; RENDER_QUANTUM_SIZE]; + assert_float_eq!(&expected, &read_buffer, abs_all <= 1e-12); + + + // second pass + let data = [2.; RENDER_QUANTUM_SIZE]; + ring_buffer.write(&data); + + // index is where it should be + let index = ring_buffer.write_index.load(Ordering::SeqCst); + assert_eq!(index, RENDER_QUANTUM_SIZE * 2); + + let mut read_buffer = [0.; RENDER_QUANTUM_SIZE]; + ring_buffer.read(&mut read_buffer, RENDER_QUANTUM_SIZE); + + let expected = [2.; RENDER_QUANTUM_SIZE]; + assert_float_eq!(&expected, &read_buffer, abs_all <= 1e-12); + + let mut full_buffer_expected = [0.; RING_BUFFER_SIZE]; + full_buffer_expected[0..RENDER_QUANTUM_SIZE] + .copy_from_slice(&[1.; RENDER_QUANTUM_SIZE]); + + full_buffer_expected[RENDER_QUANTUM_SIZE..(RENDER_QUANTUM_SIZE * 2)] + .copy_from_slice(&[2.; RENDER_QUANTUM_SIZE]); + + assert_float_eq!(&ring_buffer.raw()[..], &full_buffer_expected[..], abs_all <= 1e-12); + } - // check for small fft_size - buffer.resize(32, 0.); - analyser.get_float_time(&mut buffer[..], RENDER_QUANTUM_SIZE); - assert_float_eq!(&buffer[..], &[257.; 32][..], abs_all <= 0.); + #[test] + fn test_ring_buffer_read_unwrap() { + // check values are read from right place + { + let ring_buffer = AnalyserRingBuffer::new(); + + let offset = 10; + ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); + + let data = [1.; RENDER_QUANTUM_SIZE]; + ring_buffer.write(&data); + + let mut read_buffer = [0.; RENDER_QUANTUM_SIZE]; + ring_buffer.read(&mut read_buffer, RENDER_QUANTUM_SIZE); + + assert_float_eq!(&read_buffer, &data, abs_all <= 1e-12); + } + + // check values are read from right place and written in right order + { + let ring_buffer = AnalyserRingBuffer::new(); + let offset = 2; + ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); + + let data = [1., 2., 3., 4.]; + ring_buffer.write(&data); + + let mut read_buffer = [0.; 4]; + ring_buffer.read(&mut read_buffer, RENDER_QUANTUM_SIZE); + + assert_float_eq!(&read_buffer, &[1., 2., 3., 4.], abs_all <= 1e-12); + } } #[test] - fn test_complete_cycle() { - let alloc = Alloc::with_capacity(256); - let mut analyser = TimeAnalyser::new(); + #[should_panic] + fn test_fft_size_constraints_power_of_two() { + let mut analyser = Analyser::new(); + analyser.set_fft_size(13); + } - // check values smaller than RENDER_QUANTUM_SIZE - analyser.add_data(alloc.silence()); - assert!(analyser.check_complete_cycle(32)); + #[test] + #[should_panic] + fn test_fft_size_constraints_ge_min_fft_size() { + let mut analyser = Analyser::new(); + analyser.set_fft_size(MIN_FFT_SIZE / 2); + } - // check RENDER_QUANTUM_SIZE - analyser.add_data(alloc.silence()); - assert!(analyser.check_complete_cycle(RENDER_QUANTUM_SIZE)); + #[test] + #[should_panic] + fn test_fft_size_constraints_le_max_fft_size() { + let mut analyser = Analyser::new(); + analyser.set_fft_size(MAX_FFT_SIZE * 2); + } - // check multiple of RENDER_QUANTUM_SIZE - analyser.add_data(alloc.silence()); - assert!(!analyser.check_complete_cycle(RENDER_QUANTUM_SIZE * 2)); - analyser.add_data(alloc.silence()); - assert!(analyser.check_complete_cycle(RENDER_QUANTUM_SIZE * 2)); - analyser.add_data(alloc.silence()); - assert!(!analyser.check_complete_cycle(RENDER_QUANTUM_SIZE * 2)); + #[test] + #[should_panic] + fn test_smoothing_time_constant_constraints_lt_zero() { + let mut analyser = Analyser::new(); + analyser.set_smoothing_time_constant(-1.); + } + + #[test] + #[should_panic] + fn test_smoothing_time_constant_constraints_gt_one() { + let mut analyser = Analyser::new(); + analyser.set_smoothing_time_constant(2.); } #[test] - fn test_freq_domain() { - let alloc = Alloc::with_capacity(256); + #[should_panic] + fn test_min_decibels_constraints_lt_max_decibels() { + let mut analyser = Analyser::new(); + analyser.set_min_decibels(DEFAULT_MAX_DECIBELS); + } + + #[test] + #[should_panic] + fn test_max_decibels_constraints_lt_min_decibels() { + let mut analyser = Analyser::new(); + analyser.set_max_decibels(DEFAULT_MIN_DECIBELS); + } + + #[test] + fn test_get_float_time_domain_data_vs_fft_size() { + // dst is bigger than fft_size + { + let mut analyser = Analyser::new(); + analyser.set_fft_size(32); + + let data = [1.; RENDER_QUANTUM_SIZE]; + let buffer = analyser.get_ring_buffer_clone(); + buffer.write(&data); + + let mut dst = [0.; RENDER_QUANTUM_SIZE]; + analyser.get_float_time_domain_data(&mut dst); + + let mut expected = [0.; RENDER_QUANTUM_SIZE]; + expected.iter_mut().take(32).for_each(|v| *v = 1.); + + assert_float_eq!(&dst, &expected, abs_all <= 0.); + } - let fft_size: usize = RENDER_QUANTUM_SIZE * 4; - let mut analyser = Analyser::new(fft_size); - let mut buffer = vec![-1.; fft_size]; + // dst is smaller than fft_size + { + let mut analyser = Analyser::new(); + analyser.set_fft_size(128); - // feed single data buffer - analyser.add_data(alloc.silence()); + let data = [1.; RENDER_QUANTUM_SIZE]; + let buffer = analyser.get_ring_buffer_clone(); + buffer.write(&data); + + let mut dst = [0.; 16]; + analyser.get_float_time_domain_data(&mut dst); + + let expected = [1.; 16]; + + assert_float_eq!(&dst, &expected, abs_all <= 0.); + } + } + + #[test] + fn get_byte_time_domain_data() { + let analyser = Analyser::new(); + + let data = [1.; RENDER_QUANTUM_SIZE]; + let buffer = analyser.get_ring_buffer_clone(); + buffer.write(&data); + + let mut dst = [0; RENDER_QUANTUM_SIZE]; + analyser.get_byte_time_domain_data(&mut dst); + + let expected = [255; RENDER_QUANTUM_SIZE]; + + assert_eq!(&dst, &expected); + + let data = [-1.; RENDER_QUANTUM_SIZE]; + let buffer = analyser.get_ring_buffer_clone(); + buffer.write(&data); + + let mut dst = [0; RENDER_QUANTUM_SIZE]; + analyser.get_byte_time_domain_data(&mut dst); + + let expected = [0; RENDER_QUANTUM_SIZE]; + + assert_eq!(&dst, &expected); + } + + #[test] + fn test_get_float_frequency_data() { + // from https://support.ircam.fr/docs/AudioSculpt/3.0/co/Window%20Size.html + // Let's take a 44100 sampling rate. SR=44100 Hz, F(max) = 22050 Hz. + // With a 1024 window size (512 bins), we get . + // Freq Resolution = 44100/1024 = 43.066 + let sample_rate = 44100.; + let fft_size = 1024; + let freq_resolution = 43.066; + + for num_bin in 1..5 { + // frequency centered on `num_bin` bin, we should have highest value + // in `num_bin` bin + let freq = freq_resolution * (num_bin as f32 + 0.5); + + let mut analyser = Analyser::new(); + analyser.set_fft_size(fft_size); + + let mut signal = Vec::::with_capacity(fft_size); + + for i in 0..fft_size { + let phase = freq * i as f32 / sample_rate; + let sample = (phase * 2. * PI).sin(); + signal.push(sample); + } + + let ring_buffer = analyser.get_ring_buffer_clone(); + ring_buffer.write(&signal); + + let mut bins = vec![0.; analyser.frequency_bin_count()]; + analyser.get_float_frequency_data(&mut bins[..]); + + let highest = bins[num_bin as usize]; + + bins.iter().enumerate().for_each(|(index, db)| { + if index != num_bin as usize { + assert!(db < &highest); + } + }); + } + } + + #[test] + fn test_get_float_frequency_data_vs_frequenc_bin_count() { + let mut analyser = Analyser::new(); + analyser.set_fft_size(RENDER_QUANTUM_SIZE); // get data, should be zero (negative infinity decibel) - analyser.calculate_float_frequency(fft_size, 0.8); - analyser.get_float_frequency(&mut buffer[..]); + let mut bins = vec![-1.; RENDER_QUANTUM_SIZE]; + analyser.get_float_frequency_data(&mut bins[..]); - // only N / 2 + 1 values should contain frequency data, rest is unaltered + // only N / 2 values should contain frequency data, rest is unaltered assert!( - buffer[0..RENDER_QUANTUM_SIZE * 2 + 1] - == [f32::NEG_INFINITY; RENDER_QUANTUM_SIZE * 2 + 1] + bins[0..(RENDER_QUANTUM_SIZE / 2)] + == [f32::NEG_INFINITY; (RENDER_QUANTUM_SIZE / 2)] ); assert_float_eq!( - &buffer[2 * RENDER_QUANTUM_SIZE + 1..], - &[-1.; 2 * RENDER_QUANTUM_SIZE - 1][..], + &bins[(RENDER_QUANTUM_SIZE / 2)..], + &[-1.; (RENDER_QUANTUM_SIZE / 2)][..], abs_all <= 0. ); + } - // feed data for more than 256 times (the ring buffer size) - for i in 0..258 { - let mut signal = alloc.silence(); - // signal = i - signal.copy_from_slice(&[i as f32; RENDER_QUANTUM_SIZE]); - analyser.add_data(signal); - } + #[test] + fn test_get_byte_frequency_data_vs_frequenc_bin_count() { + let mut analyser = Analyser::new(); + analyser.set_fft_size(RENDER_QUANTUM_SIZE); + + // get data, should be zero (negative infinity decibel) + let mut bins = vec![255; RENDER_QUANTUM_SIZE]; + analyser.get_byte_frequency_data(&mut bins[..]); - // this should return other data now - analyser.calculate_float_frequency(fft_size, 0.8); - analyser.get_float_frequency(&mut buffer[..]); + // only N / 2 values should contain frequency data, rest is unaltered assert!( - buffer[0..RENDER_QUANTUM_SIZE * 2 + 1] - != [f32::NEG_INFINITY; RENDER_QUANTUM_SIZE * 2 + 1] + bins[0..(RENDER_QUANTUM_SIZE / 2)] + == [0; (RENDER_QUANTUM_SIZE / 2)] + ); + assert!( + bins[(RENDER_QUANTUM_SIZE / 2)..] + == [255; (RENDER_QUANTUM_SIZE / 2)][..], ); } + // this mostly tries to show that it works concurrently and we don't fall into + // SEGFAULT traps or something, but this is difficult to really test something + // in an accurante way, other tests are there for such thing #[test] - fn test_blackman() { - let values: Vec = generate_blackman(2048).collect(); + fn test_concurrency() { + let analyser = Arc::new(Analyser::new()); + let ring_buffer = analyser.get_ring_buffer_clone(); - let min = values - .iter() - .fold(1000., |min, &val| if val < min { val } else { min }); - let max = values - .iter() - .fold(0., |max, &val| if val > max { val } else { max }); - assert!(min < 0.01 && min > 0.); - assert!(max > 0.99 && max <= 1.); + let num_loops = 10_000; - let min_pos = values - .iter() - .position(|&v| float_eq!(v, min, abs_all <= 0.)) - .unwrap(); - let max_pos = values - .iter() - .position(|&v| float_eq!(v, max, abs_all <= 0.)) - .unwrap(); - assert_eq!(min_pos, 0); - assert_eq!(max_pos, 1024); + let _ = thread::spawn(move || { + let mut rng = rand::thread_rng(); + let mut counter = 0; + + loop { + let rand = rng.gen::(); + let data = [rand; RENDER_QUANTUM_SIZE]; + ring_buffer.write(&data); + + counter += 1; + + if counter == num_loops { + break; + } + + std::thread::sleep(std::time::Duration::from_nanos(30)); + } + }); + + std::thread::sleep(std::time::Duration::from_millis(1)); + + let mut counter = 0; + + loop { + let mut read_buffer = [0.; RENDER_QUANTUM_SIZE]; + analyser.get_float_time_domain_data(&mut read_buffer); + + counter += 1; + + if counter == num_loops { + break; + } + + std::thread::sleep(std::time::Duration::from_nanos(25)); + } } + } diff --git a/src/node/analyser.rs b/src/node/analyser.rs index ca6a825a..d401b6c8 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -1,13 +1,13 @@ -use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; -use std::sync::Arc; +use std::cell::RefCell; +use std::sync::{Arc}; -use crate::analysis::Analyser; +use crate::analysis::{Analyser, AnalyserRingBuffer}; use crate::context::{AudioContextRegistration, BaseAudioContext}; use crate::render::{AudioParamValues, AudioProcessor, AudioRenderQuantum, RenderScope}; use super::{AudioNode, ChannelConfig, ChannelConfigOptions, ChannelInterpretation}; -use crossbeam_channel::{self, Receiver, Sender}; +// use crossbeam_channel::{self, Receiver, Sender}; /// Options for constructing an [`AnalyserNode`] // dictionary AnalyserOptions : AudioNodeOptions { @@ -19,9 +19,7 @@ use crossbeam_channel::{self, Receiver, Sender}; #[derive(Clone, Debug)] pub struct AnalyserOptions { pub fft_size: usize, - #[allow(dead_code)] pub max_decibels: f64, - #[allow(dead_code)] pub min_decibels: f64, pub smoothing_time_constant: f64, pub channel_config: ChannelConfigOptions, @@ -39,24 +37,12 @@ impl Default for AnalyserOptions { } } -enum AnalyserRequest { - FloatTime { - send_done_signal: Sender<()>, - buffer: &'static mut [f32], - }, - FloatFrequency { - send_done_signal: Sender<()>, - buffer: &'static mut [f32], - }, -} - /// Provides real-time frequency and time-domain analysis information pub struct AnalyserNode { registration: AudioContextRegistration, channel_config: ChannelConfig, - fft_size: Arc, - smoothing_time_constant: Arc, - sender: Sender, + // needed to make the AnalyserNode API immutable + analyser: RefCell, } impl AudioNode for AnalyserNode { @@ -80,111 +66,120 @@ impl AudioNode for AnalyserNode { impl AnalyserNode { pub fn new(context: &C, options: AnalyserOptions) -> Self { context.register(move |registration| { - let fft_size = Arc::new(AtomicUsize::new(options.fft_size)); - let smoothing_time_constant = Arc::new(AtomicU32::new( - (options.smoothing_time_constant * 100.) as u32, - )); + // let fft_size = Arc::new(AtomicUsize::new(options.fft_size)); + // let smoothing_time_constant = Arc::new(AtomicU32::new( + // (options.smoothing_time_constant * 100.) as u32, + // )); - let (sender, receiver) = crossbeam_channel::bounded(0); + let analyser = Analyser::new(); + + // apply options let render = AnalyserRenderer { - analyser: Analyser::new(options.fft_size), - fft_size: fft_size.clone(), - smoothing_time_constant: smoothing_time_constant.clone(), - receiver, + ring_buffer: analyser.get_ring_buffer_clone(), }; let node = AnalyserNode { registration, channel_config: options.channel_config.into(), - fft_size, - smoothing_time_constant, - sender, + analyser: RefCell::new(analyser), }; (node, Box::new(render)) }) } - /// Half the FFT size - pub fn frequency_bin_count(&self) -> usize { - self.fft_size.load(Ordering::SeqCst) / 2 - } - /// The size of the FFT used for frequency-domain analysis (in sample-frames) pub fn fft_size(&self) -> usize { - self.fft_size.load(Ordering::SeqCst) + self.analyser.borrow().fft_size() } - /// This MUST be a power of two in the range 32 to 32768 + /// Set FFT size + /// + /// ## Panics + /// + /// This function panics if fft_size is not a power of two or not in the range [32, 32768] pub fn set_fft_size(&self, fft_size: usize) { - // todo assert size - self.fft_size.store(fft_size, Ordering::SeqCst); + self.analyser.borrow_mut().set_fft_size(fft_size); } /// Time averaging parameter with the last analysis frame. + /// A value from 0 -> 1 where 0 represents no time averaging with the last + /// analysis frame. The default value is 0.8. pub fn smoothing_time_constant(&self) -> f64 { - self.smoothing_time_constant.load(Ordering::SeqCst) as f64 / 100. + self.analyser.borrow().smoothing_time_constant() } - /// Set smoothing time constant, this MUST be a value between 0 and 1 + /// Set smoothing time constant + /// + /// ## Panics + /// + /// This function panics if the value is set to a value less than 0 or more than 1. pub fn set_smoothing_time_constant(&self, value: f64) { - // todo assert range - self.smoothing_time_constant - .store((value * 100.) as u32, Ordering::SeqCst); + self.analyser.borrow_mut().set_smoothing_time_constant(value); + } + + + /// Minimum power value in the scaling range for the FFT analysis data for + /// conversion to unsigned byte values. The default value is -100. + pub fn min_decibels(&self) -> f64 { + self.analyser.borrow().min_decibels() + } + + /// Set min decibels + /// + /// ## Panics + /// + /// This function panics if the value is set to a value more than or equal + /// to max decibels. + pub fn set_min_decibels(&self, value: f64) { + self.analyser.borrow_mut().set_min_decibels(value); + } + + /// Maximum power value in the scaling range for the FFT analysis data for + /// conversion to unsigned byte values. The default value is -30. + pub fn max_decibels(&self) -> f64 { + self.analyser.borrow().max_decibels() + } + + /// Set max decibels + /// + /// ## Panics + /// + /// This function panics if the value is set to a value less than or equal + /// to min decibels. + pub fn set_max_decibels(&self, value: f64) { + self.analyser.borrow_mut().set_max_decibels(value); + } + + /// Number of bins in the FFT results, is half the FFT size + pub fn frequency_bin_count(&self) -> usize { + self.analyser.borrow().frequency_bin_count() } /// Copies the current time domain data (waveform data) into the provided buffer - // we can fix this panic cf issue #101 - #[allow(clippy::missing_panics_doc)] pub fn get_float_time_domain_data(&self, buffer: &mut [f32]) { - // SAFETY: - // We transmute to a static reference so we can ship it to the render thread. - // The render thread will only write to the reference, and will send back a signal when it - // is done writing. This function will block until the signal is received. - let buffer: &'static mut [f32] = unsafe { std::mem::transmute(buffer) }; + self.analyser.borrow_mut().get_float_time_domain_data(buffer); + } - let (send_done_signal, recv_done_signal) = crossbeam_channel::bounded(0); - let request = AnalyserRequest::FloatTime { - send_done_signal, - buffer, - }; - self.sender.send(request).unwrap(); - recv_done_signal.recv().unwrap() + pub fn get_byte_time_domain_data(&self, buffer: &mut [u8]) { + self.analyser.borrow_mut().get_byte_time_domain_data(buffer); } /// Copies the current frequency data into the provided buffer - // we can fix this panic cf issue #101 - #[allow(clippy::missing_panics_doc)] pub fn get_float_frequency_data(&self, buffer: &mut [f32]) { - // SAFETY: - // We transmute to a static reference so we can ship it to the render thread. - // The render thread will only write to the reference, and will send back a signal when it - // is done writing. This function will block until the signal is received. - let buffer: &'static mut [f32] = unsafe { std::mem::transmute(buffer) }; - - let (send_done_signal, recv_done_signal) = crossbeam_channel::bounded(0); - let request = AnalyserRequest::FloatFrequency { - send_done_signal, - buffer, - }; - self.sender.send(request).unwrap(); - recv_done_signal.recv().unwrap() + self.analyser.borrow_mut().get_float_frequency_data(buffer); + } + + pub fn get_byte_frequency_data(&self, buffer: &mut [u8]) { + self.analyser.borrow_mut().get_byte_frequency_data(buffer); } } struct AnalyserRenderer { - pub analyser: Analyser, - pub fft_size: Arc, - pub smoothing_time_constant: Arc, - pub receiver: Receiver, + ring_buffer: Arc, } -// SAFETY: -// AudioBuffer is not Send, but the buffer Vec is empty when we move it to the render thread. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Send for AnalyserRenderer {} - impl AudioProcessor for AnalyserRenderer { fn process( &mut self, @@ -200,47 +195,15 @@ impl AudioProcessor for AnalyserRenderer { // pass through input *output = input.clone(); - // add current input to ring buffer + // down mix to mono let mut mono = input.clone(); mono.mix(1, ChannelInterpretation::Speakers); - let mono_data = mono.channel_data(0).clone(); - self.analyser.add_data(mono_data); - - // calculate frequency domain every `fft_size` samples - let fft_size = self.fft_size.load(Ordering::Relaxed); - let resized = self.analyser.current_fft_size() != fft_size; - let complete_cycle = self.analyser.check_complete_cycle(fft_size); - if resized || complete_cycle { - let smoothing_time_constant = - self.smoothing_time_constant.load(Ordering::Relaxed) as f32 / 100.; - self.analyser - .calculate_float_frequency(fft_size, smoothing_time_constant); - } - // check if any information was requested from the control thread - if let Ok(request) = self.receiver.try_recv() { - match request { - AnalyserRequest::FloatTime { - send_done_signal: sender, - buffer, - } => { - self.analyser.get_float_time(buffer, fft_size); - - // allow to fail when receiver is disconnected - let _ = sender.send(()); - } - AnalyserRequest::FloatFrequency { - send_done_signal: sender, - buffer, - } => { - self.analyser.get_float_frequency(buffer); - - // allow to fail when receiver is disconnected - let _ = sender.send(()); - } - } - } + // add current input to ring buffer + let data = mono.channel_data(0).as_ref(); + self.ring_buffer.write(data); + // @todo - review false } } From 594b5cc1d818d5fd53d83ce37fa1c897fbafd61e Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 12:38:34 +0100 Subject: [PATCH 02/18] fix: remove double smoothing --- src/analysis.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 0bb21c58..8399fd9e 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -370,15 +370,6 @@ impl Analyser { *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * norm; }); - - // Smooth over time the frequency domain data. - last_fft_output - .iter_mut() - .zip(output.iter().skip(1)) // skip first bin, i.e. DC Offset - .for_each(|(p, c)| { - let norm = c.norm() / fft_size as f32; - *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * norm; - }); } pub fn get_float_frequency_data(&mut self, dst: &mut [f32]) { @@ -734,10 +725,12 @@ mod tests { let fft_size = 1024; let freq_resolution = 43.066; - for num_bin in 1..5 { - // frequency centered on `num_bin` bin, we should have highest value - // in `num_bin` bin - let freq = freq_resolution * (num_bin as f32 + 0.5); + // note: we don't check all the bin range to keep low tests time + for num_bin in 1..(fft_size / 8) { + // create sines whose frequency centered on `num_bin` bin, we should + // the have highest value in `num_bin` bin + // @note (tbc): bin 0 seems to represent freq_resolution / 2 + let freq = freq_resolution * num_bin as f32; let mut analyser = Analyser::new(); analyser.set_fft_size(fft_size); @@ -756,10 +749,10 @@ mod tests { let mut bins = vec![0.; analyser.frequency_bin_count()]; analyser.get_float_frequency_data(&mut bins[..]); - let highest = bins[num_bin as usize]; + let highest = bins[num_bin]; bins.iter().enumerate().for_each(|(index, db)| { - if index != num_bin as usize { + if index != num_bin { assert!(db < &highest); } }); From b4d48bc9bf56164bbeb85eca8471f7b5ababfcc2 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 13:14:25 +0100 Subject: [PATCH 03/18] fix: back to thread safety - mic_playback example is fixed --- src/analysis.rs | 76 +++++++++++++++++++++++++------------------- src/node/analyser.rs | 34 ++++++++++---------- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 8399fd9e..49579027 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -3,12 +3,12 @@ //! These are used in the [`AnalyserNode`](crate::node::AnalyserNode) use std::f32::consts::PI; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use realfft::{num_complex::Complex, RealFftPlanner}; -use crate::RENDER_QUANTUM_SIZE; +use crate::{RENDER_QUANTUM_SIZE, AtomicF64}; // @todo - modify AtomicF32 in `lib.rs` to expose Ordering @@ -179,12 +179,14 @@ impl AnalyserRingBuffer { pub(crate) struct Analyser { // ring buffer informations ring_buffer: Arc, - fft_size: usize, - smoothing_time_constant: f64, - min_decibels: f64, - max_decibels: f64, - - fft_planner: RealFftPlanner, + fft_size: AtomicUsize, + smoothing_time_constant: AtomicF64, + min_decibels: AtomicF64, + max_decibels: AtomicF64, + + // fft planner is not thread safe, but the Mutex is ok here as we are not + // in audio thread + fft_planner: Arc>>, fft_input: Vec, fft_scratch: Vec>, fft_output: Vec>, @@ -211,11 +213,11 @@ impl Analyser { Self { ring_buffer, - fft_size: DEFAULT_FFT_SIZE, - smoothing_time_constant: DEFAULT_SMOOTHING_TIME_CONSTANT, - min_decibels: DEFAULT_MIN_DECIBELS, - max_decibels: DEFAULT_MAX_DECIBELS, - fft_planner, + fft_size: AtomicUsize::new(DEFAULT_FFT_SIZE), + smoothing_time_constant: AtomicF64::new(DEFAULT_SMOOTHING_TIME_CONSTANT), + min_decibels: AtomicF64::new(DEFAULT_MIN_DECIBELS), + max_decibels: AtomicF64::new(DEFAULT_MAX_DECIBELS), + fft_planner: Arc::new(Mutex::new(fft_planner)), fft_input, fft_scratch, fft_output, @@ -229,13 +231,13 @@ impl Analyser { } pub fn fft_size(&self) -> usize { - self.fft_size + self.fft_size.load(Ordering::SeqCst) } pub fn set_fft_size(&mut self, fft_size: usize) { assert_valid_fft_size(fft_size); - let current_fft_size = self.fft_size; + let current_fft_size = self.fft_size.swap(fft_size, Ordering::SeqCst); if current_fft_size != fft_size { // reset last fft buffer @@ -243,40 +245,38 @@ impl Analyser { // generate blackman window self.blackman.clear(); generate_blackman(fft_size).for_each(|v| self.blackman.push(v)); - - self.fft_size = fft_size; } } pub fn smoothing_time_constant(&self) -> f64 { - self.smoothing_time_constant + self.smoothing_time_constant.load() } pub fn set_smoothing_time_constant(&mut self, value: f64) { assert_valid_smoothing_time_constant(value); - self.smoothing_time_constant = value; + self.smoothing_time_constant.store(value); } pub fn min_decibels(&self) -> f64 { - self.min_decibels + self.min_decibels.load() } pub fn set_min_decibels(&mut self, value: f64) { - assert_valid_min_decibels(value, self.max_decibels); - self.min_decibels = value; + assert_valid_min_decibels(value, self.max_decibels()); + self.min_decibels.store(value); } pub fn max_decibels(&self) -> f64 { - self.max_decibels + self.max_decibels.load() } pub fn set_max_decibels(&mut self, value: f64) { - assert_valid_max_decibels(value, self.min_decibels); - self.max_decibels = value; + assert_valid_max_decibels(value, self.min_decibels()); + self.max_decibels.store(value); } pub fn frequency_bin_count(&self) -> usize { - self.fft_size / 2 + self.fft_size.load(Ordering::SeqCst) / 2 } // @note: `add_input`, `get_float_time_domain_data`, `get_byte_time_domain_data` @@ -288,14 +288,14 @@ impl Analyser { // the excess elements will be ignored. The most recent fftSize frames are // written (after downmixing) pub fn get_float_time_domain_data(&self, dst: &mut [f32]) { - let fft_size = self.fft_size; + let fft_size = self.fft_size(); self.ring_buffer.read(dst, fft_size); } // we need to duplicate the `get_float_time_domain_data` to avoid creating // an intermediate vector of floats pub fn get_byte_time_domain_data(&self, dst: &mut [u8]) { - let fft_size = self.fft_size; + let fft_size = self.fft_size(); let mut tmp = vec![0.; dst.len()]; self.ring_buffer.read(&mut tmp, fft_size); @@ -305,9 +305,9 @@ impl Analyser { } fn compute_fft(&mut self, fft_size: usize) { - let smoothing_time_constant = self.smoothing_time_constant as f32; + let smoothing_time_constant = self.smoothing_time_constant() as f32; // setup FFT planner and properly sized buffers - let r2c = self.fft_planner.plan_fft_forward(fft_size); + let r2c = self.fft_planner.lock().unwrap().plan_fft_forward(fft_size); let input = &mut self.fft_input[..fft_size]; let output = &mut self.fft_output[..fft_size / 2 + 1]; let scratch = &mut self.fft_scratch[..r2c.get_scratch_len()]; @@ -373,7 +373,7 @@ impl Analyser { } pub fn get_float_frequency_data(&mut self, dst: &mut [f32]) { - let fft_size = self.fft_size; + let fft_size = self.fft_size(); let frequency_bin_count = self.frequency_bin_count(); self.compute_fft(fft_size); @@ -391,7 +391,7 @@ impl Analyser { } pub fn get_byte_frequency_data(&mut self, dst: &mut [u8]) { - let fft_size = self.fft_size; + let fft_size = self.fft_size(); let frequency_bin_count = self.frequency_bin_count(); let min_decibels = self.min_decibels() as f32; let max_decibels = self.max_decibels() as f32; @@ -421,6 +421,7 @@ impl Analyser { #[cfg(test)] mod tests { use std::thread; + use std::sync::{RwLock}; use float_eq::{assert_float_eq, float_eq}; use rand::Rng; @@ -804,7 +805,7 @@ mod tests { // SEGFAULT traps or something, but this is difficult to really test something // in an accurante way, other tests are there for such thing #[test] - fn test_concurrency() { + fn test_ring_buffer_concurrency() { let analyser = Arc::new(Analyser::new()); let ring_buffer = analyser.get_ring_buffer_clone(); @@ -847,4 +848,13 @@ mod tests { } } + #[test] + fn test_thread_safety() { + let analyser = Arc::new(RwLock::new(Analyser::new())); + + thread::spawn(move || { + analyser.write().unwrap().set_fft_size(MIN_FFT_SIZE); + assert_eq!(analyser.write().unwrap().fft_size(), MIN_FFT_SIZE); + }); + } } diff --git a/src/node/analyser.rs b/src/node/analyser.rs index d401b6c8..be42b7ae 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -1,5 +1,5 @@ -use std::cell::RefCell; -use std::sync::{Arc}; +// use std::cell::RefCell; +use std::sync::{Arc, RwLock}; use crate::analysis::{Analyser, AnalyserRingBuffer}; use crate::context::{AudioContextRegistration, BaseAudioContext}; @@ -42,7 +42,7 @@ pub struct AnalyserNode { registration: AudioContextRegistration, channel_config: ChannelConfig, // needed to make the AnalyserNode API immutable - analyser: RefCell, + analyser: Arc>, } impl AudioNode for AnalyserNode { @@ -82,7 +82,7 @@ impl AnalyserNode { let node = AnalyserNode { registration, channel_config: options.channel_config.into(), - analyser: RefCell::new(analyser), + analyser: Arc::new(RwLock::new(analyser)), }; (node, Box::new(render)) @@ -91,7 +91,7 @@ impl AnalyserNode { /// The size of the FFT used for frequency-domain analysis (in sample-frames) pub fn fft_size(&self) -> usize { - self.analyser.borrow().fft_size() + self.analyser.read().unwrap().fft_size() } /// Set FFT size @@ -100,14 +100,14 @@ impl AnalyserNode { /// /// This function panics if fft_size is not a power of two or not in the range [32, 32768] pub fn set_fft_size(&self, fft_size: usize) { - self.analyser.borrow_mut().set_fft_size(fft_size); + self.analyser.write().unwrap().set_fft_size(fft_size); } /// Time averaging parameter with the last analysis frame. /// A value from 0 -> 1 where 0 represents no time averaging with the last /// analysis frame. The default value is 0.8. pub fn smoothing_time_constant(&self) -> f64 { - self.analyser.borrow().smoothing_time_constant() + self.analyser.read().unwrap().smoothing_time_constant() } /// Set smoothing time constant @@ -116,14 +116,14 @@ impl AnalyserNode { /// /// This function panics if the value is set to a value less than 0 or more than 1. pub fn set_smoothing_time_constant(&self, value: f64) { - self.analyser.borrow_mut().set_smoothing_time_constant(value); + self.analyser.write().unwrap().set_smoothing_time_constant(value); } /// Minimum power value in the scaling range for the FFT analysis data for /// conversion to unsigned byte values. The default value is -100. pub fn min_decibels(&self) -> f64 { - self.analyser.borrow().min_decibels() + self.analyser.read().unwrap().min_decibels() } /// Set min decibels @@ -133,13 +133,13 @@ impl AnalyserNode { /// This function panics if the value is set to a value more than or equal /// to max decibels. pub fn set_min_decibels(&self, value: f64) { - self.analyser.borrow_mut().set_min_decibels(value); + self.analyser.write().unwrap().set_min_decibels(value); } /// Maximum power value in the scaling range for the FFT analysis data for /// conversion to unsigned byte values. The default value is -30. pub fn max_decibels(&self) -> f64 { - self.analyser.borrow().max_decibels() + self.analyser.read().unwrap().max_decibels() } /// Set max decibels @@ -149,30 +149,30 @@ impl AnalyserNode { /// This function panics if the value is set to a value less than or equal /// to min decibels. pub fn set_max_decibels(&self, value: f64) { - self.analyser.borrow_mut().set_max_decibels(value); + self.analyser.write().unwrap().set_max_decibels(value); } /// Number of bins in the FFT results, is half the FFT size pub fn frequency_bin_count(&self) -> usize { - self.analyser.borrow().frequency_bin_count() + self.analyser.read().unwrap().frequency_bin_count() } /// Copies the current time domain data (waveform data) into the provided buffer pub fn get_float_time_domain_data(&self, buffer: &mut [f32]) { - self.analyser.borrow_mut().get_float_time_domain_data(buffer); + self.analyser.write().unwrap().get_float_time_domain_data(buffer); } pub fn get_byte_time_domain_data(&self, buffer: &mut [u8]) { - self.analyser.borrow_mut().get_byte_time_domain_data(buffer); + self.analyser.write().unwrap().get_byte_time_domain_data(buffer); } /// Copies the current frequency data into the provided buffer pub fn get_float_frequency_data(&self, buffer: &mut [f32]) { - self.analyser.borrow_mut().get_float_frequency_data(buffer); + self.analyser.write().unwrap().get_float_frequency_data(buffer); } pub fn get_byte_frequency_data(&self, buffer: &mut [u8]) { - self.analyser.borrow_mut().get_byte_frequency_data(buffer); + self.analyser.write().unwrap().get_byte_frequency_data(buffer); } } From 13aeecdd2174a21c903521e4bfb3f414ac1c5a15 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 14:09:42 +0100 Subject: [PATCH 04/18] feat: handle all options --- src/analysis.rs | 133 +++++++++++++++++++++---------------------- src/node/analyser.rs | 59 ++++++++++++------- 2 files changed, 105 insertions(+), 87 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 49579027..9b738c3a 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -3,13 +3,12 @@ //! These are used in the [`AnalyserNode`](crate::node::AnalyserNode) use std::f32::consts::PI; -use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; use realfft::{num_complex::Complex, RealFftPlanner}; -use crate::{RENDER_QUANTUM_SIZE, AtomicF64}; - +use crate::{RENDER_QUANTUM_SIZE}; // @todo - modify AtomicF32 in `lib.rs` to expose Ordering #[derive(Debug)] @@ -29,7 +28,8 @@ impl AtomicF32 { } pub fn store(&self, v: f32, ordering: Ordering) { - self.inner.store(u32::from_ne_bytes(v.to_ne_bytes()), ordering); + self.inner + .store(u32::from_ne_bytes(v.to_ne_bytes()), ordering); } } @@ -46,19 +46,13 @@ pub fn generate_blackman(size: usize) -> impl Iterator { }) } - -/// FFT size is max 32768 samples, mandated in spec -// const MAX_SAMPLES: usize = 32768; -/// Max FFT size corresponds to 256 render quanta -// const MAX_QUANTA: usize = MAX_SAMPLES / RENDER_QUANTUM_SIZE; - -const DEFAULT_SMOOTHING_TIME_CONSTANT: f64 = 0.8; -const DEFAULT_MIN_DECIBELS: f64 = -100.; -const DEFAULT_MAX_DECIBELS: f64 = -30.; +pub(crate) const DEFAULT_SMOOTHING_TIME_CONSTANT: f64 = 0.8; +pub(crate) const DEFAULT_MIN_DECIBELS: f64 = -100.; +pub(crate) const DEFAULT_MAX_DECIBELS: f64 = -30.; +pub(crate) const DEFAULT_FFT_SIZE: usize = 2048; const MIN_FFT_SIZE: usize = 32; const MAX_FFT_SIZE: usize = 32768; -const DEFAULT_FFT_SIZE: usize = 2048; // [spec] This MUST be a power of two in the range 32 to 32768, otherwise an // IndexSizeError exception MUST be thrown. @@ -111,7 +105,6 @@ fn assert_valid_max_decibels(max_decibels: f64, min_decibels: f64) { } } - // as the queue is composed of AtomicF32 having only 1 render quantum of room should be enough const RING_BUFFER_SIZE: usize = MAX_FFT_SIZE + RENDER_QUANTUM_SIZE; @@ -155,11 +148,14 @@ impl AnalyserRingBuffer { // let fft_size = self.fft_size.load(Ordering::SeqCst); let len = dst.len().min(max_len); - dst.iter_mut().take(len).enumerate().for_each(|(index, value)| { - // offset calculation by RING_BUFFER_SIZE so we cant negative values - let position = (RING_BUFFER_SIZE + write_index - len + index) % RING_BUFFER_SIZE; - *value = self.buffer[position].load(Ordering::Relaxed); - }); + dst.iter_mut() + .take(len) + .enumerate() + .for_each(|(index, value)| { + // offset calculation by RING_BUFFER_SIZE so we cant negative values + let position = (RING_BUFFER_SIZE + write_index - len + index) % RING_BUFFER_SIZE; + *value = self.buffer[position].load(Ordering::Relaxed); + }); } // so that we can easily share the tests with the unsafe version @@ -175,24 +171,24 @@ impl AnalyserRingBuffer { } } -// this cannot be made thread safe because RealFftPlanner does not support it +// As the analyser is wrapped into a Arc> by the analyser node to get interior +// mutability and expose an immutable public API, we should be ok with thread safety. pub(crate) struct Analyser { - // ring buffer informations ring_buffer: Arc, - fft_size: AtomicUsize, - smoothing_time_constant: AtomicF64, - min_decibels: AtomicF64, - max_decibels: AtomicF64, - - // fft planner is not thread safe, but the Mutex is ok here as we are not - // in audio thread + fft_size: usize, + smoothing_time_constant: f64, + min_decibels: f64, + max_decibels: f64, + // If not wrapped into Arc>, compiler complains about thread safety: + // `(dyn rustfft::avx::avx_planner::AvxPlannerInternalAPI + 'static)` + // cannot be shared between threads safely + // But the Mutex is ok here as `compute_fft` Analyser lives outside the audio thread. fft_planner: Arc>>, fft_input: Vec, fft_scratch: Vec>, fft_output: Vec>, last_fft_output: Vec, blackman: Vec, - } impl Analyser { @@ -213,10 +209,10 @@ impl Analyser { Self { ring_buffer, - fft_size: AtomicUsize::new(DEFAULT_FFT_SIZE), - smoothing_time_constant: AtomicF64::new(DEFAULT_SMOOTHING_TIME_CONSTANT), - min_decibels: AtomicF64::new(DEFAULT_MIN_DECIBELS), - max_decibels: AtomicF64::new(DEFAULT_MAX_DECIBELS), + fft_size: DEFAULT_FFT_SIZE, + smoothing_time_constant: DEFAULT_SMOOTHING_TIME_CONSTANT, + min_decibels: DEFAULT_MIN_DECIBELS, + max_decibels: DEFAULT_MAX_DECIBELS, fft_planner: Arc::new(Mutex::new(fft_planner)), fft_input, fft_scratch, @@ -231,13 +227,13 @@ impl Analyser { } pub fn fft_size(&self) -> usize { - self.fft_size.load(Ordering::SeqCst) + self.fft_size } pub fn set_fft_size(&mut self, fft_size: usize) { assert_valid_fft_size(fft_size); - let current_fft_size = self.fft_size.swap(fft_size, Ordering::SeqCst); + let current_fft_size = self.fft_size; if current_fft_size != fft_size { // reset last fft buffer @@ -245,38 +241,40 @@ impl Analyser { // generate blackman window self.blackman.clear(); generate_blackman(fft_size).for_each(|v| self.blackman.push(v)); + + self.fft_size = fft_size; } } pub fn smoothing_time_constant(&self) -> f64 { - self.smoothing_time_constant.load() + self.smoothing_time_constant } pub fn set_smoothing_time_constant(&mut self, value: f64) { assert_valid_smoothing_time_constant(value); - self.smoothing_time_constant.store(value); + self.smoothing_time_constant = value; } pub fn min_decibels(&self) -> f64 { - self.min_decibels.load() + self.min_decibels } pub fn set_min_decibels(&mut self, value: f64) { assert_valid_min_decibels(value, self.max_decibels()); - self.min_decibels.store(value); + self.min_decibels = value; } pub fn max_decibels(&self) -> f64 { - self.max_decibels.load() + self.max_decibels } pub fn set_max_decibels(&mut self, value: f64) { assert_valid_max_decibels(value, self.min_decibels()); - self.max_decibels.store(value); + self.max_decibels = value; } pub fn frequency_bin_count(&self) -> usize { - self.fft_size.load(Ordering::SeqCst) / 2 + self.fft_size() / 2 } // @note: `add_input`, `get_float_time_domain_data`, `get_byte_time_domain_data` @@ -368,7 +366,6 @@ impl Analyser { .for_each(|(p, c)| { let norm = c.norm() * normalize_factor; *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * norm; - }); } @@ -417,11 +414,10 @@ impl Analyser { } } - #[cfg(test)] mod tests { + use std::sync::RwLock; use std::thread; - use std::sync::{RwLock}; use float_eq::{assert_float_eq, float_eq}; use rand::Rng; @@ -453,7 +449,6 @@ mod tests { assert_eq!(max_pos, 1024); } - #[test] fn test_ring_buffer_write_simple() { let ring_buffer = AnalyserRingBuffer::new(); @@ -468,7 +463,8 @@ mod tests { // check write index is properly updated let write_index = ring_buffer.write_index.load(Ordering::SeqCst); - let expected = (j * RENDER_QUANTUM_SIZE + RENDER_QUANTUM_SIZE) % RING_BUFFER_SIZE; + let expected = + (j * RENDER_QUANTUM_SIZE + RENDER_QUANTUM_SIZE) % RING_BUFFER_SIZE; assert_eq!(write_index, expected); } @@ -488,7 +484,9 @@ mod tests { let ring_buffer = AnalyserRingBuffer::new(); let offset = 10; - ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); + ring_buffer + .write_index + .store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); let data = [1.; RENDER_QUANTUM_SIZE]; ring_buffer.write(&data); @@ -510,7 +508,9 @@ mod tests { { let ring_buffer = AnalyserRingBuffer::new(); let offset = 2; - ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); + ring_buffer + .write_index + .store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); let data = [1., 2., 3., 4.]; ring_buffer.write(&data); @@ -543,7 +543,6 @@ mod tests { let expected = [1.; RENDER_QUANTUM_SIZE]; assert_float_eq!(&expected, &read_buffer, abs_all <= 1e-12); - // second pass let data = [2.; RENDER_QUANTUM_SIZE]; ring_buffer.write(&data); @@ -559,13 +558,16 @@ mod tests { assert_float_eq!(&expected, &read_buffer, abs_all <= 1e-12); let mut full_buffer_expected = [0.; RING_BUFFER_SIZE]; - full_buffer_expected[0..RENDER_QUANTUM_SIZE] - .copy_from_slice(&[1.; RENDER_QUANTUM_SIZE]); + full_buffer_expected[0..RENDER_QUANTUM_SIZE].copy_from_slice(&[1.; RENDER_QUANTUM_SIZE]); full_buffer_expected[RENDER_QUANTUM_SIZE..(RENDER_QUANTUM_SIZE * 2)] .copy_from_slice(&[2.; RENDER_QUANTUM_SIZE]); - assert_float_eq!(&ring_buffer.raw()[..], &full_buffer_expected[..], abs_all <= 1e-12); + assert_float_eq!( + &ring_buffer.raw()[..], + &full_buffer_expected[..], + abs_all <= 1e-12 + ); } #[test] @@ -575,7 +577,9 @@ mod tests { let ring_buffer = AnalyserRingBuffer::new(); let offset = 10; - ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); + ring_buffer + .write_index + .store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); let data = [1.; RENDER_QUANTUM_SIZE]; ring_buffer.write(&data); @@ -590,7 +594,9 @@ mod tests { { let ring_buffer = AnalyserRingBuffer::new(); let offset = 2; - ring_buffer.write_index.store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); + ring_buffer + .write_index + .store(RING_BUFFER_SIZE - offset, Ordering::SeqCst); let data = [1., 2., 3., 4.]; ring_buffer.write(&data); @@ -771,8 +777,7 @@ mod tests { // only N / 2 values should contain frequency data, rest is unaltered assert!( - bins[0..(RENDER_QUANTUM_SIZE / 2)] - == [f32::NEG_INFINITY; (RENDER_QUANTUM_SIZE / 2)] + bins[0..(RENDER_QUANTUM_SIZE / 2)] == [f32::NEG_INFINITY; (RENDER_QUANTUM_SIZE / 2)] ); assert_float_eq!( &bins[(RENDER_QUANTUM_SIZE / 2)..], @@ -781,7 +786,7 @@ mod tests { ); } - #[test] + #[test] fn test_get_byte_frequency_data_vs_frequenc_bin_count() { let mut analyser = Analyser::new(); analyser.set_fft_size(RENDER_QUANTUM_SIZE); @@ -791,14 +796,8 @@ mod tests { analyser.get_byte_frequency_data(&mut bins[..]); // only N / 2 values should contain frequency data, rest is unaltered - assert!( - bins[0..(RENDER_QUANTUM_SIZE / 2)] - == [0; (RENDER_QUANTUM_SIZE / 2)] - ); - assert!( - bins[(RENDER_QUANTUM_SIZE / 2)..] - == [255; (RENDER_QUANTUM_SIZE / 2)][..], - ); + assert!(bins[0..(RENDER_QUANTUM_SIZE / 2)] == [0; (RENDER_QUANTUM_SIZE / 2)]); + assert!(bins[(RENDER_QUANTUM_SIZE / 2)..] == [255; (RENDER_QUANTUM_SIZE / 2)][..],); } // this mostly tries to show that it works concurrently and we don't fall into diff --git a/src/node/analyser.rs b/src/node/analyser.rs index be42b7ae..028c3666 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -1,7 +1,10 @@ // use std::cell::RefCell; use std::sync::{Arc, RwLock}; -use crate::analysis::{Analyser, AnalyserRingBuffer}; +use crate::analysis::{ + Analyser, AnalyserRingBuffer, DEFAULT_FFT_SIZE, DEFAULT_MAX_DECIBELS, DEFAULT_MIN_DECIBELS, + DEFAULT_SMOOTHING_TIME_CONSTANT, +}; use crate::context::{AudioContextRegistration, BaseAudioContext}; use crate::render::{AudioParamValues, AudioProcessor, AudioRenderQuantum, RenderScope}; @@ -28,10 +31,10 @@ pub struct AnalyserOptions { impl Default for AnalyserOptions { fn default() -> Self { Self { - fft_size: 2048, - max_decibels: -30., - min_decibels: 100., - smoothing_time_constant: 0.8, + fft_size: DEFAULT_FFT_SIZE, + max_decibels: DEFAULT_MAX_DECIBELS, + min_decibels: DEFAULT_MIN_DECIBELS, + smoothing_time_constant: DEFAULT_SMOOTHING_TIME_CONSTANT, channel_config: ChannelConfigOptions::default(), } } @@ -41,7 +44,7 @@ impl Default for AnalyserOptions { pub struct AnalyserNode { registration: AudioContextRegistration, channel_config: ChannelConfig, - // needed to make the AnalyserNode API immutable + // RwLock is needed to make the AnalyserNode API immutable analyser: Arc>, } @@ -66,14 +69,16 @@ impl AudioNode for AnalyserNode { impl AnalyserNode { pub fn new(context: &C, options: AnalyserOptions) -> Self { context.register(move |registration| { - // let fft_size = Arc::new(AtomicUsize::new(options.fft_size)); - // let smoothing_time_constant = Arc::new(AtomicU32::new( - // (options.smoothing_time_constant * 100.) as u32, - // )); + let fft_size = options.fft_size; + let smoothing_time_constant = options.smoothing_time_constant; + let min_decibels = options.min_decibels; + let max_decibels = options.max_decibels; - let analyser = Analyser::new(); - - // apply options + let mut analyser = Analyser::new(); + analyser.set_fft_size(fft_size); + analyser.set_smoothing_time_constant(smoothing_time_constant); + analyser.set_min_decibels(min_decibels); + analyser.set_max_decibels(max_decibels); let render = AnalyserRenderer { ring_buffer: analyser.get_ring_buffer_clone(), @@ -116,10 +121,12 @@ impl AnalyserNode { /// /// This function panics if the value is set to a value less than 0 or more than 1. pub fn set_smoothing_time_constant(&self, value: f64) { - self.analyser.write().unwrap().set_smoothing_time_constant(value); + self.analyser + .write() + .unwrap() + .set_smoothing_time_constant(value); } - /// Minimum power value in the scaling range for the FFT analysis data for /// conversion to unsigned byte values. The default value is -100. pub fn min_decibels(&self) -> f64 { @@ -157,22 +164,34 @@ impl AnalyserNode { self.analyser.read().unwrap().frequency_bin_count() } - /// Copies the current time domain data (waveform data) into the provided buffer + /// Copies the current time domain data into the provided buffer pub fn get_float_time_domain_data(&self, buffer: &mut [f32]) { - self.analyser.write().unwrap().get_float_time_domain_data(buffer); + self.analyser + .write() + .unwrap() + .get_float_time_domain_data(buffer); } pub fn get_byte_time_domain_data(&self, buffer: &mut [u8]) { - self.analyser.write().unwrap().get_byte_time_domain_data(buffer); + self.analyser + .write() + .unwrap() + .get_byte_time_domain_data(buffer); } /// Copies the current frequency data into the provided buffer pub fn get_float_frequency_data(&self, buffer: &mut [f32]) { - self.analyser.write().unwrap().get_float_frequency_data(buffer); + self.analyser + .write() + .unwrap() + .get_float_frequency_data(buffer); } pub fn get_byte_frequency_data(&self, buffer: &mut [u8]) { - self.analyser.write().unwrap().get_byte_frequency_data(buffer); + self.analyser + .write() + .unwrap() + .get_byte_frequency_data(buffer); } } From 704b97a5e2c56e741c881de2f6f263d74e872252 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 14:18:35 +0100 Subject: [PATCH 05/18] refactor: modify AtomicF32 to expose ordering argument --- src/analysis.rs | 27 ++------------------------- src/lib.rs | 12 +++++------- src/node/dynamics_compressor.rs | 7 ++++--- src/param.rs | 8 ++++---- 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 9b738c3a..b3d8b880 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -3,35 +3,12 @@ //! These are used in the [`AnalyserNode`](crate::node::AnalyserNode) use std::f32::consts::PI; -use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use realfft::{num_complex::Complex, RealFftPlanner}; -use crate::{RENDER_QUANTUM_SIZE}; - -// @todo - modify AtomicF32 in `lib.rs` to expose Ordering -#[derive(Debug)] -struct AtomicF32 { - inner: AtomicU32, -} - -impl AtomicF32 { - pub fn new(v: f32) -> Self { - Self { - inner: AtomicU32::new(u32::from_ne_bytes(v.to_ne_bytes())), - } - } - - pub fn load(&self, ordering: Ordering) -> f32 { - f32::from_ne_bytes(self.inner.load(ordering).to_ne_bytes()) - } - - pub fn store(&self, v: f32, ordering: Ordering) { - self.inner - .store(u32::from_ne_bytes(v.to_ne_bytes()), ordering); - } -} +use crate::{RENDER_QUANTUM_SIZE, AtomicF32}; /// Blackman window values iterator with alpha = 0.16 pub fn generate_blackman(size: usize) -> impl Iterator { diff --git a/src/lib.rs b/src/lib.rs index 30af3047..468bc1ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,13 +80,11 @@ pub use io::{enumerate_devices, MediaDeviceInfo, MediaDeviceInfoKind}; mod analysis; mod message; -/// Atomic float 32, only `load` and `store` are supported, no arithmetics #[derive(Debug)] -pub(crate) struct AtomicF32 { +pub (crate) struct AtomicF32 { inner: AtomicU32, } -// `swap()` is not implemented as `AtomicF32` is only used in `param.rs` for now impl AtomicF32 { pub fn new(v: f32) -> Self { Self { @@ -94,13 +92,13 @@ impl AtomicF32 { } } - pub fn load(&self) -> f32 { - f32::from_ne_bytes(self.inner.load(Ordering::SeqCst).to_ne_bytes()) + pub fn load(&self, ordering: Ordering) -> f32 { + f32::from_ne_bytes(self.inner.load(ordering).to_ne_bytes()) } - pub fn store(&self, v: f32) { + pub fn store(&self, v: f32, ordering: Ordering) { self.inner - .store(u32::from_ne_bytes(v.to_ne_bytes()), Ordering::SeqCst) + .store(u32::from_ne_bytes(v.to_ne_bytes()), ordering); } } diff --git a/src/node/dynamics_compressor.rs b/src/node/dynamics_compressor.rs index 02e851cf..e887bfb3 100644 --- a/src/node/dynamics_compressor.rs +++ b/src/node/dynamics_compressor.rs @@ -1,4 +1,5 @@ -use std::sync::Arc; +use std::sync::atomic::{Ordering}; +use std::sync::{Arc}; use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext}; use crate::param::{AudioParam, AudioParamDescriptor}; @@ -240,7 +241,7 @@ impl DynamicsCompressorNode { } pub fn reduction(&self) -> f32 { - self.reduction.load() + self.reduction.load(Ordering::SeqCst) } } @@ -385,7 +386,7 @@ impl AudioProcessor for DynamicsCompressorRenderer { // update prev_detector_value for next block self.prev_detector_value = prev_detector_value; // update reduction shared w/ main thread - self.reduction.store(reduction_gain); + self.reduction.store(reduction_gain, Ordering::SeqCst); // store input in delay line self.ring_buffer[self.ring_index] = input; diff --git a/src/param.rs b/src/param.rs index 9201e159..3299b3ed 100644 --- a/src/param.rs +++ b/src/param.rs @@ -304,7 +304,7 @@ impl AudioParam { // test_exponential_ramp_a_rate_multiple_blocks // test_exponential_ramp_k_rate_multiple_blocks pub fn value(&self) -> f32 { - self.current_value.load() + self.current_value.load(Ordering::SeqCst) } /// Set the value of the `AudioParam`. @@ -320,8 +320,8 @@ impl AudioParam { // cf. https://www.w3.org/TR/webaudio/#dom-audioparam-value pub fn set_value(&self, value: f32) -> &Self { // current_value should always be clamped - self.current_value - .store(value.clamp(self.min_value, self.max_value)); + let clamped = value.clamp(self.min_value, self.max_value); + self.current_value.store(clamped, Ordering::SeqCst); // this event is meant to update param intrisic value before any calculation // is done, will behave as SetValueAtTime with `time == block_timestamp` @@ -987,7 +987,7 @@ impl AudioParamProcessor { // Set [[current value]] to the value of paramIntrinsicValue at the // beginning of this render quantum. let clamped = self.intrisic_value.clamp(self.min_value, self.max_value); - self.current_value.store(clamped); + self.current_value.store(clamped, Ordering::SeqCst); // clear the buffer for this block self.buffer.clear(); From c715c166561de1342abbe7fb9f8306d9a02b47b7 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 14:24:42 +0100 Subject: [PATCH 06/18] fix: clamp time domain data before converting to u8 --- src/analysis.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/analysis.rs b/src/analysis.rs index b3d8b880..5d41526a 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -275,7 +275,9 @@ impl Analyser { self.ring_buffer.read(&mut tmp, fft_size); dst.iter_mut().zip(tmp.iter()).for_each(|(o, i)| { - *o = (128. * (1. + i)) as u8; + let scaled = (128. * (1. + i)); + let clamped = scaled.max(0).min(255); + *o = clamped as u8; }); } From 764103149bb6c80afab42977d127b00244b54d76 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 14:43:45 +0100 Subject: [PATCH 07/18] feat: do not recompute fft if called in same render quantum --- src/analysis.rs | 39 +++++++++++++++++++++++++++------------ src/node/analyser.rs | 13 +++++++++---- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 5d41526a..60692642 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -165,6 +165,7 @@ pub(crate) struct Analyser { fft_scratch: Vec>, fft_output: Vec>, last_fft_output: Vec, + last_fft_time: f64, blackman: Vec, } @@ -195,6 +196,7 @@ impl Analyser { fft_scratch, fft_output, last_fft_output, + last_fft_time: f64::NEG_INFINITY, blackman, } } @@ -275,13 +277,14 @@ impl Analyser { self.ring_buffer.read(&mut tmp, fft_size); dst.iter_mut().zip(tmp.iter()).for_each(|(o, i)| { - let scaled = (128. * (1. + i)); - let clamped = scaled.max(0).min(255); + let scaled = 128. * (1. + i); + let clamped = scaled.max(0.).min(255.); *o = clamped as u8; }); } - fn compute_fft(&mut self, fft_size: usize) { + fn compute_fft(&mut self) { + let fft_size = self.fft_size(); let smoothing_time_constant = self.smoothing_time_constant() as f32; // setup FFT planner and properly sized buffers let r2c = self.fft_planner.lock().unwrap().plan_fft_forward(fft_size); @@ -348,11 +351,17 @@ impl Analyser { }); } - pub fn get_float_frequency_data(&mut self, dst: &mut [f32]) { - let fft_size = self.fft_size(); + pub fn get_float_frequency_data(&mut self, dst: &mut [f32], current_time: f64) { let frequency_bin_count = self.frequency_bin_count(); - self.compute_fft(fft_size); + // [spec] If another call to getByteFrequencyData() or getFloatFrequencyData() + // occurs within the same render quantum as a previous call, the current + // frequency data is not updated with the same data. Instead, the previously + // computed data is returned. + if current_time != self.last_fft_time { + self.compute_fft(); + self.last_fft_time = current_time; + } // [spec] Write the current frequency data into array. If array’s byte // length is less than frequencyBinCount, the excess elements will be @@ -366,13 +375,19 @@ impl Analyser { .for_each(|(v, b)| *v = 20. * b.log10()); } - pub fn get_byte_frequency_data(&mut self, dst: &mut [u8]) { - let fft_size = self.fft_size(); + pub fn get_byte_frequency_data(&mut self, dst: &mut [u8], current_time: f64) { let frequency_bin_count = self.frequency_bin_count(); let min_decibels = self.min_decibels() as f32; let max_decibels = self.max_decibels() as f32; - self.compute_fft(fft_size); + // [spec] If another call to getByteFrequencyData() or getFloatFrequencyData() + // occurs within the same render quantum as a previous call, the current + // frequency data is not updated with the same data. Instead, the previously + // computed data is returned. + if current_time != self.last_fft_time { + self.compute_fft(); + self.last_fft_time = current_time; + } // [spec] Write the current frequency data into array. If array’s byte // length is less than frequencyBinCount, the excess elements will be @@ -733,7 +748,7 @@ mod tests { ring_buffer.write(&signal); let mut bins = vec![0.; analyser.frequency_bin_count()]; - analyser.get_float_frequency_data(&mut bins[..]); + analyser.get_float_frequency_data(&mut bins[..], 0.); let highest = bins[num_bin]; @@ -752,7 +767,7 @@ mod tests { // get data, should be zero (negative infinity decibel) let mut bins = vec![-1.; RENDER_QUANTUM_SIZE]; - analyser.get_float_frequency_data(&mut bins[..]); + analyser.get_float_frequency_data(&mut bins[..], 0.); // only N / 2 values should contain frequency data, rest is unaltered assert!( @@ -772,7 +787,7 @@ mod tests { // get data, should be zero (negative infinity decibel) let mut bins = vec![255; RENDER_QUANTUM_SIZE]; - analyser.get_byte_frequency_data(&mut bins[..]); + analyser.get_byte_frequency_data(&mut bins[..], 0.); // only N / 2 values should contain frequency data, rest is unaltered assert!(bins[0..(RENDER_QUANTUM_SIZE / 2)] == [0; (RENDER_QUANTUM_SIZE / 2)]); diff --git a/src/node/analyser.rs b/src/node/analyser.rs index 028c3666..c425ca24 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -164,7 +164,7 @@ impl AnalyserNode { self.analyser.read().unwrap().frequency_bin_count() } - /// Copies the current time domain data into the provided buffer + /// Copy the current time domain data as f32 values into the provided buffer pub fn get_float_time_domain_data(&self, buffer: &mut [f32]) { self.analyser .write() @@ -172,6 +172,7 @@ impl AnalyserNode { .get_float_time_domain_data(buffer); } + /// Copy the current time domain data as u8 values into the provided buffer pub fn get_byte_time_domain_data(&self, buffer: &mut [u8]) { self.analyser .write() @@ -179,19 +180,23 @@ impl AnalyserNode { .get_byte_time_domain_data(buffer); } - /// Copies the current frequency data into the provided buffer + /// Copy the current frequency data into the provided buffer pub fn get_float_frequency_data(&self, buffer: &mut [f32]) { + let current_time = self.registration.context().current_time(); self.analyser .write() .unwrap() - .get_float_frequency_data(buffer); + .get_float_frequency_data(buffer, current_time); } + /// Copy the current frequency data scaled between min_decibels and + /// max_decibels into the provided buffer pub fn get_byte_frequency_data(&self, buffer: &mut [u8]) { + let current_time = self.registration.context().current_time(); self.analyser .write() .unwrap() - .get_byte_frequency_data(buffer); + .get_byte_frequency_data(buffer, current_time); } } From fcc3abe5e53f511bae6a2fe8016ba347766f65c9 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 15:18:20 +0100 Subject: [PATCH 08/18] doc: review analyser node doc + simple example + clippy & fmt --- examples/analyser.rs | 22 +++++++++ src/analysis.rs | 10 ++-- src/lib.rs | 2 +- src/node/analyser.rs | 87 +++++++++++++++++++++++++++++++-- src/node/dynamics_compressor.rs | 4 +- 5 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 examples/analyser.rs diff --git a/examples/analyser.rs b/examples/analyser.rs new file mode 100644 index 00000000..f01b2c20 --- /dev/null +++ b/examples/analyser.rs @@ -0,0 +1,22 @@ +use web_audio_api::context::{AudioContext, BaseAudioContext}; +use web_audio_api::node::{AudioNode, AudioScheduledSourceNode}; + +fn main() { + let context = AudioContext::default(); + + let analyser = context.create_analyser(); + analyser.connect(&context.destination()); + + let osc = context.create_oscillator(); + osc.frequency().set_value(200.); + osc.connect(&analyser); + osc.start(); + + let mut bins = vec![0.; analyser.frequency_bin_count()]; + + loop { + analyser.get_float_frequency_data(&mut bins); + println!("{:?}", &bins[0..20]); // print 20 first bins + std::thread::sleep(std::time::Duration::from_millis(1000)); + } +} diff --git a/src/analysis.rs b/src/analysis.rs index 60692642..c8eb1619 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex}; use realfft::{num_complex::Complex, RealFftPlanner}; -use crate::{RENDER_QUANTUM_SIZE, AtomicF32}; +use crate::{AtomicF32, RENDER_QUANTUM_SIZE}; /// Blackman window values iterator with alpha = 0.16 pub fn generate_blackman(size: usize) -> impl Iterator { @@ -41,7 +41,7 @@ fn assert_valid_fft_size(fft_size: usize) { ); } - if fft_size < MIN_FFT_SIZE || fft_size > MAX_FFT_SIZE { + if !(MIN_FFT_SIZE..=MAX_FFT_SIZE).contains(&fft_size) { panic!( "IndexSizeError - Invalid fft size: {:?} is outside range [{:?}, {:?}]", fft_size, MIN_FFT_SIZE, MAX_FFT_SIZE @@ -52,7 +52,7 @@ fn assert_valid_fft_size(fft_size: usize) { // [spec] If the value of this attribute is set to a value less than 0 or more // than 1, an IndexSizeError exception MUST be thrown. fn assert_valid_smoothing_time_constant(smoothing_time_constant: f64) { - if smoothing_time_constant < 0. || smoothing_time_constant > 1. { + if !(0. ..=1.).contains(&smoothing_time_constant) { panic!( "IndexSizeError - Invalid smoothing time constant: {:?} is outside range [0, 1]", smoothing_time_constant @@ -278,7 +278,7 @@ impl Analyser { dst.iter_mut().zip(tmp.iter()).for_each(|(o, i)| { let scaled = 128. * (1. + i); - let clamped = scaled.max(0.).min(255.); + let clamped = scaled.clamp(0., 255.); *o = clamped as u8; }); } @@ -402,7 +402,7 @@ impl Analyser { let db = 20. * b.log10(); // 𝑏[π‘˜]=⌊255 / dBπ‘šπ‘Žπ‘₯βˆ’dBπ‘šπ‘–π‘› * (π‘Œ[π‘˜]βˆ’dBπ‘šπ‘–π‘›)βŒ‹ let scaled = 255. / (max_decibels - min_decibels) * (db - min_decibels); - let clamped = scaled.max(0.).min(255.); + let clamped = scaled.clamp(0., 255.); *v = clamped as u8; }); } diff --git a/src/lib.rs b/src/lib.rs index 468bc1ce..c1ef666c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,7 @@ mod analysis; mod message; #[derive(Debug)] -pub (crate) struct AtomicF32 { +pub(crate) struct AtomicF32 { inner: AtomicU32, } diff --git a/src/node/analyser.rs b/src/node/analyser.rs index c425ca24..f90357b4 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -40,7 +40,48 @@ impl Default for AnalyserOptions { } } -/// Provides real-time frequency and time-domain analysis information +/// `AnalyserNode` represents a node able to provide real-time frequency and +/// time-domain analysis information. +/// +/// It is an AudioNode that passes the audio stream unchanged from the input to +/// the output, but allows you to take the generated data, process it, and create +/// audio visualizations.. +/// +/// - MDN documentation: +/// - specification: +/// - see also: [`BaseAudioContext::create_analyser`](crate::context::BaseAudioContext::create_analyser) +/// +/// # Usage +/// +/// ```no_run +/// use web_audio_api::context::{BaseAudioContext, AudioContext}; +/// use web_audio_api::node::{AudioNode, AudioScheduledSourceNode}; +/// +/// let context = AudioContext::default(); +/// +/// let analyser = context.create_analyser(); +/// analyser.connect(&context.destination()); +/// +/// let osc = context.create_oscillator(); +/// osc.frequency().set_value(200.); +/// osc.connect(&analyser); +/// osc.start(); +/// +/// let mut bins = vec![0.; analyser.frequency_bin_count()]; +/// +/// +/// loop { +/// analyser.get_float_frequency_data(&mut bins); +/// println!("{:?}", &bins[0..20]); // print 20 first bins +/// std::thread::sleep(std::time::Duration::from_millis(1000)); +/// } +/// ``` +/// +/// # Examples +/// +/// - `cargo run --release --example analyser` +/// - `cargo run --release --example mic_playback` +/// pub struct AnalyserNode { registration: AudioContextRegistration, channel_config: ChannelConfig, @@ -95,13 +136,17 @@ impl AnalyserNode { } /// The size of the FFT used for frequency-domain analysis (in sample-frames) + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn fft_size(&self) -> usize { self.analyser.read().unwrap().fft_size() } /// Set FFT size /// - /// ## Panics + /// # Panics /// /// This function panics if fft_size is not a power of two or not in the range [32, 32768] pub fn set_fft_size(&self, fft_size: usize) { @@ -111,13 +156,17 @@ impl AnalyserNode { /// Time averaging parameter with the last analysis frame. /// A value from 0 -> 1 where 0 represents no time averaging with the last /// analysis frame. The default value is 0.8. + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn smoothing_time_constant(&self) -> f64 { self.analyser.read().unwrap().smoothing_time_constant() } /// Set smoothing time constant /// - /// ## Panics + /// # Panics /// /// This function panics if the value is set to a value less than 0 or more than 1. pub fn set_smoothing_time_constant(&self, value: f64) { @@ -129,13 +178,17 @@ impl AnalyserNode { /// Minimum power value in the scaling range for the FFT analysis data for /// conversion to unsigned byte values. The default value is -100. + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn min_decibels(&self) -> f64 { self.analyser.read().unwrap().min_decibels() } /// Set min decibels /// - /// ## Panics + /// # Panics /// /// This function panics if the value is set to a value more than or equal /// to max decibels. @@ -145,13 +198,17 @@ impl AnalyserNode { /// Maximum power value in the scaling range for the FFT analysis data for /// conversion to unsigned byte values. The default value is -30. + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn max_decibels(&self) -> f64 { self.analyser.read().unwrap().max_decibels() } /// Set max decibels /// - /// ## Panics + /// # Panics /// /// This function panics if the value is set to a value less than or equal /// to min decibels. @@ -160,11 +217,19 @@ impl AnalyserNode { } /// Number of bins in the FFT results, is half the FFT size + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn frequency_bin_count(&self) -> usize { self.analyser.read().unwrap().frequency_bin_count() } /// Copy the current time domain data as f32 values into the provided buffer + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn get_float_time_domain_data(&self, buffer: &mut [f32]) { self.analyser .write() @@ -173,6 +238,10 @@ impl AnalyserNode { } /// Copy the current time domain data as u8 values into the provided buffer + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn get_byte_time_domain_data(&self, buffer: &mut [u8]) { self.analyser .write() @@ -181,6 +250,10 @@ impl AnalyserNode { } /// Copy the current frequency data into the provided buffer + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn get_float_frequency_data(&self, buffer: &mut [f32]) { let current_time = self.registration.context().current_time(); self.analyser @@ -191,6 +264,10 @@ impl AnalyserNode { /// Copy the current frequency data scaled between min_decibels and /// max_decibels into the provided buffer + /// + /// # Panics + /// + /// This method may panic if the lock to the inner analyser is poisoned pub fn get_byte_frequency_data(&self, buffer: &mut [u8]) { let current_time = self.registration.context().current_time(); self.analyser diff --git a/src/node/dynamics_compressor.rs b/src/node/dynamics_compressor.rs index e887bfb3..2e7bfce3 100644 --- a/src/node/dynamics_compressor.rs +++ b/src/node/dynamics_compressor.rs @@ -1,5 +1,5 @@ -use std::sync::atomic::{Ordering}; -use std::sync::{Arc}; +use std::sync::atomic::Ordering; +use std::sync::Arc; use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext}; use crate::param::{AudioParam, AudioParamDescriptor}; From 8831aeb13527563ff4b7d21bb6cb0f308245e182 Mon Sep 17 00:00:00 2001 From: b-ma Date: Wed, 25 Jan 2023 16:40:36 +0100 Subject: [PATCH 09/18] fix: #236 --- src/analysis.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index c8eb1619..c923ae82 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -345,9 +345,10 @@ impl Analyser { last_fft_output .iter_mut() .zip(output.iter()) - .for_each(|(p, c)| { + .for_each(|(o, c)| { let norm = c.norm() * normalize_factor; - *p = smoothing_time_constant * *p + (1. - smoothing_time_constant) * norm; + let value = smoothing_time_constant * *o + (1. - smoothing_time_constant) * norm; + *o = if value.is_finite() { value } else { 0. }; }); } From 9fd68aa412ec26adcef723797e0cbb6071efb50e Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 26 Jan 2023 08:57:15 +0100 Subject: [PATCH 10/18] refactor: clean comments --- src/analysis.rs | 15 ++++++--------- src/node/analyser.rs | 3 --- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index c923ae82..75a14f51 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -82,10 +82,10 @@ fn assert_valid_max_decibels(max_decibels: f64, min_decibels: f64) { } } -// as the queue is composed of AtomicF32 having only 1 render quantum of room should be enough +// as the queue is composed of AtomicF32 having only 1 render quantum of extra room should be enough const RING_BUFFER_SIZE: usize = MAX_FFT_SIZE + RENDER_QUANTUM_SIZE; -// single producer / single consumer ring buffer +// single producer / multiple consumer ring buffer pub(crate) struct AnalyserRingBuffer { buffer: Arc>, write_index: AtomicUsize, @@ -135,7 +135,7 @@ impl AnalyserRingBuffer { }); } - // so that we can easily share the tests with the unsafe version + // to simply share tests with the unsafe version #[cfg(test)] fn raw(&self) -> Vec { let mut slice = vec![0.; RING_BUFFER_SIZE]; @@ -157,9 +157,9 @@ pub(crate) struct Analyser { min_decibels: f64, max_decibels: f64, // If not wrapped into Arc>, compiler complains about thread safety: - // `(dyn rustfft::avx::avx_planner::AvxPlannerInternalAPI + 'static)` - // cannot be shared between threads safely - // But the Mutex is ok here as `compute_fft` Analyser lives outside the audio thread. + // > `(dyn rustfft::avx::avx_planner::AvxPlannerInternalAPI + 'static)` + // > cannot be shared between threads safely + // But the Mutex is ok here as `Analyser` lives outside the audio thread. fft_planner: Arc>>, fft_input: Vec, fft_scratch: Vec>, @@ -256,9 +256,6 @@ impl Analyser { self.fft_size() / 2 } - // @note: `add_input`, `get_float_time_domain_data`, `get_byte_time_domain_data` - // are the methods that should be adapted to review the buffering strategy - // [spec] Write the current time-domain data (waveform data) into array. // If array has fewer elements than the value of fftSize, the excess elements // will be dropped. If array has more elements than the value of fftSize, diff --git a/src/node/analyser.rs b/src/node/analyser.rs index f90357b4..b5014fc8 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -1,4 +1,3 @@ -// use std::cell::RefCell; use std::sync::{Arc, RwLock}; use crate::analysis::{ @@ -10,8 +9,6 @@ use crate::render::{AudioParamValues, AudioProcessor, AudioRenderQuantum, Render use super::{AudioNode, ChannelConfig, ChannelConfigOptions, ChannelInterpretation}; -// use crossbeam_channel::{self, Receiver, Sender}; - /// Options for constructing an [`AnalyserNode`] // dictionary AnalyserOptions : AudioNodeOptions { // unsigned long fftSize = 2048; From 7030ebb0da087d835ad5047965a8a7f46ea11183 Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 26 Jan 2023 12:12:22 +0100 Subject: [PATCH 11/18] fix: remove unnecessary Arc --- src/analysis.rs | 16 +++++++++------- src/node/analyser.rs | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 75a14f51..3e01b794 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -86,9 +86,10 @@ fn assert_valid_max_decibels(max_decibels: f64, min_decibels: f64) { const RING_BUFFER_SIZE: usize = MAX_FFT_SIZE + RENDER_QUANTUM_SIZE; // single producer / multiple consumer ring buffer +#[derive(Clone)] pub(crate) struct AnalyserRingBuffer { buffer: Arc>, - write_index: AtomicUsize, + write_index: Arc, } impl AnalyserRingBuffer { @@ -97,8 +98,8 @@ impl AnalyserRingBuffer { buffer.resize_with(RING_BUFFER_SIZE, || AtomicF32::new(0.)); Self { - buffer: Arc::new(buffer), - write_index: AtomicUsize::new(0), + buffer: Arc::from(buffer), + write_index: Arc::new(AtomicUsize::new(0)), } } @@ -151,7 +152,7 @@ impl AnalyserRingBuffer { // As the analyser is wrapped into a Arc> by the analyser node to get interior // mutability and expose an immutable public API, we should be ok with thread safety. pub(crate) struct Analyser { - ring_buffer: Arc, + ring_buffer: AnalyserRingBuffer, fft_size: usize, smoothing_time_constant: f64, min_decibels: f64, @@ -171,7 +172,7 @@ pub(crate) struct Analyser { impl Analyser { pub fn new() -> Self { - let ring_buffer = Arc::new(AnalyserRingBuffer::new()); + let ring_buffer = AnalyserRingBuffer::new(); // FFT utils let mut fft_planner = RealFftPlanner::::new(); let max_fft = fft_planner.plan_fft_forward(MAX_FFT_SIZE); @@ -179,7 +180,8 @@ impl Analyser { let fft_input = max_fft.make_input_vec(); let fft_scratch = max_fft.make_scratch_vec(); let fft_output = max_fft.make_output_vec(); - let last_fft_output = vec![0.; fft_output.len()]; + let mut last_fft_output = Vec::with_capacity(fft_output.len()); + last_fft_output.resize_with(fft_output.len(), || 0.); // precalculate Blackman window values, reserve enough space for all input sizes let mut blackman = Vec::with_capacity(fft_input.len()); @@ -201,7 +203,7 @@ impl Analyser { } } - pub fn get_ring_buffer_clone(&self) -> Arc { + pub fn get_ring_buffer_clone(&self) -> AnalyserRingBuffer { self.ring_buffer.clone() } diff --git a/src/node/analyser.rs b/src/node/analyser.rs index b5014fc8..96ef51fc 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, RwLock}; +use std::sync::RwLock; use crate::analysis::{ Analyser, AnalyserRingBuffer, DEFAULT_FFT_SIZE, DEFAULT_MAX_DECIBELS, DEFAULT_MIN_DECIBELS, @@ -83,7 +83,7 @@ pub struct AnalyserNode { registration: AudioContextRegistration, channel_config: ChannelConfig, // RwLock is needed to make the AnalyserNode API immutable - analyser: Arc>, + analyser: RwLock, } impl AudioNode for AnalyserNode { @@ -125,7 +125,7 @@ impl AnalyserNode { let node = AnalyserNode { registration, channel_config: options.channel_config.into(), - analyser: Arc::new(RwLock::new(analyser)), + analyser: RwLock::new(analyser), }; (node, Box::new(render)) @@ -275,7 +275,7 @@ impl AnalyserNode { } struct AnalyserRenderer { - ring_buffer: Arc, + ring_buffer: AnalyserRingBuffer, } impl AudioProcessor for AnalyserRenderer { From fe3fd98a14a7384169c3901e638c13dec1d27000 Mon Sep 17 00:00:00 2001 From: b-ma Date: Thu, 26 Jan 2023 14:50:45 +0100 Subject: [PATCH 12/18] refactor: remove todo on tail time + make blackman generator private --- src/analysis.rs | 9 ++++----- src/node/analyser.rs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 3e01b794..da6e7015 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -11,7 +11,7 @@ use realfft::{num_complex::Complex, RealFftPlanner}; use crate::{AtomicF32, RENDER_QUANTUM_SIZE}; /// Blackman window values iterator with alpha = 0.16 -pub fn generate_blackman(size: usize) -> impl Iterator { +fn generate_blackman(size: usize) -> impl Iterator { let alpha = 0.16; let a0 = (1. - alpha) / 2.; let a1 = 1. / 2.; @@ -82,7 +82,8 @@ fn assert_valid_max_decibels(max_decibels: f64, min_decibels: f64) { } } -// as the queue is composed of AtomicF32 having only 1 render quantum of extra room should be enough +// as the queue is composed of AtomicF32 having only 1 render quantum of extra +// room should be enough const RING_BUFFER_SIZE: usize = MAX_FFT_SIZE + RENDER_QUANTUM_SIZE; // single producer / multiple consumer ring buffer @@ -268,8 +269,6 @@ impl Analyser { self.ring_buffer.read(dst, fft_size); } - // we need to duplicate the `get_float_time_domain_data` to avoid creating - // an intermediate vector of floats pub fn get_byte_time_domain_data(&self, dst: &mut [u8]) { let fft_size = self.fft_size(); let mut tmp = vec![0.; dst.len()]; @@ -400,7 +399,7 @@ impl Analyser { .zip(self.last_fft_output.iter()) .for_each(|(v, b)| { let db = 20. * b.log10(); - // 𝑏[π‘˜]=⌊255 / dBπ‘šπ‘Žπ‘₯βˆ’dBπ‘šπ‘–π‘› * (π‘Œ[π‘˜]βˆ’dBπ‘šπ‘–π‘›)βŒ‹ + // 𝑏[π‘˜] = ⌊255 / dBπ‘šπ‘Žπ‘₯βˆ’dBπ‘šπ‘–π‘› * (π‘Œ[π‘˜]βˆ’dBπ‘šπ‘–π‘›)βŒ‹ let scaled = 255. / (max_decibels - min_decibels) * (db - min_decibels); let clamped = scaled.clamp(0., 255.); *v = clamped as u8; diff --git a/src/node/analyser.rs b/src/node/analyser.rs index 96ef51fc..2888fc19 100644 --- a/src/node/analyser.rs +++ b/src/node/analyser.rs @@ -301,7 +301,7 @@ impl AudioProcessor for AnalyserRenderer { let data = mono.channel_data(0).as_ref(); self.ring_buffer.write(data); - // @todo - review + // no tail-time false } } From c6921b04b927fb4f9965e1d03f3ab60e3505f306 Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 28 Jan 2023 11:47:57 +0100 Subject: [PATCH 13/18] Attempt to perform some CI benchmarking on the AnalyserNode --- benches/my_benchmark.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/benches/my_benchmark.rs b/benches/my_benchmark.rs index bcf1c0f8..02ee15df 100644 --- a/benches/my_benchmark.rs +++ b/benches/my_benchmark.rs @@ -177,6 +177,26 @@ pub fn bench_stereo_panning_automation() { assert_eq!(ctx.start_rendering_sync().length(), SAMPLES); } +// This only benchmarks the render thread filling the analyser buffers. +// We don't request freq/time data because that happens off thread and there is no sensible way to +// benchmark this in deterministic way [citation needed]. +pub fn bench_analyser_node() { + let ctx = OfflineAudioContext::new(2, black_box(SAMPLES), SAMPLE_RATE); + let file = std::fs::File::open("samples/think-stereo-48000.wav").unwrap(); + let buffer = ctx.decode_audio_data_sync(file).unwrap(); + + let analyser = ctx.create_analyser(); + analyser.connect(&ctx.destination()); + + let src = ctx.create_buffer_source(); + src.connect(&analyser); + src.set_buffer(buffer); + src.set_loop(true); + src.start(); + + assert_eq!(ctx.start_rendering_sync().length(), SAMPLES); +} + iai::main!( bench_ctor, bench_sine, @@ -188,4 +208,5 @@ iai::main!( bench_buffer_src_biquad, bench_stereo_positional, bench_stereo_panning_automation, + bench_analyser_node, ); From 16706396f09c5a511dd25656962844aecf55e790 Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 28 Jan 2023 11:57:14 +0100 Subject: [PATCH 14/18] Clippy fixes for new rust version --- src/context/online.rs | 2 +- src/io/cubeb.rs | 4 ++-- src/node/iir_filter.rs | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/context/online.rs b/src/context/online.rs index cbc3e5ef..5deb8e09 100644 --- a/src/context/online.rs +++ b/src/context/online.rs @@ -241,7 +241,7 @@ impl AudioContext { } if !is_valid_sink_id(&sink_id) { - Err(format!("NotFoundError: invalid sinkId {}", sink_id))?; + Err(format!("NotFoundError: invalid sinkId {sink_id}"))?; }; let mut backend_manager_guard = self.backend_manager.lock().unwrap(); diff --git a/src/io/cubeb.rs b/src/io/cubeb.rs index 8fbf6839..d6ec81b1 100644 --- a/src/io/cubeb.rs +++ b/src/io/cubeb.rs @@ -124,7 +124,7 @@ fn init_output_backend( output.len() as isize }) .state_callback(|state| { - println!("stream state changed: {:?}", state); + println!("stream state changed: {state:?}"); }); let stream = builder.init(ctx).expect("Failed to create cubeb stream"); @@ -317,7 +317,7 @@ impl AudioBackendManager for CubebBackend { input.len() as isize }) .state_callback(|state| { - println!("stream state changed: {:?}", state); + println!("stream state changed: {state:?}"); }); let stream = builder.init(&ctx).expect("Failed to create cubeb stream"); diff --git a/src/node/iir_filter.rs b/src/node/iir_filter.rs index 60ffd85d..2e4cac1d 100644 --- a/src/node/iir_filter.rs +++ b/src/node/iir_filter.rs @@ -539,7 +539,6 @@ mod tests { context.start_rendering_sync() }; - println!("{:?}", filter_type); assert_float_eq!( biquad_res.get_channel_data(0), iir_res.get_channel_data(0), @@ -776,7 +775,6 @@ mod tests { (mags, phases) }; - println!("{:?}", filter_type); assert_float_eq!(biquad_response.0, iir_response.0, abs_all <= 1e-6); assert_float_eq!(biquad_response.1, iir_response.1, abs_all <= 1e-6); } From 1c29c2f8ca820ab46442222935b6596c9bd635ef Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 28 Jan 2023 12:05:04 +0100 Subject: [PATCH 15/18] Fix double indirection of buffer in AnalyserNode - buffer: Arc>, + buffer: Arc<[AtomicF32]>, --- src/analysis.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index da6e7015..cb85bfd5 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -89,7 +89,7 @@ const RING_BUFFER_SIZE: usize = MAX_FFT_SIZE + RENDER_QUANTUM_SIZE; // single producer / multiple consumer ring buffer #[derive(Clone)] pub(crate) struct AnalyserRingBuffer { - buffer: Arc>, + buffer: Arc<[AtomicF32]>, write_index: Arc, } @@ -99,7 +99,7 @@ impl AnalyserRingBuffer { buffer.resize_with(RING_BUFFER_SIZE, || AtomicF32::new(0.)); Self { - buffer: Arc::from(buffer), + buffer: buffer.into(), write_index: Arc::new(AtomicUsize::new(0)), } } From 589985b12d7f13b2b401ab73dda42a5cf9281ee2 Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 28 Jan 2023 12:47:39 +0100 Subject: [PATCH 16/18] Remove Arc/Mutex from Analyser FFTPlanner, not needed --- src/analysis.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index cb85bfd5..6ce341cd 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -4,7 +4,7 @@ use std::f32::consts::PI; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use realfft::{num_complex::Complex, RealFftPlanner}; @@ -158,11 +158,7 @@ pub(crate) struct Analyser { smoothing_time_constant: f64, min_decibels: f64, max_decibels: f64, - // If not wrapped into Arc>, compiler complains about thread safety: - // > `(dyn rustfft::avx::avx_planner::AvxPlannerInternalAPI + 'static)` - // > cannot be shared between threads safely - // But the Mutex is ok here as `Analyser` lives outside the audio thread. - fft_planner: Arc>>, + fft_planner: RealFftPlanner, fft_input: Vec, fft_scratch: Vec>, fft_output: Vec>, @@ -194,7 +190,7 @@ impl Analyser { smoothing_time_constant: DEFAULT_SMOOTHING_TIME_CONSTANT, min_decibels: DEFAULT_MIN_DECIBELS, max_decibels: DEFAULT_MAX_DECIBELS, - fft_planner: Arc::new(Mutex::new(fft_planner)), + fft_planner, fft_input, fft_scratch, fft_output, @@ -285,7 +281,7 @@ impl Analyser { let fft_size = self.fft_size(); let smoothing_time_constant = self.smoothing_time_constant() as f32; // setup FFT planner and properly sized buffers - let r2c = self.fft_planner.lock().unwrap().plan_fft_forward(fft_size); + let r2c = self.fft_planner.plan_fft_forward(fft_size); let input = &mut self.fft_input[..fft_size]; let output = &mut self.fft_output[..fft_size / 2 + 1]; let scratch = &mut self.fft_scratch[..r2c.get_scratch_len()]; From 4ab73bc92cc1987f7aaeb7afa2c30cbbe6695aed Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 28 Jan 2023 12:48:32 +0100 Subject: [PATCH 17/18] Improve tests in src/analysis.rs, make sure threads are actually run --- src/analysis.rs | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 6ce341cd..64481d6d 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -796,42 +796,28 @@ mod tests { fn test_ring_buffer_concurrency() { let analyser = Arc::new(Analyser::new()); let ring_buffer = analyser.get_ring_buffer_clone(); - let num_loops = 10_000; + let (sender, receiver) = crossbeam_channel::bounded(1); - let _ = thread::spawn(move || { + thread::spawn(move || { let mut rng = rand::thread_rng(); - let mut counter = 0; + sender.send(()).unwrap(); // signal ready - loop { + for _ in 0..num_loops { let rand = rng.gen::(); let data = [rand; RENDER_QUANTUM_SIZE]; ring_buffer.write(&data); - counter += 1; - - if counter == num_loops { - break; - } - std::thread::sleep(std::time::Duration::from_nanos(30)); } }); - std::thread::sleep(std::time::Duration::from_millis(1)); - - let mut counter = 0; + // wait for thread to boot + receiver.recv().unwrap(); - loop { + for _ in 0..num_loops { let mut read_buffer = [0.; RENDER_QUANTUM_SIZE]; analyser.get_float_time_domain_data(&mut read_buffer); - - counter += 1; - - if counter == num_loops { - break; - } - std::thread::sleep(std::time::Duration::from_nanos(25)); } } @@ -840,9 +826,11 @@ mod tests { fn test_thread_safety() { let analyser = Arc::new(RwLock::new(Analyser::new())); - thread::spawn(move || { + let handle = thread::spawn(move || { analyser.write().unwrap().set_fft_size(MIN_FFT_SIZE); assert_eq!(analyser.write().unwrap().fft_size(), MIN_FFT_SIZE); }); + + handle.join().unwrap(); } } From caf735db6d1139145574bf0a8f049b5dec7c9596 Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 28 Jan 2023 12:54:58 +0100 Subject: [PATCH 18/18] Revisit "Remove Arc/Mutex from Analyser FFTPlanner, not needed" The Mutex is necessary on avx-enabled systems This reverts commit 589985b12d7f13b2b401ab73dda42a5cf9281ee2. --- src/analysis.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index 64481d6d..0d098e48 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -4,7 +4,7 @@ use std::f32::consts::PI; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use realfft::{num_complex::Complex, RealFftPlanner}; @@ -158,7 +158,7 @@ pub(crate) struct Analyser { smoothing_time_constant: f64, min_decibels: f64, max_decibels: f64, - fft_planner: RealFftPlanner, + fft_planner: Mutex>, // RealFftPlanner is not `Sync` on all platforms fft_input: Vec, fft_scratch: Vec>, fft_output: Vec>, @@ -190,7 +190,7 @@ impl Analyser { smoothing_time_constant: DEFAULT_SMOOTHING_TIME_CONSTANT, min_decibels: DEFAULT_MIN_DECIBELS, max_decibels: DEFAULT_MAX_DECIBELS, - fft_planner, + fft_planner: Mutex::new(fft_planner), fft_input, fft_scratch, fft_output, @@ -281,7 +281,7 @@ impl Analyser { let fft_size = self.fft_size(); let smoothing_time_constant = self.smoothing_time_constant() as f32; // setup FFT planner and properly sized buffers - let r2c = self.fft_planner.plan_fft_forward(fft_size); + let r2c = self.fft_planner.lock().unwrap().plan_fft_forward(fft_size); let input = &mut self.fft_input[..fft_size]; let output = &mut self.fft_output[..fft_size / 2 + 1]; let scratch = &mut self.fft_scratch[..r2c.get_scratch_len()];