Skip to content

Add noise generators and improve their distribution #755

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
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

roderickvd
Copy link
Collaborator

@roderickvd roderickvd commented Jun 13, 2025

Fixes

The current WhiteNoise generator has a problem where its distribution is not at all in range of [-1.0, 1.0] due to precision loss, truncation and bias to zero from doing this:

let rand = self.rng.next_u32() as f32 / u32::MAX as f32;
let scaled = rand * 2.0 - 1.0;

This PR fixes that by using a proper f32 generated uniform distribution.

Then the current PinkNoise builds on top of the incorrect WhiteNoise but worse: has coefficients that are valid for 44.1 kHz only. This PR fixes that by algorithmically generating proper pink noise.

Finally, try_seek should provide deterministic seeking (same results after seeking) and PinkNoise cannot provide that. This PR correctly implements try_seek for noise generators that are deterministic (or uniformly random).

New noise generators

Threw in some freebies:

  • Gaussian white noise
  • Triangular white noise
  • Blue noise
  • Pink noise
  • Brownian noise
  • Velvet noise

Including documentation on when you'd want to use which.

I noticed that docs.rs didn't document the feature-gated noise generators, so I've set it to generate documentation for all features.

Advanced construction options

The current API remains as-is: source::white() will give you a WhiteGenerator<SmallRng>. But you see that I made the generators generic for more choice to the user: source::WhiteNoise::<R: Rng>::new{_with_seed}().

This now provides a rather complete suite of high-quality noise generators for audio synthesis, testing, and dithering.

@dvdsk
Copy link
Collaborator

dvdsk commented Jun 14, 2025

Finally, try_seek should provide deterministic seeking (same results after seeking)

should it? I've never seen seek as reproducing something rather navigating in an underlying stream. I would say the seek implementation for noise like this should do nothing. But if we do go on with seek in noise -> error then we should provide a wrapper that "eats" the seek operation. Basically:

struct DisableSeek;

impl Source for DisableSeek {
    ....
    
    fn try_seek() -> Result<() , _> {
        Ok(())
    }
}

Copy link
Collaborator

@dvdsk dvdsk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not have time to look at the implementation in depth I do have some feedback/questions regarding the API and tests.

@@ -87,7 +87,11 @@ mod zero;
#[cfg(feature = "noise")]
mod noise;
#[cfg(feature = "noise")]
pub use self::noise::{pink, white, PinkNoise, WhiteNoise};
pub use self::noise::{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider making the noise module public in source. Then the end user could access them like:

use rodio::source::noise;

let a = noise::brownian(); 

The advantage is that it makes it possible to have the word noise next to the noise generator. Alternative would be naming them all: <color>_noise.

If we do make the noise folder public we could also remove the Noise suffix from the structs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I'll do that.

}

#[test]
fn test_pink_noise_with_seed_deterministic() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the equality tests can be de-duplicated using rstest by making the functions generic over the noise kind.

See the seek.rs tests for how rstest works

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll clean up the tests.

}

#[test]
fn test_convenience_functions_work() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this tests anything not already tested by the above or verified by the compiler.

}

#[test]
fn test_source_trait_properties() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, there's not really any complex logic that leads to these values. (They are hard-coded in the two macros).

}

#[test]
fn test_different_seeds_produce_different_outputs() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels more like a unit test to me. Though one could argue this test prevents a regression I cant really imagine that happening. I think the compilers unused variable warnings would catch that right?

//! let triangular = triangular_white(44100); // For TPDF dithering
//! let blue = blue(44100); // For high-passed dithering applications
//!
//! // Advanced: create with custom RNG (useful for deterministic output)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the use-case for bringing a custom RNG? Is it reproducibility in future versions, or performance vs quality?

Having these generic adds a small amount of friction to the API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As developer of downstream apps and libraries myself, I often have random generators in my own code. To create message or session UUIDs for example. Then, I want to keep it clean and have my entire codebase draw from that single generator. I dislike the bloat and redundancy of dependencies pulling in different Rngs, increasing binary size and memory usage.

I believe the slight complexity in the API is OK, by keeping the simple white, pink, etc. functions that hide this from the user who is either novice or doesn't care.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the bloat and redundancy of dependencies pulling in different Rngs, increasing binary size and memory usage.

I had no idea the increase was that big. I would think a RNG uses maybe a kB (for both). Especially if we use a small fast RNG.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may be right that it's mostly a mindset thing.

w.r.t. various RNGs: rand's default Rng is ThreadRng and at some point SmallRng was behind a feature gate. Then there are a couple more that users can experiment with.

We can still have best of both worlds:

