├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── audio ├── amen.flac └── ir.flac ├── build.sh ├── index.html ├── js ├── setup.js └── worklet.js ├── server.sh ├── src ├── binaural_loop_player.rs ├── binaural_loop_player │ ├── ambisonics.rs │ ├── binauralizer.rs │ ├── binauralizer │ │ └── convolver.rs │ └── loop_player.rs └── lib.rs └── wasm └── wasm_loop_player.wasm /.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | *~ 3 | \#* 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-loop-player" 3 | version = "0.1.0" 4 | authors = ["nik "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | lazy_static = "1.2.0" 12 | wee_alloc = { version = "0.4.2", optional = true } 13 | num = "0.2.0" 14 | chfft = "0.2.4" 15 | 16 | [dev-dependencies] 17 | assert_approx_eq = "1.1.0" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Niklas Reppel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-loop-player 2 | A (binaural) loop player made with **Rust**, **AudioWorklets** and **WebAssembly**. 3 | 4 | See it running: https://parkellipsen.de/amen (sometimes you need to re-load once to make it work ...) 5 | -------------------------------------------------------------------------------- /audio/amen.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-drunk-coder/wasm-loop-player/a91da8752fc34e287e63de4d71bc9d9aa025152a/audio/amen.flac -------------------------------------------------------------------------------- /audio/ir.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-drunk-coder/wasm-loop-player/a91da8752fc34e287e63de4d71bc9d9aa025152a/audio/ir.flac -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | echo "compile wasm" 4 | cargo build --target wasm32-unknown-unknown --release 5 | 6 | echo "copy" 7 | cp target/wasm32-unknown-unknown/release/wasm_loop_player.wasm ./wasm/ 8 | 9 | echo "finish!" 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Play the Amen Break !!!11

9 | Click the On/Off toggle to start! 10 |
11 |

On/Off

12 |

Horizontal Pan

13 |

Vertical Pan

