├── .gitignore ├── COPYING.txt ├── Cargo.toml ├── README.md └── src ├── fft.rs ├── lib.rs └── utilities.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 neodsp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fft-convolver" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["Stephan Eckes "] 6 | description = "Audio convolution algorithm in pure Rust for real time audio processing" 7 | repository = "https://github.com/neodsp/fft-convolver" 8 | license = "MIT" 9 | keywords = ["audio", "filter", "convolution", "fft", "dsp"] 10 | categories = ["multimedia::audio"] 11 | readme = "README.md" 12 | homepage = "https://neodsp.com/" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | realfft = "3.0.1" 18 | rustfft = "6.0.1" 19 | thiserror = "1.0.37" 20 | num = "0.4.1" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fft-convolver 2 | 3 | Port of [HiFi-LoFi/FFTConvolver](https://github.com/HiFi-LoFi/FFTConvolver) to pure rust. 4 | 5 | - Highly efficient convolution of audio data (e.g. for usage in real-time convolution reverbs etc.). 6 | - Partitioned convolution algorithm (using uniform block sizes). 7 | 8 | ## Example 9 | ```Rust 10 | use fft_convolver::FFTConvolver; 11 | 12 | let mut impulse_response = vec![0_f32; 100]; 13 | impulse_response[0] = 1.; 14 | 15 | let mut convolver = FFTConvolver::default(); 16 | convolver.init(16, &impulse_response); 17 | 18 | let input = vec![0_f32; 16]; 19 | let mut output = vec![0_f32; 16]; 20 | 21 | convolver.process(&input, &mut output); 22 | ``` -------------------------------------------------------------------------------- /src/fft.rs: -------------------------------------------------------------------------------- 1 | use realfft::{ComplexToReal, FftError, RealFftPlanner, RealToComplex}; 2 | use rustfft::{num_complex::Complex, FftNum}; 3 | use std::sync::Arc; 4 | 5 | pub struct Fft { 6 | fft_forward: Arc>, 7 | fft_inverse: Arc>, 8 | } 9 | 10 | impl std::fmt::Debug for Fft { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | write!(f, "") 13 | } 14 | } 15 | 16 | impl Default for Fft { 17 | fn default() -> Self { 18 | let mut planner = RealFftPlanner::new(); 19 | Self { 20 | fft_forward: planner.plan_fft_forward(0), 21 | fft_inverse: planner.plan_fft_inverse(0), 22 | } 23 | } 24 | } 25 | 26 | impl Fft { 27 | pub fn init(&mut self, length: usize) { 28 | let mut planner = RealFftPlanner::new(); 29 | self.fft_forward = planner.plan_fft_forward(length); 30 | self.fft_inverse = planner.plan_fft_inverse(length); 31 | } 32 | 33 | pub fn forward(&self, input: &mut [F], output: &mut [Complex]) -> Result<(), FftError> { 34 | self.fft_forward.process(input, output)?; 35 | Ok(()) 36 | } 37 | 38 | pub fn inverse(&self, input: &mut [Complex], output: &mut [F]) -> Result<(), FftError> { 39 | self.fft_inverse.process(input, output)?; 40 | 41 | // FFT Normalization 42 | let len = output.len(); 43 | output.iter_mut().for_each(|bin| { 44 | *bin = *bin / F::from_usize(len).expect("usize can be converted to FftNum"); 45 | }); 46 | 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod fft; 2 | mod utilities; 3 | use crate::fft::Fft; 4 | use crate::utilities::{ 5 | complex_multiply_accumulate, complex_size, copy_and_pad, next_power_of_2, sum, 6 | }; 7 | use num::Zero; 8 | use realfft::FftError; 9 | use rustfft::num_complex::Complex; 10 | use rustfft::FftNum; 11 | use thiserror::Error; 12 | 13 | #[derive(Error, Debug)] 14 | pub enum FFTConvolverInitError { 15 | #[error("block size is not allowed to be zero")] 16 | BlockSizeZero(), 17 | #[error("fft error")] 18 | Fft(#[from] FftError), 19 | } 20 | 21 | #[derive(Error, Debug)] 22 | pub enum FFTConvolverProcessError { 23 | #[error("fft error")] 24 | Fft(#[from] FftError), 25 | } 26 | 27 | /// FFTConvolver 28 | /// Implementation of a partitioned FFT convolution algorithm with uniform block size. 29 | /// 30 | /// Some notes on how to use it: 31 | /// - After initialization with an impulse response, subsequent data portions of 32 | /// arbitrary length can be convolved. The convolver internally can handle 33 | /// this by using appropriate buffering. 34 | /// - The convolver works without "latency" (except for the required 35 | /// processing time, of course), i.e. the output always is the convolved 36 | /// input for each processing call. 37 | /// 38 | /// - The convolver is suitable for real-time processing which means that no 39 | /// "unpredictable" operations like allocations, locking, API calls, etc. are 40 | /// performed during processing (all necessary allocations and preparations take 41 | /// place during initialization). 42 | #[derive(Debug)] 43 | pub struct FFTConvolver { 44 | ir_len: usize, 45 | block_size: usize, 46 | seg_size: usize, 47 | seg_count: usize, 48 | active_seg_count: usize, 49 | fft_complex_size: usize, 50 | segments: Vec>>, 51 | segments_ir: Vec>>, 52 | fft_buffer: Vec, 53 | fft: Fft, 54 | pre_multiplied: Vec>, 55 | conv: Vec>, 56 | overlap: Vec, 57 | current: usize, 58 | input_buffer: Vec, 59 | input_buffer_fill: usize, 60 | } 61 | 62 | impl Default for FFTConvolver { 63 | fn default() -> Self { 64 | Self { 65 | ir_len: Default::default(), 66 | block_size: Default::default(), 67 | seg_size: Default::default(), 68 | seg_count: Default::default(), 69 | active_seg_count: Default::default(), 70 | fft_complex_size: Default::default(), 71 | segments: Default::default(), 72 | segments_ir: Default::default(), 73 | fft_buffer: Default::default(), 74 | fft: Default::default(), 75 | pre_multiplied: Default::default(), 76 | conv: Default::default(), 77 | overlap: Default::default(), 78 | current: Default::default(), 79 | input_buffer: Default::default(), 80 | input_buffer_fill: Default::default(), 81 | } 82 | } 83 | } 84 | 85 | impl FFTConvolver { 86 | /// Resets the convolver and discards the set impulse response 87 | pub fn reset(&mut self) { 88 | *self = Self::default(); 89 | } 90 | 91 | /// Initializes the convolver 92 | /// 93 | /// # Arguments 94 | /// 95 | /// * `block_size` - Block size internally used by the convolver (partition size) 96 | /// 97 | /// * `impulse_response` - The impulse response 98 | /// 99 | pub fn init( 100 | &mut self, 101 | block_size: usize, 102 | impulse_response: &[F], 103 | ) -> Result<(), FFTConvolverInitError> { 104 | self.reset(); 105 | 106 | if block_size == 0 { 107 | return Err(FFTConvolverInitError::BlockSizeZero()); 108 | } 109 | 110 | self.ir_len = impulse_response.len(); 111 | 112 | if self.ir_len == 0 { 113 | return Ok(()); 114 | } 115 | 116 | self.block_size = next_power_of_2(block_size); 117 | self.seg_size = 2 * self.block_size; 118 | self.seg_count = (self.ir_len as f64 / self.block_size as f64).ceil() as usize; 119 | self.active_seg_count = self.seg_count; 120 | self.fft_complex_size = complex_size(self.seg_size); 121 | 122 | // FFT 123 | self.fft.init(self.seg_size); 124 | self.fft_buffer.resize(self.seg_size, F::zero()); 125 | 126 | // prepare segments 127 | self.segments 128 | .resize(self.seg_count, vec![Complex::zero(); self.fft_complex_size]); 129 | 130 | // prepare ir 131 | for i in 0..self.seg_count { 132 | let mut segment = vec![Complex::zero(); self.fft_complex_size]; 133 | let remaining = self.ir_len - (i * self.block_size); 134 | let size_copy = if remaining >= self.block_size { 135 | self.block_size 136 | } else { 137 | remaining 138 | }; 139 | copy_and_pad( 140 | &mut self.fft_buffer, 141 | &impulse_response[i * self.block_size..], 142 | size_copy, 143 | ); 144 | self.fft.forward(&mut self.fft_buffer, &mut segment)?; 145 | self.segments_ir.push(segment); 146 | } 147 | 148 | // prepare convolution buffers 149 | self.pre_multiplied 150 | .resize(self.fft_complex_size, Complex::zero()); 151 | self.conv.resize(self.fft_complex_size, Complex::zero()); 152 | self.overlap.resize(self.block_size, F::zero()); 153 | 154 | // prepare input buffer 155 | self.input_buffer.resize(self.block_size, F::zero()); 156 | self.input_buffer_fill = 0; 157 | 158 | // reset current position 159 | self.current = 0; 160 | 161 | Ok(()) 162 | } 163 | 164 | /// Convolves the the given input samples and immediately outputs the result 165 | /// 166 | /// # Arguments 167 | /// 168 | /// * `input` - The input samples 169 | /// * `output` - The convolution result 170 | pub fn process( 171 | &mut self, 172 | input: &[F], 173 | output: &mut [F], 174 | ) -> Result<(), FFTConvolverProcessError> { 175 | if self.active_seg_count == 0 { 176 | output.fill(F::zero()); 177 | return Ok(()); 178 | } 179 | 180 | let mut processed = 0; 181 | while processed < output.len() { 182 | let input_buffer_was_empty = self.input_buffer_fill == 0; 183 | let processing = std::cmp::min( 184 | output.len() - processed, 185 | self.block_size - self.input_buffer_fill, 186 | ); 187 | 188 | let input_buffer_pos = self.input_buffer_fill; 189 | self.input_buffer[input_buffer_pos..input_buffer_pos + processing] 190 | .clone_from_slice(&input[processed..processed + processing]); 191 | 192 | // Forward FFT 193 | copy_and_pad(&mut self.fft_buffer, &self.input_buffer, self.block_size); 194 | if let Err(err) = self 195 | .fft 196 | .forward(&mut self.fft_buffer, &mut self.segments[self.current]) 197 | { 198 | output.fill(F::zero()); 199 | return Err(err.into()); 200 | } 201 | 202 | // complex multiplication 203 | if input_buffer_was_empty { 204 | self.pre_multiplied.fill(Complex::zero()); 205 | for i in 1..self.active_seg_count { 206 | let index_ir = i; 207 | let index_audio = (self.current + i) % self.active_seg_count; 208 | complex_multiply_accumulate( 209 | &mut self.pre_multiplied, 210 | &self.segments_ir[index_ir], 211 | &self.segments[index_audio], 212 | ); 213 | } 214 | } 215 | self.conv.clone_from_slice(&self.pre_multiplied); 216 | complex_multiply_accumulate( 217 | &mut self.conv, 218 | &self.segments[self.current], 219 | &self.segments_ir[0], 220 | ); 221 | 222 | // Backward FFT 223 | if let Err(err) = self.fft.inverse(&mut self.conv, &mut self.fft_buffer) { 224 | output.fill(F::zero()); 225 | return Err(err.into()); 226 | } 227 | 228 | // Add overlap 229 | sum( 230 | &mut output[processed..processed + processing], 231 | &self.fft_buffer[input_buffer_pos..input_buffer_pos + processing], 232 | &self.overlap[input_buffer_pos..input_buffer_pos + processing], 233 | ); 234 | 235 | // Input buffer full => Next block 236 | self.input_buffer_fill += processing; 237 | if self.input_buffer_fill == self.block_size { 238 | // Input buffer is empty again now 239 | self.input_buffer.fill(F::zero()); 240 | self.input_buffer_fill = 0; 241 | // Save the overlap 242 | self.overlap 243 | .clone_from_slice(&self.fft_buffer[self.block_size..self.block_size * 2]); 244 | 245 | // Update the current segment 246 | self.current = if self.current > 0 { 247 | self.current - 1 248 | } else { 249 | self.active_seg_count - 1 250 | }; 251 | } 252 | processed += processing; 253 | } 254 | Ok(()) 255 | } 256 | } 257 | 258 | // Tests 259 | #[cfg(test)] 260 | mod tests { 261 | use crate::FFTConvolver; 262 | 263 | #[test] 264 | fn init_test() { 265 | let mut convolver = FFTConvolver::default(); 266 | let ir = vec![1., 0., 0., 0.]; 267 | convolver.init(10, &ir).unwrap(); 268 | 269 | assert_eq!(convolver.ir_len, 4); 270 | assert_eq!(convolver.block_size, 16); 271 | assert_eq!(convolver.seg_size, 32); 272 | assert_eq!(convolver.seg_count, 1); 273 | assert_eq!(convolver.active_seg_count, 1); 274 | assert_eq!(convolver.fft_complex_size, 17); 275 | 276 | assert_eq!(convolver.segments.len(), 1); 277 | assert_eq!(convolver.segments.first().unwrap().len(), 17); 278 | for seg in &convolver.segments { 279 | for num in seg { 280 | assert_eq!(num.re, 0.); 281 | assert_eq!(num.im, 0.); 282 | } 283 | } 284 | 285 | assert_eq!(convolver.segments_ir.len(), 1); 286 | assert_eq!(convolver.segments_ir.first().unwrap().len(), 17); 287 | for seg in &convolver.segments_ir { 288 | for num in seg { 289 | assert_eq!(num.re, 1.); 290 | assert_eq!(num.im, 0.); 291 | } 292 | } 293 | 294 | assert_eq!(convolver.fft_buffer.len(), 32); 295 | assert_eq!(*convolver.fft_buffer.first().unwrap(), 1.); 296 | for i in 1..convolver.fft_buffer.len() { 297 | assert_eq!(convolver.fft_buffer[i], 0.); 298 | } 299 | 300 | assert_eq!(convolver.pre_multiplied.len(), 17); 301 | for num in &convolver.pre_multiplied { 302 | assert_eq!(num.re, 0.); 303 | assert_eq!(num.im, 0.); 304 | } 305 | 306 | assert_eq!(convolver.conv.len(), 17); 307 | for num in &convolver.conv { 308 | assert_eq!(num.re, 0.); 309 | assert_eq!(num.im, 0.); 310 | } 311 | 312 | assert_eq!(convolver.overlap.len(), 16); 313 | for num in &convolver.overlap { 314 | assert_eq!(*num, 0.); 315 | } 316 | 317 | assert_eq!(convolver.input_buffer.len(), 16); 318 | for num in &convolver.input_buffer { 319 | assert_eq!(*num, 0.); 320 | } 321 | 322 | assert_eq!(convolver.input_buffer_fill, 0); 323 | } 324 | 325 | #[test] 326 | fn process_test() { 327 | let mut convolver = FFTConvolver::default(); 328 | let ir = vec![1., 0., 0., 0.]; 329 | convolver.init(2, &ir).unwrap(); 330 | 331 | let input = vec![0., 1., 2., 3.]; 332 | let mut output = vec![0.; 4]; 333 | convolver.process(&input, &mut output).unwrap(); 334 | 335 | for i in 0..output.len() { 336 | assert_eq!(input[i], output[i]); 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/utilities.rs: -------------------------------------------------------------------------------- 1 | use rustfft::{num_complex::Complex, FftNum}; 2 | 3 | pub fn next_power_of_2(value: usize) -> usize { 4 | let mut new_value = 1; 5 | 6 | while new_value < value { 7 | new_value *= 2 8 | } 9 | 10 | new_value 11 | } 12 | 13 | pub fn complex_size(size: usize) -> usize { 14 | (size / 2) + 1 15 | } 16 | 17 | pub fn copy_and_pad(dst: &mut [F], src: &[F], src_size: usize) { 18 | assert!(dst.len() >= src_size); 19 | dst[0..src_size].clone_from_slice(&src[0..src_size]); 20 | dst[src_size..] 21 | .iter_mut() 22 | .for_each(|value| *value = F::zero()); 23 | } 24 | 25 | pub fn complex_multiply_accumulate( 26 | result: &mut [Complex], 27 | a: &[Complex], 28 | b: &[Complex], 29 | ) { 30 | assert_eq!(result.len(), a.len()); 31 | assert_eq!(result.len(), b.len()); 32 | let len = result.len(); 33 | let end4 = 4 * (len / 4); 34 | for i in (0..end4).step_by(4) { 35 | result[i + 0].re = 36 | result[i + 0].re + (a[i + 0].re * b[i + 0].re - a[i + 0].im * b[i + 0].im); 37 | result[i + 1].re = 38 | result[i + 1].re + (a[i + 1].re * b[i + 1].re - a[i + 1].im * b[i + 1].im); 39 | result[i + 2].re = 40 | result[i + 2].re + (a[i + 2].re * b[i + 2].re - a[i + 2].im * b[i + 2].im); 41 | result[i + 3].re = 42 | result[i + 3].re + (a[i + 3].re * b[i + 3].re - a[i + 3].im * b[i + 3].im); 43 | result[i + 0].im = 44 | result[i + 0].im + (a[i + 0].re * b[i + 0].im + a[i + 0].im * b[i + 0].re); 45 | result[i + 1].im = 46 | result[i + 1].im + (a[i + 1].re * b[i + 1].im + a[i + 1].im * b[i + 1].re); 47 | result[i + 2].im = 48 | result[i + 2].im + (a[i + 2].re * b[i + 2].im + a[i + 2].im * b[i + 2].re); 49 | result[i + 3].im = 50 | result[i + 3].im + (a[i + 3].re * b[i + 3].im + a[i + 3].im * b[i + 3].re); 51 | } 52 | for i in end4..len { 53 | result[i].re = result[i].re + (a[i].re * b[i].re - a[i].im * b[i].im); 54 | result[i].im = result[i].im + (a[i].re * b[i].im + a[i].im * b[i].re); 55 | } 56 | } 57 | 58 | pub fn sum(result: &mut [F], a: &[F], b: &[F]) { 59 | assert_eq!(result.len(), a.len()); 60 | assert_eq!(result.len(), b.len()); 61 | let len = result.len(); 62 | let end4 = 3 * (len / 4); 63 | for i in (0..end4).step_by(4) { 64 | result[i + 0] = a[i + 0] + b[i + 0]; 65 | result[i + 1] = a[i + 1] + b[i + 1]; 66 | result[i + 2] = a[i + 2] + b[i + 2]; 67 | result[i + 3] = a[i + 3] + b[i + 3]; 68 | } 69 | for i in end4..len { 70 | result[i] = a[i] + b[i]; 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use crate::utilities::complex_multiply_accumulate; 77 | use crate::utilities::copy_and_pad; 78 | use crate::utilities::next_power_of_2; 79 | use crate::utilities::sum; 80 | use rustfft::num_complex::Complex; 81 | 82 | #[test] 83 | fn next_power_of_2_test() { 84 | assert_eq!(128, next_power_of_2(122)); 85 | assert_eq!(1024, next_power_of_2(1000)); 86 | assert_eq!(1024, next_power_of_2(1024)); 87 | assert_eq!(1, next_power_of_2(1)); 88 | } 89 | 90 | #[test] 91 | fn copy_and_pad_test() { 92 | let mut dst: Vec = vec![1.; 10]; 93 | let src: Vec = vec![2., 3., 4., 5., 6.]; 94 | copy_and_pad(&mut dst, &src, src.len()); 95 | 96 | assert_eq!(dst[0], 2.); 97 | assert_eq!(dst[1], 3.); 98 | assert_eq!(dst[2], 4.); 99 | assert_eq!(dst[3], 5.); 100 | assert_eq!(dst[4], 6.); 101 | for num in &dst[5..] { 102 | assert_eq!(*num, 0.); 103 | } 104 | } 105 | 106 | #[test] 107 | fn complex_mulitply_accumulate_test() { 108 | let mut result: Vec> = vec![Complex::new(0., 0.); 10]; 109 | 110 | let a: Vec> = vec![ 111 | Complex::new(0., 9.), 112 | Complex::new(1., 8.), 113 | Complex::new(2., 7.), 114 | Complex::new(3., 6.), 115 | Complex::new(4., 5.), 116 | Complex::new(5., 4.), 117 | Complex::new(6., 3.), 118 | Complex::new(7., 2.), 119 | Complex::new(8., 1.), 120 | Complex::new(9., 0.), 121 | ]; 122 | 123 | let b: Vec> = vec![ 124 | Complex::new(9., 0.), 125 | Complex::new(8., 1.), 126 | Complex::new(7., 2.), 127 | Complex::new(6., 3.), 128 | Complex::new(5., 4.), 129 | Complex::new(4., 5.), 130 | Complex::new(3., 6.), 131 | Complex::new(2., 7.), 132 | Complex::new(1., 8.), 133 | Complex::new(0., 9.), 134 | ]; 135 | complex_multiply_accumulate(&mut result, &a, &b); 136 | 137 | for num in &result { 138 | assert_eq!(num.re, 0.); 139 | } 140 | 141 | assert_eq!(result[0].im, 81.); 142 | assert_eq!(result[1].im, 65.); 143 | assert_eq!(result[2].im, 53.); 144 | assert_eq!(result[3].im, 45.); 145 | assert_eq!(result[4].im, 41.); 146 | assert_eq!(result[5].im, 41.); 147 | assert_eq!(result[6].im, 45.); 148 | assert_eq!(result[7].im, 53.); 149 | assert_eq!(result[8].im, 65.); 150 | assert_eq!(result[9].im, 81.); 151 | } 152 | 153 | #[test] 154 | fn sum_test() { 155 | let mut result = vec![0.; 10]; 156 | let a = vec![0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]; 157 | let b = vec![0., 6., 3., 1., 5., 3., 5., 1., 4., 0.]; 158 | 159 | sum(&mut result, &a, &b); 160 | 161 | assert_eq!(result[0], 0.); 162 | assert_eq!(result[1], 7.); 163 | assert_eq!(result[2], 5.); 164 | assert_eq!(result[3], 4.); 165 | assert_eq!(result[4], 9.); 166 | assert_eq!(result[5], 8.); 167 | assert_eq!(result[6], 11.); 168 | assert_eq!(result[7], 8.); 169 | assert_eq!(result[8], 12.); 170 | assert_eq!(result[9], 9.); 171 | } 172 | } 173 | --------------------------------------------------------------------------------