-
-
Notifications
You must be signed in to change notification settings - Fork 21
Add Echo/Delay node #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
piedoom
wants to merge
10
commits into
BillyDM:main
Choose a base branch
from
piedoom:echo
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
6172e56
Added multichannel echo/delay node
piedoom 82fb41d
fixed minor issues with rebase
piedoom d428161
moved delayline to dsp and added sample delay/fractional delay methods
piedoom 86aa343
Added separate smooth seconds param for delay to avoid clicking and f…
piedoom beab10d
fixed nostd issues
piedoom 24ee20b
hopefully fixed phase discontinuity issue in delay_line
piedoom 6126465
added const defaults and types for mono and stereo echo, removed unne…
piedoom c00d519
added stereo and mono helper methods, improved docs
4afe6ed
consistent stereo echo naming with rest of crate
1c97001
fixed failing example
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| use core::num::NonZeroU32; | ||
|
|
||
| use bevy_platform::prelude::Vec; | ||
| #[cfg(not(feature = "std"))] | ||
| use num_traits::Float; | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct DelayLine { | ||
| buffer: Vec<f64>, | ||
| index: usize, | ||
| } | ||
|
|
||
| impl DelayLine { | ||
| pub fn new(length: usize) -> Self { | ||
| // No need to carry extra capacity around. | ||
| let mut buffer = Vec::new(); | ||
| buffer.reserve_exact(length); | ||
| buffer.extend(core::iter::repeat_n(0.0, length)); | ||
|
|
||
| Self { buffer, index: 0 } | ||
| } | ||
|
|
||
| /// Read the least recent sample pushed to this delay line (the sample that | ||
| /// will be replaced with the next [`Self::write_and_advance`]). | ||
| pub fn read_last(&self) -> f64 { | ||
| self.buffer[self.index] | ||
| } | ||
|
|
||
| pub fn read(&self, num_samples_delay: usize) -> Option<f64> { | ||
| let buffer_len = self.buffer.len(); | ||
|
|
||
| // Ensure that requested samples of delay are not greater than our capacity and that the number of samples of delay is not zero. | ||
| if buffer_len < num_samples_delay || num_samples_delay == 0 { | ||
| return None; | ||
| } | ||
|
|
||
| // Wrap the requested delay if necessary | ||
| let index = match num_samples_delay > self.index { | ||
| // Wrapping is needed - wrap from the end of the vec. | ||
| true => buffer_len - (num_samples_delay - self.index), | ||
| // No wrapping required - just subtract from the index. | ||
| false => self.index - num_samples_delay, | ||
| }; | ||
|
|
||
| // Our index must be in range | ||
| Some(self.buffer[index]) | ||
| } | ||
|
|
||
| /// Read a sample at some delay of samples. Fractional delays will linearly | ||
| /// interpolate between the two nearest samples. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// Returns the value of the delayed sample, if the delay samples is not | ||
| /// greater than the delay line capacity, in which case `None` is returned. | ||
| pub fn read_seconds(&self, seconds_delay: f32, sample_rate: NonZeroU32) -> Option<f64> { | ||
| // Get the number of samples to delay. This number may be fractional and | ||
| // will be interpolated. Add 1.0, as a delay of 0.0 is invalid and fractional delays | ||
| // will always start at at least 1.0. | ||
| let num_samples_delay_f = (seconds_delay * sample_rate.get() as f32) + 1f32; | ||
|
|
||
| let buffer_len = self.buffer.len(); | ||
|
|
||
| // Ensure the requested delay is within bounds | ||
| if buffer_len < num_samples_delay_f.ceil() as usize { | ||
| return None; | ||
| } | ||
|
|
||
| // Get the actual index of the delay, as a fraction | ||
| let mut index_f = self.index as f32 - num_samples_delay_f; | ||
|
|
||
| // If negative, wrap to the end of the buffer | ||
| if index_f.is_sign_negative() { | ||
| index_f = buffer_len as f32 - index_f.abs(); | ||
| } | ||
|
|
||
| // Find the two indices to interpolate between | ||
| let mut index_a = index_f.floor() as usize; | ||
| // Account for rounding errors | ||
| if index_a == buffer_len { | ||
| index_a -= 1; | ||
| } | ||
| let index_b = (index_a + 1) % buffer_len; | ||
|
|
||
| let sample_a = self.buffer[index_a]; | ||
| let sample_b = self.buffer[index_b]; | ||
|
|
||
| // Amount to interpolate | ||
| let fract = index_f.fract() as f64; | ||
|
|
||
| let mix_a = sample_a * (1.0 - fract); | ||
| let mix_b = sample_b * fract; | ||
|
|
||
| Some(mix_a + mix_b) | ||
| } | ||
|
|
||
| /// Overwrite the least recent sample. | ||
| pub fn write_and_advance(&mut self, value: f64) { | ||
| self.buffer[self.index] = value; | ||
|
|
||
| if self.index == self.buffer.len() - 1 { | ||
| self.index = 0; | ||
| } else { | ||
| self.index += 1; | ||
| } | ||
| } | ||
|
|
||
| pub fn reset(&mut self) { | ||
| self.buffer.fill(0.0); | ||
| } | ||
|
|
||
| pub fn resize(&mut self, size: usize) { | ||
| // little point in messing around with the exact | ||
| // capacity here | ||
| self.buffer.resize(size, 0.0); | ||
| self.index %= self.buffer.len(); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| macro_rules! delay_line_test { | ||
| ($name:ident, $length:expr) => { | ||
| #[test] | ||
| fn $name() { | ||
| let mut line = super::DelayLine::new($length); | ||
| for i in 0..$length { | ||
| assert_eq!(line.read_last(), 0.0); | ||
| line.write_and_advance(i as f64); | ||
| } | ||
| for i in 0..$length { | ||
| assert_eq!(line.read_last(), i as f64); | ||
| line.write_and_advance(0.0); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| delay_line_test!(length_1, 1); | ||
| delay_line_test!(length_3, 3); | ||
| delay_line_test!(length_10, 10); | ||
|
|
||
| #[test] | ||
| fn read_delay_line() { | ||
| let mut line = super::DelayLine::new(10); | ||
|
|
||
| // Write enough times to overwrite some old values | ||
| for i in 0..14 { | ||
| line.write_and_advance(i as f64); | ||
| } | ||
|
|
||
| // 10, 11, 12, 13, 4, 5, 6, 7, 8, 9 | ||
| // └ Index | ||
|
|
||
| assert_eq!(line.read_last(), 4.0); | ||
| // Read without wrapping | ||
| assert_eq!(line.read(1), Some(13.0)); | ||
| // Read with wrapping | ||
| assert_eq!(line.read(8), Some(6.0)); | ||
| // The index should be equal to the maximum delay | ||
| assert_eq!(line.read_last(), line.read(line.buffer.len()).unwrap()); | ||
| // Obtain nothing with invalid ranges | ||
| assert_eq!(line.read(0), None); | ||
| assert_eq!(line.read(11), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn read_delay_line_fractional() { | ||
| let mut line = super::DelayLine::new(10); | ||
|
|
||
| // Write enough times to overwrite some old values | ||
| for i in 0..14 { | ||
| line.write_and_advance(i as f64); | ||
| } | ||
|
|
||
| let sample_rate = core::num::NonZeroU32::new(1u32).unwrap(); | ||
|
|
||
| // 10, 11, 12, 13, 4, 5, 6, 7, 8, 9 | ||
| // │ └ Index | ||
| // └─── 0s | ||
|
|
||
| // Read without interpolation | ||
| assert_eq!(line.read_seconds(0.0, sample_rate), Some(13.0)); | ||
| // Read with interpolation, without wrapping | ||
| assert_eq!(line.read_seconds(1.5, sample_rate), Some(11.5)); | ||
| // Read with interpolation, with wrapping | ||
| assert_eq!(line.read_seconds(5.5, sample_rate), Some(7.5)); | ||
| // Obtain nothing with invalid ranges | ||
| assert_eq!(line.read_seconds(9.5, sample_rate), None); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all of this can use some bikeshedding, idk if these are correct terms