-
Notifications
You must be signed in to change notification settings - Fork 242
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
A fix for #207 — Signal Generator distorts over long periods of time #649
Conversation
I think at large values of |
@dvdsk also floats' precision is relative while period is fixed, hence generator timing precision will drop gradually over time. I see wrapping may only be a problem with rather small periods (with frequencies that are close to the sample rate). Rounding error there may become comparable to sampling period. That means that rounded remainder may introduce slight timing error once per signal period. |
|
listing all related issues/pr's (so we can close em once this is done) |
Current approach using sample counter will not work. You have to switch to adding an increment.
thus every sample advances we advance (1/sample_rate) * (1/period) of a full cycle (between 0 and 1) Example:
gives: cycle_increment = freq/sample_rate * 2pi which is what #202 uses. To prevent |
It did not occur to me that floats support modulo too. Yes, float increment is better, since wrap-around will be calculated exactly. As I understand this requires to change So the implementation could: fn next(&mut self) {
let sample = self.f(self.t); // t is cycle_pos
self.t += self.dt;
if self.t >= 1.0 { self.t -= 1.0; } // or self.t = self.t.fract() or t %= 1.0
sample
} Then, fn render(t: f32) -> f32 {
(t * 32::consts::TAU).sin()
} This limits functions to be periodic, but it looks like fancier versions are not anticipated. Regarding API, I am not sure it makes sense to do |
Making the function generic makes it a chore to pass the source around. Any struct the source is stored in, or function its passed too now must be generic too. Unless you use Box, but that is slower then the match that is there now.
Are there enough other functions that you could want be passed in for this to make sense? If we miss a few then adding them to signal-generator makes more sense to me. Since that does not require the code to become more complex. |
We could implement the function as a Could then also add a second initializer to the struct that would allow a client to write their own |
wouldn't that make the Signalgenerator generic over the function? Thats my main issue the custom function approach. Having generics in Source could become annoying for end users. |
No, because the #[derive(Clone, Debug)]
pub struct SignalGenerator {
sample_rate: cpal::SampleRate,
period: f32,
function: fn(f32) -> f32, //this was Function,
t: f32, // this was i: u32
} Then in the initializer we set |
ahh yes I forgot fn is a function pointer. Wonder what that does to perf, we shall see. Given how fast signal generator is already I do not think it matters at all. Kinda fun since you can throw f32::sin in there if you want a sine wave :) |
Having a lot of cruft in type parameters is a price for not having User-provided function could be a wavetable synth? :) It does not have to be some analytically simple stateless function. But if making this generator flexible requires too much of a hassle its not worth it unless there is actual need for that. If only a plain function is supported does it mean it cannot be a closure? A plain (stateless) |
in order of my personal preference we can have
|
Separate sources will have some duplicating code. But likely those can be extracted into helper functions. I would not object this version. We can have other implementations later when needed. |
Thanks all, let me get to work on this and I'll post changes. Per @dvdsk 's note I'll continue to implement (2) but it's easy to add interfaces along the lines of (1), either with some boilerplate or maybe a macro if we're being really nerdy. |
SignalGenerator's internal time variable is now a f32 and will wrap to prevent loss of precision over long-running sessions.
Added SawtoothWave, SquareWave and TriangleWave
This addresses the guts of everything discussed here I think:
|
A typo I left when I made the new generators, also clarified "rate" to "sample rate" just so people didn't think we were talking about cycle frequency.
Also passed this implementation through to `TriangleWave`, `SquareWave` etc.
very clean nice 👍 I guess the little artifacts come from resampling or the Fourier itself? |
Also elaborated documentation, changed some param names.
You know what? When I make a test signal with iZotope or Pro Tools it's perfect, I'm only getting from the signal generated by SignalGenerator. I'm not sure exactly what's going on here. |
let me know once this is ready (I think it still needs a note in the change-log it supports a custom function now?, or am I reading the source wrong?) |
And made the type `pub`
Okay all ready I think. |
very nice! thanks for all the hard work! |
Always a pleasure and very educational! |
Since pub fn sawtooth(frequency: f32, sample_rate: u32) -> SignalGenerator {
SignalGenerator::with_function(
cpal::SampleRate(sample_rate), frequency, sawtooth_signal)
} OK, can refine this later... |
your right, its a little bit more complex then it needs to be. Feel free to address that directly on main. edit: (main is still called master on this repo) |
Yeah I didn't do this because it'd be a breaking change to remove the enum but please feel free if you want. |
I might have misunderstood @PetrGlad so let me clarify my stance. I agree the individual sources (structs) Sine, Sawtooth and Square may use |
This fix changes
SignalGenerator
such that its internal time value to wrap-around for every period of the generated waveform. This requires more testing but is posted here for visibility.(This PR was posted just before and closed due to an error by me, I was basing off the wrong HEAD.)