I'm seeing some unexpected FM behavior in both Rodio 0.20.1 and 0.21.0
Below is my main.rs
demonstrating the problem. When DEMONSTRATING_DEFECT
is false
, you hear what you would expect, a siren-like sound alternating between two frequencies.
When DEMONSTRATING_DEFECT
is true
, you would expect smooth alternation between one frequency and the next. Instead, the high frequency gets higher and the low frequency seems to get lower...
use std::{f32::consts::PI, io::stdin};
use rodio::{OutputStream, OutputStreamBuilder, Source};
const SAMPLE_RATE: u32 = 44_100;
struct FrequencyModulation {
sample_num: u32,
frequency: Box<dyn Fn(f32) -> f32 + Send + 'static>,
}
fn amplitude(time_in_secs: f32, hz: f32) -> f32 /* (-1.0, 1.0) */ {
(2.0 * PI * hz * time_in_secs).sin()
}
fn time(sample_num: u32) -> f32 {
sample_num as f32 / SAMPLE_RATE as f32
}
impl FrequencyModulation {
pub fn new(frequency: Box<dyn Fn(f32) -> f32 + Send + 'static>) -> FrequencyModulation {
FrequencyModulation { sample_num: 0, frequency }
}
}
impl Source for FrequencyModulation {
fn current_span_len(&self) -> Option<usize> { None }
fn channels(&self) -> u16 { 1 }
fn sample_rate(&self) -> u32 { SAMPLE_RATE }
fn total_duration(&self) -> Option<std::time::Duration> { None }
}
impl Iterator for FrequencyModulation {
type Item = f32;
fn next(&mut self) -> Option<Self::Item> {
let time_in_secs = time(self.sample_num);
let instantaneous_frequency = (self.frequency)(time_in_secs);
let sample = amplitude(time_in_secs, instantaneous_frequency);
self.sample_num = self.sample_num + 1;
Some(sample)
}
}
const DEMONSTRATING_DEFECT: bool = true;
fn main() {
let stream_handle = OutputStreamBuilder::open_default_stream().unwrap();
let freq = if DEMONSTRATING_DEFECT {
// behaves unexpectedly with high rising and low lowering
|t: f32| 440.0 + 50.0 * (t * 2.0 * PI).sin()
} else {
// behaves as expected with siren-like alternating pitch
|t: f32| 440.0 + if (t * 2.0 * PI).sin() > 0.0 { 50.0 } else { -50.0 }
};
let source = FrequencyModulation::new(Box::new(freq));
let _ = stream_handle.mixer().add(source);
stdin().read_line(&mut String::new()).unwrap();
}
Your problem is that you calculate the amplitude on the global timeframe. In other words it is calculated irrespective of the previous sample and the previous phase angle of the signal, as if the frequency has always been the instantaneous one.
As a result every time the frequency changes the phase of your sine wave shifts which leads to the sound artifact you hear. When the frequency changes in big steps you only have one transition every now and then, so it's not a noticable problem, but with frequent or a contiuous frequency change those changes in the phase affect the audible signal.
To remedy it you can simply keep track of the current phase, calculate a phase delta for each time step and then derive the amplitude from the caculated phase:
use std::f32::consts::TAU;
struct FrequencyModulation {
sample_num: u32,
phase: f32,
frequency: Box<dyn Fn(f32) -> f32 + Send + 'static>,
}
impl FrequencyModulation {
pub fn new(frequency: Box<dyn Fn(f32) -> f32 + Send + 'static>) -> FrequencyModulation {
FrequencyModulation {
sample_num: 0,
phase: 0.0,
frequency,
}
}
fn next_amplitude(&mut self, freq: f32) -> f32 {
let phase_change = freq / SAMPLE_RATE as f32 * TAU;
self.phase += phase_change;
self.phase %= TAU; // keep `self.phase` within [0, TAU) for better precision
self.phase.sin()
}
}
impl Iterator for FrequencyModulation {
type Item = f32;
fn next(&mut self) -> Option<Self::Item> {
let time_in_secs = time(self.sample_num);
let instantaneous_frequency = (self.frequency)(time_in_secs);
let sample = self.next_amplitude(instantaneous_frequency);
self.sample_num = self.sample_num + 1;
Some(sample)
}
}