rodio icon indicating copy to clipboard operation
rodio copied to clipboard

Add noise generators and improve their distribution

Open roderickvd opened this issue 6 months ago • 4 comments

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.

roderickvd avatar Jun 13 '25 21:06 roderickvd

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(())
    }
}

yara-blue avatar Jun 14 '25 15:06 yara-blue

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.

roderickvd avatar Jun 14 '25 22:06 roderickvd

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.

yara-blue avatar Jun 14 '25 22:06 yara-blue

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.

roderickvd avatar Jun 17 '25 19:06 roderickvd

Thanks for all the hard work, it looks really nice.

Only thing I'm still unsure about is the seeking behavior. You've explained you want seeking to be reproducible/deterministic. I think we currently use seeking mostly for decoders. Effects just forward the seek or adjust it in the case of speed. Seeking in decoders is not 100% accurate but we find that okay. Isn't it strange to then disable seeking as soon as a piece of audio gets mixed with noise?

yara-blue avatar Jul 04 '25 20:07 yara-blue

Isn't it strange to then disable seeking as soon as a piece of audio gets mixed with noise?

[emphasis mine]

Are you referring to Mix specifically? Because Mix::try_seek currently is always non-seekable regardless of its underlying sources.

But yes, you're probably right to not be too academic about this. It's probably OK to "eat" the seek and always return Ok(()) as any generator will continue to produce continuous noise as long as we don't reset the state.

roderickvd avatar Jul 04 '25 20:07 roderickvd

Are you referring to Mix specifically? Because Mix::try_seek currently is always non-seekable regardless of its underlying sources.

your right! In my head that was already fixed. Well we should be able to fix that now that we have TrackPosition.

yara-blue avatar Jul 04 '25 20:07 yara-blue

OK, a952f97 should do it, making all noise generators seekable. Once you agree, I can squash merge.

roderickvd avatar Jul 04 '25 20:07 roderickvd

I think this epic is now done :)

Lets get it merged! I'll see if I can finish up get 0.21 ready for release tomorrow.

yara-blue avatar Jul 04 '25 20:07 yara-blue