14 |
15 |
16 |
17 | Inspired by this! 18 |
19 |
20 |
21 | Made with Rust, AudioWorklet and WebAssembly! 22 |
23 |
24 |
25 | Code: https://github.com/the-drunk-coder/wasm-loop-player 26 |
27 |
28 |
29 | HRIRs adapted from KU100 NF Set: http://audiogroup.web.th-koeln.de/ku100nfhrir.html 30 |
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /js/setup.js: -------------------------------------------------------------------------------- 1 | // Get the audio context. 2 | // Omitting the low latency hint for simplicity. 3 | const ctx = new AudioContext({ 4 | sampleRate: 44100, 5 | }) 6 | 7 | // Not all Browsers currently support the AudioWorklet. 8 | if (ctx.audioWorklet === undefined) { 9 | alert("AudioWorklet isn't supported... It cannot work.") 10 | } else { 11 | // First, load the AudioWorklet module. 12 | ctx.audioWorklet.addModule('js/worklet.js?t=' + new Date().getTime()) 13 | .then(() => { 14 | 15 | /////////////////////////////// 16 | // BASIC WEB AUDIO API SETUP // 17 | /////////////////////////////// 18 | 19 | // once the module has been loaded, create the DSP graph 20 | const n = new AudioWorkletNode(ctx, 'loop-player-processor', { numberOfInputs: 1, 21 | numberOfOutputs: 1, 22 | outputChannelCount: [2], } ); 23 | // Activate the node by connecting it to the output. 24 | n.connect(ctx.destination) 25 | 26 | ////////////////// 27 | // GUI ELEMENTS // 28 | ////////////////// 29 | 30 | // This button activates or deactivates the sample player. 31 | // Automatic playback is not allowed. 32 | const autoPlay = document.getElementById('auto-play') 33 | autoPlay.addEventListener('change', e => { 34 | if (e.target.value === 0) { 35 | n.port.postMessage({ type: 'disable', }) 36 | } else { 37 | if(ctx.state === "suspended") { 38 | ctx.resume(); 39 | } 40 | n.port.postMessage({ type: 'enable', }) 41 | } 42 | }) 43 | 44 | // Azimuth slider. 45 | const azi = document.getElementById('azimuth-slider') 46 | azi.addEventListener('input', e => { 47 | n.parameters.get('azimuth').value = e.target.value 48 | }) 49 | 50 | // Elevation slider. 51 | const ele = document.getElementById('elevation-slider') 52 | ele.addEventListener('input', e => { 53 | n.parameters.get('elevation').value = e.target.value 54 | }) 55 | 56 | ////////////////////////////// 57 | // ADDITIONAL WORKLET SETUP // 58 | ////////////////////////////// 59 | 60 | // Here's one of the crucial challenges. The WebAudio API currently doesn't 61 | // allow any fetch(...) calls in the worklet module itself. 62 | // 63 | // This is why we need to fetch everything in the main thread and post it (as binary data) 64 | // to the worklet using the worklet using the message port facilities. 65 | // 66 | // This doesn't only include the sample files, but also the WebAssembly module itself. 67 | // 68 | // In the worklet you'll find the other end of the message port. 69 | 70 | // Fetch the actual sample. 71 | fetch('audio/amen.flac?t=' + new Date().getTime()) 72 | .then(r => r.arrayBuffer()) 73 | .then(r => ctx.decodeAudioData(r) 74 | .then(r => n.port.postMessage({ type: 'loadSample', samples: r.getChannelData(0), length: r.length }))) 75 | 76 | // Fetch the impulse response we need for the binaural processing. 77 | fetch('audio/ir.flac?t=' + new Date().getTime()) 78 | .then(r => r.arrayBuffer()) 79 | .then(r => ctx.decodeAudioData(r) 80 | .then(r => n.port.postMessage({ type: 'loadIr', samples: r.getChannelData(0), length: r.length }))) 81 | 82 | // Fetch the WebAssembly module which contains our main DSP logic. 83 | fetch('wasm/wasm_loop_player.wasm?t=' + new Date().getTime()) 84 | .then(r => r.arrayBuffer()) 85 | .then(r => n.port.postMessage({ type: 'loadWasm', data: r })) 86 | }) 87 | // If successful, post samplerate ... 88 | console.log("Samplerate: " + ctx.sampleRate); 89 | } 90 | -------------------------------------------------------------------------------- /js/worklet.js: -------------------------------------------------------------------------------- 1 | class LoopPlayerProcessor extends AudioWorkletProcessor { 2 | // The parameter descriptors are part of the API standard. 3 | static get parameterDescriptors() { 4 | return [ 5 | { 6 | name: 'azimuth', 7 | defaultValue: 0.0 8 | }, 9 | { 10 | name: 'elevation', 11 | defaultValue: -1.57 12 | } 13 | ] 14 | } 15 | 16 | constructor(options) { 17 | super(options) 18 | // Set up the receiving part of the worklet's message port. 19 | // Use the 'data.type' field to define the handling. 20 | this.port.onmessage = e => { 21 | // Recieve the WebAssembly module (the Rust part) as binary data. 22 | // In the setup.js part there's some explanation why this is necessary. 23 | // Basically, 'fetch()' calls aren't allowed in the worklet itself. 24 | if (e.data.type === 'loadWasm') { 25 | 26 | // Instantiate the WebAssembly module from the received binary data. 27 | WebAssembly.instantiate(e.data.data).then(w => { 28 | 29 | // Call wasm module constructor. 30 | this._wasm = w.instance 31 | 32 | // Grow memory to accomodate full sample. 33 | this._wasm.exports.memory.grow(250) 34 | 35 | // Set the blocksize. 36 | this._size = 128 37 | 38 | // If the sample data has already been received (the order isn't necessarily 39 | // deterministic), allocate some memory in the linear WebAssembly 40 | // memory to hold ths sample and share it with the WebAssembly module. 41 | if(this._sample_data) { 42 | 43 | // Receive the pointer. 44 | this._samplePtr = this._wasm.exports.alloc(this._sample_size) 45 | // Create the JS Array in that memory location. 46 | this._sampleBuf = new Float32Array ( 47 | this._wasm.exports.memory.buffer, 48 | this._samplePtr, 49 | this._sample_size 50 | ) 51 | 52 | // Copy the sample data to the shared space. 53 | this._sampleBuf.set(this._sample_data); 54 | 55 | // Tell the WebAssembly module which sample to use. 56 | this._wasm.exports.set_sample_data_raw(this._samplePtr, this._sample_size) 57 | 58 | // Flag for the processing method. 59 | this._sample_set = true; 60 | } 61 | 62 | // Same procedure as above, just for the impulse response. 63 | if(this._ir_data) { 64 | this._irPtr = this._wasm.exports.alloc(this._ir_size) 65 | this._irBuf = new Float32Array ( 66 | this._wasm.exports.memory.buffer, 67 | this._irPtr, 68 | this._ir_size 69 | ) 70 | 71 | this._irBuf.set(this._ir_data); 72 | this._wasm.exports.set_ir_data_raw(this._irPtr, this._ir_size) 73 | this._ir_set = true; 74 | } 75 | 76 | // And once more, for the output buffers. 77 | this._outPtr_r = this._wasm.exports.alloc(this._size) 78 | this._outBuf_r = new Float32Array ( 79 | this._wasm.exports.memory.buffer, 80 | this._outPtr_r, 81 | this._size 82 | ) 83 | this._outPtr_l = this._wasm.exports.alloc(this._size) 84 | this._outBuf_l = new Float32Array ( 85 | this._wasm.exports.memory.buffer, 86 | this._outPtr_l, 87 | this._size 88 | ) 89 | }) 90 | } else if (e.data.type === 'loadSample') { 91 | // Load the sample date from binary data received over the message port. 92 | this._sample_size = e.data.length 93 | this._sample_data = e.data.samples 94 | console.log("sample size: " + this._sample_data.length); 95 | 96 | // If the WebAssembly module is already loaded (the order isn't necessarily 97 | // deterministic), allocate buffer space (see explanation above). 98 | if(this._wasm) { 99 | this._samplePtr = this._wasm.exports.alloc(this._sample_size) 100 | this._sampleBuf = new Float32Array ( 101 | this._wasm.exports.memory.buffer, 102 | this._samplePtr, 103 | this._sample_size 104 | ) 105 | // copy to wasm buffer 106 | this._sampleBuf.set(this._sample_data); 107 | // load to loop player 108 | this._wasm.exports.set_sample_data_raw(this._samplePtr, this._sample_size) 109 | this._sample_set = true; 110 | } 111 | } else if (e.data.type === 'loadIr') { 112 | // Load the sample date from binary data received over the message port. 113 | this._ir_size = e.data.length 114 | this._ir_data = e.data.samples 115 | console.log("ir size: " + this._ir_data.length); 116 | 117 | // If the WebAssembly module is already loaded (the order isn't necessarily 118 | // deterministic), allocate buffer space (see explanation above). 119 | if(this._wasm) { 120 | this._irPtr = this._wasm.exports.alloc(this._ir_size) 121 | this._irBuf = new Float32Array ( 122 | this._wasm.exports.memory.buffer, 123 | this._irPtr, 124 | this._ir_size 125 | ) 126 | // copy to wasm buffer 127 | this._irBuf.set(this._ir_data); 128 | // load to loop player 129 | this._wasm.exports.set_ir_data_raw(this._irPtr, this._ir_size) 130 | this._ir_set = true; 131 | } 132 | } else if (e.data.type === 'enable') { 133 | // Enable playing if WebAssembly module is enabled. 134 | if(this._wasm) { 135 | this._wasm.exports.enable(); 136 | } 137 | } else if (e.data.type === 'disable') { 138 | // Disable playing if WebAssembly module is enabled. 139 | if(this._wasm) { 140 | this._wasm.exports.disable(); 141 | } 142 | } 143 | } 144 | } 145 | 146 | process(inputs, outputs, parameters) { 147 | if (!this._wasm || !this._sample_set) { 148 | return true 149 | } 150 | 151 | // Set parameters 152 | this._wasm.exports.set_azimuth(parameters.azimuth[0]) 153 | this._wasm.exports.set_elevation(parameters.elevation[0]) 154 | // get the output buffer (DeInterleaved) 155 | let output = outputs[0]; 156 | 157 | // Process ... 158 | this._wasm.exports.process(this._outPtr_l, this._outPtr_r, this._size) 159 | 160 | // Set output buffers. 161 | output[0].set(this._outBuf_l) 162 | output[1].set(this._outBuf_r) 163 | 164 | return true 165 | } 166 | } 167 | 168 | // Register processor module. 169 | registerProcessor('loop-player-processor', LoopPlayerProcessor) 170 | -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python3 -m http.server 1234 4 | -------------------------------------------------------------------------------- /src/binaural_loop_player.rs: -------------------------------------------------------------------------------- 1 | pub mod loop_player; 2 | pub mod ambisonics; 3 | pub mod binauralizer; 4 | 5 | use loop_player::*; 6 | use ambisonics::*; 7 | use binauralizer::*; 8 | 9 | pub struct BinauralLoopPlayer { 10 | player: LoopPlayer, 11 | encoder: FoaEncoder, 12 | binauralizer: Binauralizer, 13 | azimuth: f32, 14 | elevation: f32, 15 | enabled: bool, 16 | } 17 | 18 | impl BinauralLoopPlayer { 19 | pub fn new() -> BinauralLoopPlayer { 20 | BinauralLoopPlayer { 21 | player: LoopPlayer::new(), 22 | encoder: FoaEncoder::new(), 23 | binauralizer: Binauralizer::new(), 24 | azimuth: 0.0, 25 | elevation: 0.0, 26 | enabled: false, 27 | } 28 | } 29 | 30 | pub fn enable(&mut self) { 31 | self.enabled = true; 32 | } 33 | 34 | pub fn disable(&mut self) { 35 | self.enabled = false; 36 | } 37 | 38 | pub fn process(&mut self, out_ptr_l: *mut f32, out_ptr_r: *mut f32, size: usize) { 39 | 40 | if self.enabled { 41 | let buf = self.player.get_next_block(); 42 | let buf_ambi = self.encoder.encode_block(&buf, self.azimuth, self.elevation); 43 | let buf_bin = self.binauralizer.binauralize(&buf_ambi); 44 | 45 | let out_buf_l: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(out_ptr_l, size)}; 46 | let out_buf_r: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(out_ptr_r, size)}; 47 | for i in 0..size { 48 | out_buf_l[i] = buf_bin[0][i] as f32; 49 | out_buf_r[i] = buf_bin[1][i] as f32; 50 | } 51 | } else { 52 | let out_buf_l: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(out_ptr_l, size)}; 53 | let out_buf_r: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(out_ptr_r, size)}; 54 | for i in 0..size { 55 | out_buf_l[i] = 0.0; 56 | out_buf_r[i] = 0.0; 57 | } 58 | } 59 | } 60 | 61 | pub fn set_sample_data_raw(&mut self, in_ptr: *mut f32, sample_size: usize) { 62 | let in_buf: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(in_ptr, sample_size)}; 63 | self.player.set_loop(&in_buf); 64 | } 65 | 66 | pub fn set_ir_data(&mut self, in_ptr: *mut f32, sample_size: usize) { 67 | let in_buf: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(in_ptr, sample_size)}; 68 | self.binauralizer.set_ir(&in_buf); 69 | } 70 | 71 | pub fn set_azimuth(&mut self, azi: f32) { 72 | self.azimuth = azi; 73 | } 74 | 75 | pub fn set_elevation(&mut self, ele: f32) { 76 | self.elevation = ele; 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/binaural_loop_player/ambisonics.rs: -------------------------------------------------------------------------------- 1 | /** 2 | * a simple first order ambisonics encoder 3 | */ 4 | pub struct FoaEncoder { 5 | a_1_0:f32, 6 | a_1_1:f32, 7 | } 8 | 9 | impl FoaEncoder { 10 | 11 | pub fn new() -> FoaEncoder { 12 | FoaEncoder { 13 | a_1_0: 1.0, 14 | a_1_1: 1.0, 15 | } 16 | } 17 | 18 | pub fn coefs(&self, azi: f32, ele: f32) -> [f32; 4] { 19 | let mut coefs = [0.0; 4]; 20 | 21 | let sin_a = azi.sin(); 22 | let cos_a = azi.cos(); 23 | let sin_e = ele.sin(); 24 | let cos_e = ele.cos(); 25 | 26 | coefs[0] = 1.0; 27 | coefs[1] = self.a_1_1 * sin_a * sin_e; 28 | coefs[2] = self.a_1_0 * cos_e; 29 | coefs[3] = self.a_1_1 * cos_a * sin_e; 30 | 31 | coefs 32 | } 33 | 34 | pub fn encode_block(&mut self, input: &[f32; 128], azi:f32, ele:f32) -> [[f32; 128]; 4] { 35 | let coefs = self.coefs(azi, ele); 36 | 37 | let mut enc_block = [[0.0; 128]; 4]; 38 | 39 | for i in 0..128 { 40 | 41 | enc_block[0][i] = input[i] * coefs[0]; 42 | enc_block[1][i] = input[i] * coefs[1]; 43 | enc_block[2][i] = input[i] * coefs[2]; 44 | enc_block[3][i] = input[i] * coefs[3]; 45 | } 46 | enc_block 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/binaural_loop_player/binauralizer.rs: -------------------------------------------------------------------------------- 1 | pub mod convolver; 2 | use convolver::*; 3 | /** 4 | * a simple first-order convolution binauralizer 5 | */ 6 | pub struct Binauralizer { 7 | left: Vec, 8 | right: Vec, 9 | } 10 | 11 | impl Binauralizer { 12 | 13 | // initialize with unit IRs 14 | pub fn new() -> Binauralizer { 15 | let mut ir = [0.0; 128]; 16 | ir[1] = 1.0; 17 | 18 | let left = vec![BlockConvolver::from_ir(&ir); 4]; 19 | let right = vec![BlockConvolver::from_ir(&ir); 4]; 20 | 21 | Binauralizer { 22 | left, 23 | right, 24 | } 25 | } 26 | 27 | fn to_array(sli: &[f32]) -> [f32; 128] { 28 | let mut arr = [0.0;128]; 29 | for i in 0..128 { 30 | arr[i] = sli[i]; 31 | } 32 | arr 33 | } 34 | 35 | pub fn set_ir(&mut self, ir: &[f32]) { 36 | self.left[0] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[0..128])); 37 | self.left[1] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[128..256])); 38 | self.left[2] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[256..384])); 39 | self.left[3] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[384..512])); 40 | self.right[0] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[512..640])); 41 | self.right[1] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[640..768])); 42 | self.right[2] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[768..896])); 43 | self.right[3] = BlockConvolver::from_ir(&Binauralizer::to_array(&ir[896..1024])); 44 | } 45 | 46 | pub fn binauralize(&mut self, input: &[[f32; 128]; 4]) -> [[f32; 128]; 2] { 47 | let mut bin_block = [[0.0; 128]; 2]; 48 | 49 | for ach in 0..4 { 50 | let lch = self.left[ach].convolve(&input[ach]); 51 | let rch = self.right[ach].convolve(&input[ach]); 52 | for fr in 0..128 { 53 | bin_block[0][fr] += lch[fr]; 54 | bin_block[1][fr] += rch[fr]; 55 | } 56 | } 57 | 58 | bin_block 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/binaural_loop_player/binauralizer/convolver.rs: -------------------------------------------------------------------------------- 1 | extern crate chfft; 2 | extern crate num; 3 | 4 | use num::complex::Complex; 5 | use chfft::RFft1D; 6 | 7 | /** 8 | * A simple, non-partitioned block convolver. 9 | * Uses the Overlap-Save method (also called Overlap-Scrap) for block convolution. 10 | */ 11 | pub struct BlockConvolver { 12 | ir_freq_domain: Vec>, 13 | in_freq_domain: Vec>, 14 | fft: RFft1D, 15 | tmp_in: Vec, 16 | tmp_out: Vec, 17 | remainder: Vec, 18 | len: usize, 19 | } 20 | 21 | impl std::clone::Clone for BlockConvolver { 22 | 23 | fn clone(&self) -> Self { 24 | let fft = RFft1D::::new(self.len); 25 | 26 | BlockConvolver { 27 | ir_freq_domain: self.ir_freq_domain.clone(), 28 | in_freq_domain: self.in_freq_domain.clone(), 29 | fft: fft, 30 | tmp_in: vec![0.0; 256], 31 | tmp_out: vec![0.0; 256], 32 | remainder: vec![0.0; 128], 33 | len: self.len, 34 | } 35 | } 36 | } 37 | 38 | impl BlockConvolver { 39 | 40 | // create block convolver from ir 41 | pub fn from_ir(ir: &[f32; 128]) -> BlockConvolver { 42 | 43 | let mut fft = RFft1D::::new(ir.len() * 2); 44 | 45 | // zero-pad impulse response (to match IR lenght) 46 | let mut ir_zeropad = [0.0; 256]; 47 | for i in 0..128 { 48 | ir_zeropad[i] = ir[i]; 49 | } 50 | 51 | BlockConvolver { 52 | ir_freq_domain: fft.forward(&ir_zeropad), 53 | in_freq_domain: vec![Complex::new(0.0,0.0); ir.len() * 2], 54 | fft: fft, 55 | tmp_in: vec![0.0; 256], 56 | tmp_out: vec![0.0; 256], 57 | remainder: vec![0.0; 128], 58 | len: ir.len() * 2, 59 | } 60 | } 61 | 62 | pub fn convolve(&mut self, input: &[f32; 128]) -> [f32; 128] { 63 | 64 | // assemble input block from remainder part from previous block 65 | // (in this case, as filter length is always equal to blocksize, 66 | // the remainder is just the previous block) 67 | for i in 0..128 { 68 | self.tmp_in[i] = self.remainder[i]; 69 | self.tmp_in[i + 128] = input[i]; 70 | } 71 | 72 | // perform fft 73 | self.in_freq_domain = self.fft.forward(&self.tmp_in); 74 | 75 | // pointwise convolution 76 | for i in 0..self.in_freq_domain.len(){ 77 | self.in_freq_domain[i] = self.ir_freq_domain[i] * self.in_freq_domain[i]; 78 | } 79 | 80 | // ifft 81 | self.tmp_out = self.fft.backward(&self.in_freq_domain); 82 | 83 | // copy relevant part from ifft, scrap the rest 84 | let mut outarr = [0.0; 128]; 85 | for i in 0..128 { 86 | self.remainder[i] = input[i]; 87 | outarr[i] = self.tmp_out[i + 128]; 88 | } 89 | 90 | // return result block ... 91 | outarr 92 | } 93 | } 94 | 95 | pub struct TimeDomainConvolver { 96 | ir: [f32; 128], 97 | delay: [f32; 256], 98 | delay_idx: usize, 99 | } 100 | 101 | impl std::clone::Clone for TimeDomainConvolver { 102 | 103 | fn clone(&self) -> Self { 104 | let mut ir = [0.0; 128]; 105 | let mut delay = [0.0; 256]; 106 | 107 | for i in 0..128 { 108 | ir[i] = self.ir[i]; 109 | delay[i] = self.delay[i]; 110 | } 111 | 112 | for i in 128..256 { 113 | delay[i] = self.delay[i]; 114 | } 115 | 116 | TimeDomainConvolver { 117 | ir, 118 | delay, 119 | delay_idx: self.delay_idx 120 | } 121 | } 122 | } 123 | 124 | impl TimeDomainConvolver { 125 | pub fn from_ir(ir: &[f32; 128]) -> TimeDomainConvolver { 126 | let mut n_ir = [0.0; 128]; 127 | for i in 0..128 { 128 | n_ir[i] = ir[i]; 129 | } 130 | 131 | TimeDomainConvolver { 132 | ir: n_ir, 133 | delay: [0.0; 256], 134 | delay_idx: 128, 135 | } 136 | } 137 | 138 | pub fn convolve(&mut self, input: &[f32; 128]) -> [f32; 128] { 139 | let mut out = [0.0; 128]; 140 | 141 | for k in 0..128 { 142 | self.delay[self.delay_idx + k] = input[k]; 143 | } 144 | 145 | if self.delay_idx == 0 { 146 | for k in 0..128 { 147 | for i in 0..128 { 148 | let mut idx; 149 | if i > k { 150 | idx = 256 - (i - k); 151 | } else { 152 | idx = k - i; 153 | } 154 | out[k] += self.ir[i] * self.delay[idx]; 155 | } 156 | } 157 | self.delay_idx = 128; 158 | } else if self.delay_idx == 128 { 159 | for k in 0..128 { 160 | for i in 0..128 { 161 | out[k] += self.ir[i] * self.delay[(self.delay_idx + k) - i]; 162 | } 163 | } 164 | self.delay_idx = 0; 165 | } 166 | 167 | out 168 | } 169 | } 170 | 171 | mod tests { 172 | // Note this useful idiom: importing names from outer (for mod tests) scope. 173 | use super::*; 174 | use std::f32::consts::PI; 175 | 176 | #[test] 177 | fn test_freq_domain_impulse_convolution() { 178 | // test convolution with impulse ... 179 | let mut ir = [0.0; 128]; 180 | ir[0] = 1.0; 181 | 182 | let mut signal_in = [0.0; 128]; 183 | let mut signal_out = [0.0; 128]; 184 | 185 | let mut conv = BlockConvolver::from_ir(&ir); 186 | 187 | let mut dev_accum = 0.0; 188 | 189 | for b in 0.. 100 { 190 | for i in 0..128 { 191 | let pi_idx = ((b * 128 + i) as f32) * PI; 192 | signal_in[i] = ((220.0 / 44100.0) * pi_idx).sin(); 193 | signal_in[i] += ((432.0 / 44100.0) * pi_idx).sin(); 194 | signal_in[i] += ((648.0 / 44100.0) * pi_idx).sin(); 195 | } 196 | let mut signal_out = conv.convolve(&signal_in); 197 | for i in 0..128 { 198 | dev_accum += (signal_out[i] - signal_in[i]) * (signal_out[i] - signal_in[i]); 199 | } 200 | } 201 | 202 | assert_approx_eq::assert_approx_eq!(dev_accum / (100.0 * 128.0) , 0.0, 0.00001); 203 | } 204 | } 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/binaural_loop_player/loop_player.rs: -------------------------------------------------------------------------------- 1 | /** 2 | * a very simple loop player ... 3 | */ 4 | pub struct LoopPlayer { 5 | index: usize, 6 | loop_buffer: Vec, 7 | } 8 | 9 | impl LoopPlayer { 10 | 11 | pub fn new() -> LoopPlayer { 12 | LoopPlayer { 13 | index: 0, 14 | loop_buffer: vec![0.0; 128], 15 | } 16 | } 17 | 18 | pub fn get_next_block(&mut self) -> [f32; 128] { 19 | let mut out_buf: [f32; 128] = [0.0; 128]; 20 | 21 | for i in 0..128 { 22 | out_buf[i] = self.loop_buffer[self.index]; 23 | 24 | if (self.index + 1) < self.loop_buffer.len() { 25 | self.index = self.index + 1; 26 | } else { 27 | self.index = 0; 28 | } 29 | } 30 | out_buf 31 | } 32 | 33 | pub fn set_loop(&mut self, samples:&[f32]) { 34 | self.loop_buffer.resize(samples.len(), 0.0); 35 | self.index = 0; 36 | for i in 0..samples.len() { 37 | self.loop_buffer[i] = samples[i] as f32; 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | use std::sync::Mutex; 5 | 6 | #[cfg(feature = "wee_alloc")] 7 | #[global_allocator] 8 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 9 | 10 | #[no_mangle] 11 | pub extern "C" fn alloc(size: usize) -> *mut f32 { 12 | let vec: Vec = vec![0.0; size]; 13 | Box::into_raw(vec.into_boxed_slice()) as *mut f32 14 | } 15 | 16 | mod binaural_loop_player; 17 | 18 | lazy_static! { 19 | static ref LOOPER: Mutex = Mutex::new(binaural_loop_player::BinauralLoopPlayer::new()); 20 | } 21 | 22 | #[no_mangle] 23 | pub extern "C" fn process(out_ptr_l: *mut f32, out_ptr_r: *mut f32, size: usize) { 24 | let mut looper = LOOPER.lock().unwrap(); 25 | looper.process(out_ptr_l, out_ptr_r, size); 26 | } 27 | 28 | #[no_mangle] 29 | pub extern "C" fn set_sample_data_raw(sample_ptr: *mut f32, sample_size: usize) { 30 | let mut looper = LOOPER.lock().unwrap(); 31 | looper.set_sample_data_raw(sample_ptr, sample_size); 32 | } 33 | 34 | #[no_mangle] 35 | pub extern "C" fn set_ir_data_raw(sample_ptr: *mut f32, sample_size: usize) { 36 | let mut looper = LOOPER.lock().unwrap(); 37 | looper.set_ir_data(sample_ptr, sample_size); 38 | } 39 | 40 | #[no_mangle] 41 | pub extern "C" fn set_azimuth(azi: f32) { 42 | let mut looper = LOOPER.lock().unwrap(); 43 | looper.set_azimuth(azi); 44 | } 45 | 46 | #[no_mangle] 47 | pub extern "C" fn set_elevation(ele: f32) { 48 | let mut looper = LOOPER.lock().unwrap(); 49 | looper.set_elevation(ele); 50 | } 51 | 52 | #[no_mangle] 53 | pub extern "C" fn enable() { 54 | let mut looper = LOOPER.lock().unwrap(); 55 | looper.enable(); 56 | } 57 | 58 | #[no_mangle] 59 | pub extern "C" fn disable() { 60 | let mut looper = LOOPER.lock().unwrap(); 61 | looper.disable(); 62 | } 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /wasm/wasm_loop_player.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-drunk-coder/wasm-loop-player/a91da8752fc34e287e63de4d71bc9d9aa025152a/wasm/wasm_loop_player.wasm --------------------------------------------------------------------------------