audiodevlog 003: Writing generators for Disperse
-
Categories:
- Audio software
I started writing this post mere seconds after pushing the latest commit of the day to the Disperse repository, so that all things I have learned are still fresh in memory.
The first piece of code I committed was a white noise generator
function, which simply returned a random f32 value
between -1.0 and 1.0. I then made it into a stateful
struct, so it could reuse the thread-local generator for
improved performance in returning random values. Its function
get_next_value returns the next random
value from the already initialized random number generator.
// white_noise.rs use ; use crateGenerator;
Then, I built a Sine generator, which 'ticked' its phase
to the next value (dependent on its frequency), so the next call to
get_next_value neatly returns the next
value in line. A more functional approach would be to make the phase
part of the caller, but the entire point of theses stateful generator
structs is to simplify writing and using generators. Moreover, a
typical synthesizer has stateful oscillators, where an incoming MIDI
note may or may not 'retrigger' the oscillator back to phase 0.
That's when I started noticing a pattern among the two generators (the
get_next_value function) and extracted it
into a Generator trait, which should be implemented by
any generator.
// sine.rs (partial)
After that, I followed up with a Triangle and a
Sawtooth generator. All generators are covered by unit
tests, except for the white noise one, because I can't think of a way
to reliably test random data.
Mathematics become much simpler and more correct when minimizing usage of π in calculations
The number π is a pretty complex number, and performing
float calculations mostly leads to inaccuracies. I doubt
that anyone would hear the difference between an audio output level of
0.25 or 0.2499999, but if there's some calculation ordering or
simplifications I can do to make it more accurate (without sacrificing
performance), I went with it.
Also, I started out calculating the current phase of a generator over a range of [0, 2π), but that meant I had to use calculations with π in conditional expressions (e.g. for a triangle generator, where the waveform increases to 1 over [0, π/2), decreases to -1 over [π/2, 3π/2) and returns to 0 towards 2π). That meant the value at exactly π/2 was sometimes exactly equal to 1, and sometimes it was not.
The resulting implementation of the Generator trait was
much more satisfying than before:
// triangle.rs (partial)
The unit tests for the triangle generator, that I copied from the sine generator, also became much better. Before, I had to use a certain tolerance for output inaccuracies, because the values could deviate by 0.000001.
// Before // After