impl WhiteNoise<SmallRng> {
    pub fn new(sample_rate: SampleRate) -> Self {
        Self::new_with_rng(sample_rate, SmallRng::from_os_rng())
    }
}

impl<R: Rng + SeedableRng> NoiseGenerator<R> for WhiteNoise<R> {
    fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self {
        // ...
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then there are a couple more that users can experiment with.

What is the effect of different rng's on the noise?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audibly nothing. Just pointing out that there's more than one or two Rngs out there that users care for because of speed/uniformity/academia/fun/belief/whatever, and not difficult to support with both new and new_with_rng.

/// use rodio::source::white;
/// let white_noise = white(44100); // 44.1kHz sample rate
/// ```
pub fn white(sample_rate: SampleRate) -> WhiteNoise<SmallRng> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we want to continue having these factories. Maybe we should just have the structs with a new method each. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no particular preference. The current way doesn't break the API, is not super idiomatic, but also not super un-idiomatic. We could also keep just the existing white and pink ones and tag them as deprecated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SignalGenerator uses the struct::new instead of factory so it would align with those. I like the idea of keep white and pink and deprecating them. We can throw them out later.

/// Trait providing common constructor patterns for noise generators.
/// Provides default implementations for `new()` and `new_with_seed()`
/// that delegate to `new_with_rng()`.
pub trait NoiseGenerator<R: Rng + SeedableRng> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a use-case for a unified API for noise generating? If not then a macro might be a better fit. Then the user does not need to have the trait in scope. That will be especially relevant if we decided to drop the factory functions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good point. I just did it for the purpose of DRY.

@roderickvd
Copy link
Collaborator Author

Finally, try_seek should provide deterministic seeking (same results after seeking)

should it? I've never seen seek as reproducing something rather navigating in an underlying stream. I would say the seek implementation for noise like this should do nothing. But if we do go on with seek in noise -> error then we should provide a wrapper that "eats" the seek operation.

Here is an example of what would happen if we would "eat" seeks:

// Create with a fixed seed for determinism
let mut noise = pink::new_with_seed(44100, 12345);
// Play for a while, changing internal state
for _ in 0..1000 { noise.next(); }
// Rewind to the beginning
noise.try_seek(Duration::from_secs(0)).unwrap();

// Create another generator with the same seed
let mut noise2 = pink::new_with_seed(44100, 12345);
// Noises should be the same, but are not
assert_eq!(noise.next(), noise2.next());

...where you've got me however is that this is also true for my proposed implementation of WhiteNoise. We can get around that by storing/resetting original seeds, but I'm not sure about the added value vs. added complexity.

Thinking out loud: maybe we should remove _with_seed altogether and prevent illusions of determinism.

@dvdsk
Copy link
Collaborator

dvdsk commented Jun 14, 2025

Thinking out loud: maybe we should remove _with_seed altogether and prevent illusions of determinism.

I think a deterministic noise gen is pretty neat but I struggle to think of a use case for it. Noise pretty much sounds the same regardless of seed right?

If we can not find a good use case now I would propose we scrap determinism. We can always add deterministic_noise later that goes through all kinds of complex slow tricks to stay perfectly deterministic.

@roderickvd
Copy link
Collaborator Author

If we can not find a good use case now I would propose we scrap determinism. We can always add deterministic_noise later that goes through all kinds of complex slow tricks to stay perfectly deterministic.

Yes, let's scrap it.

The commit improves and expands the noise generation capabilities by adding new
types and fixing distribution issues in existing ones. Key changes:

- Add more generators: Gaussian, triangular, blue, brownian, violet, velvet
- Fix white noise uniform distribution
- Fix pink noise sampling rate issues
- Make noise generators properly deterministic with seeds
- Add comprehensive tests for noise generator properties
- Improve documentation with detailed usage guidance

This provides a complete suite of high-quality noise generators for audio
synthesis, testing, and dithering applications.
- Rename noise generators to PascalCase types (e.g. WhiteUniform, Pink)
- Move all noise generators under `source::noise` module
- Deprecate `white()` and `pink()` in favor of `noise::WhiteUniform::new()` and `noise::Pink::new()`
- Remove legacy noise generator functions/types from prelude
- Update `rstest`, `rstest_reuse` to fix templates in unit tests
- Refactor and expand noise generator documentation and examples
- Move and consolidate noise generator tests into `src/source/noise.rs`
- Update `examples/noise_generator.rs` to use new API and add descriptions
@roderickvd roderickvd force-pushed the feat/more-and-better-noise branch from 94f0f53 to 377fbb9 Compare July 3, 2025 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants