In audio synthesis, the sine wave is a fundamental element in many algorithms, sounds, and effects. A sine wave can be used as an LFO (low-frequency oscillator) to control various elements of a sound like its amplitude or frequency. Sine waves can also be used for additive synthesis, such as generating band-limited versions of saw, triangle, or square waves. Many audio processing applications are also real-time applications, so the computation needs to happen quickly and consistently.
While doing research into audio processing and functional reactive programming, I stumbled upon the work of Paul Hudak. His work on the The Haskell School of Music and Euterpea are inspiring. In his paper Audio Processing and Sound Synthesis in Haskell (2009) with Eric Cheng, a section is devoted to the optimizing of signal functions, sine waves in particular.
While the sample code in the paper is in Haskell, we're going to be working with Rust as it's what I use for audio processing.
§ Initial implementation
A straight-forward method for generating a sine wave is to use the standard sin
function. In order to generate a sine wave of the frequency
struct Sine {
n: usize,
frequency: f64,
fs: f64,
}
impl Sine {
fn step(&mut self) -> f64 {
let &mut Sine {
ref mut n,
frequency,
fs,
} = self;
let t = *n as f64 / fs;
let value = (2.0 * PI * frequency * t).sin();
*n += 1;
value
}
}
§ The Goertzel algorithm & fixed rate oscillators
The Goertzel algorithm is used to detect a single frequency/tone in a waveform. A common application of the algorithm can be seen in analog telephones where each button generates a unique tone and the Goertzel algorithm is used to detect from the tone which button was pressed. The Goertzel algorithm computes a value of
So, here's the algorithm:
In order to use it for our sine wave generator, we need to solve for an x and y that hold for all n. Using the identity
Therefore:
In order for the equation to hold for all n, we need the following to be true.
Solving for x and y yields
Now, assuming that the initial phase offset for our sine wave generator is 0 (
Hooray! Given that our sine wave generator has a fixed frequency,
§ Fast implementation
Now, let's turn this into code. We can break the above equation down a bit more to simplify the creation of a sine wave generator
We'll initialize our delay
lines with step
of the wave, we'll calculate the next sample s
, update the delay
lines, and return s
.
pub struct Sine {
c: f64,
z1: f64,
z2: f64,
}
impl Sine {
pub fn with_frequency(frequency: f64, fs: f64) -> Self {
let (s, c) = (TAU * frequency / fs).sin_cos();
Self {
c: 2. * c,
z1: 0.,
z2: s,
}
}
pub fn step(&mut self) -> f64 {
let Self { c, z1, z2 } = *self;
self.z2 = c.mul_add(z2, -z1);
self.z1 = z2;
z1
}
}
And there we have it!
§ References
- Eric Cheng and Paul Hudak. Audio Processing and Sound Synthesis in Haskell. 2009.