Skip to content

Commit

Permalink
lerp between previous and current parameter values
Browse files Browse the repository at this point in the history
this prevents discontinuities in some kinds of modulation where that matters, especially volume modulation

todo:
- test thoroughly
- make sure i've chosen the right parameters to lerp in the effects
  • Loading branch information
tesselode committed Dec 23, 2024
1 parent cb54874 commit 51374a5
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 123 deletions.
12 changes: 8 additions & 4 deletions crates/kira/src/effect/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ impl Effect for Compressor {
let attack_duration = self.attack_duration.value();
let release_duration = self.release_duration.value();

for frame in input {
let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
let makeup_gain = self.makeup_gain.interpolated_value(time_in_chunk);
let mix = self.mix.interpolated_value(time_in_chunk);

let input_decibels = [
20.0 * frame.left.abs().log10(),
20.0 * frame.right.abs().log10(),
Expand All @@ -104,14 +109,13 @@ impl Effect for Compressor {
.map(|envelope_follower| envelope_follower * ((1.0 / ratio) - 1.0));
let amplitude =
gain_reduction.map(|gain_reduction| 10.0f32.powf(gain_reduction / 20.0));
let makeup_gain_linear = 10.0f32.powf(self.makeup_gain.value().0 / 20.0);
let makeup_gain_linear = 10.0f32.powf(makeup_gain.0 / 20.0);
let output = Frame {
left: amplitude[0] * frame.left,
right: amplitude[1] * frame.right,
} * makeup_gain_linear;

let mix = self.mix.value().0;
*frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt()
*frame = output * mix.0.sqrt() + *frame * (1.0 - mix.0).sqrt()
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions crates/kira/src/effect/distortion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ impl Effect for Distortion {
fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) {
self.drive.update(dt * input.len() as f64, info);
self.mix.update(dt * input.len() as f64, info);
let drive = self.drive.value().as_amplitude();

for frame in input {
let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
let drive = self.drive.interpolated_value(time_in_chunk).as_amplitude();
let mix = self.mix.interpolated_value(time_in_chunk);

let mut output = *frame * drive;
output = match self.kind {
DistortionKind::HardClip => {
Expand All @@ -73,8 +77,7 @@ impl Effect for Distortion {
};
output /= drive;

let mix = self.mix.value().0;
*frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt()
*frame = output * mix.0.sqrt() + *frame * (1.0 - mix.0).sqrt()
}
}
}
Expand Down
134 changes: 71 additions & 63 deletions crates/kira/src/effect/eq_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,77 @@ impl EqFilter {
ic2eq: Frame::ZERO,
}
}
}

impl Effect for EqFilter {
fn on_start_processing(&mut self) {
if let Some(kind) = self.command_readers.set_kind.read() {
self.kind = kind;
}
read_commands_into_parameters!(self, frequency, gain, q);
}

fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) {
self.frequency.update(dt * input.len() as f64, info);
self.gain.update(dt * input.len() as f64, info);
self.q.update(dt * input.len() as f64, info);

let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
let frequency = self.frequency.interpolated_value(time_in_chunk);
let q = self.q.interpolated_value(time_in_chunk);
let gain = self.gain.interpolated_value(time_in_chunk);

let Coefficients {
a1,
a2,
a3,
m0,
m1,
m2,
} = Coefficients::calculate(self.kind, frequency, q, gain, dt);
let v3 = *frame - self.ic2eq;
let v1 = self.ic1eq * (a1 as f32) + v3 * (a2 as f32);
let v2 = self.ic2eq + self.ic1eq * (a2 as f32) + v3 * (a3 as f32);
self.ic1eq = v1 * 2.0 - self.ic1eq;
self.ic2eq = v2 * 2.0 - self.ic2eq;
*frame = *frame * (m0 as f32) + v1 * (m1 as f32) + v2 * (m2 as f32)
}
}
}

/// The shape of the frequency adjustment curve.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EqFilterKind {
/// Frequencies around the user-defined frequency are adjusted.
Bell,
/// Frequencies around and lower than the user-defined frequency are adjusted.
LowShelf,
/// Frequencies around and higher than the user-defined frequency are adjusted.
HighShelf,
}

struct Coefficients {
a1: f64,
a2: f64,
a3: f64,
m0: f64,
m1: f64,
m2: f64,
}

impl Coefficients {
#[must_use]
fn calculate_coefficients(&self, dt: f64) -> Coefficients {
fn calculate(kind: EqFilterKind, frequency: f64, q: f64, gain: Decibels, dt: f64) -> Self {
// In my testing, the filter goes unstable when the frequency exceeds half the sample rate,
// so I'm clamping this value to 0.5
let relative_frequency = (self.frequency.value() * dt).clamp(0.0, 0.5);
let q = self.q.value().max(MIN_Q);
match self.kind {
let relative_frequency = (frequency * dt).clamp(0.0, 0.5);
let q = q.max(MIN_Q);
match kind {
EqFilterKind::Bell => {
let a = 10.0f64.powf(self.gain.value().0 as f64 / 40.0);
let a = 10.0f64.powf(gain.0 as f64 / 40.0);
let g = (PI * relative_frequency).tan();
let k = 1.0 / (q * a);
let a1 = 1.0 / (1.0 + g * (g + k));
Expand All @@ -64,7 +125,7 @@ impl EqFilter {
let m0 = 1.0;
let m1 = k * (a * a - 1.0);
let m2 = 0.0;
Coefficients {
Self {
a1,
a2,
a3,
Expand All @@ -74,7 +135,7 @@ impl EqFilter {
}
}
EqFilterKind::LowShelf => {
let a = 10.0f64.powf(self.gain.value().0 as f64 / 40.0);
let a = 10.0f64.powf(gain.0 as f64 / 40.0);
let g = (PI * relative_frequency).tan() / a.sqrt();
let k = 1.0 / q;
let a1 = 1.0 / (1.0 + g * (g + k));
Expand All @@ -83,7 +144,7 @@ impl EqFilter {
let m0 = 1.0;
let m1 = k * (a - 1.0);
let m2 = a * a - 1.0;
Coefficients {
Self {
a1,
a2,
a3,
Expand All @@ -93,7 +154,7 @@ impl EqFilter {
}
}
EqFilterKind::HighShelf => {
let a = 10.0f64.powf(self.gain.value().0 as f64 / 40.0);
let a = 10.0f64.powf(gain.0 as f64 / 40.0);
let g = (PI * relative_frequency).tan() * a.sqrt();
let k = 1.0 / q;
let a1 = 1.0 / (1.0 + g * (g + k));
Expand All @@ -102,7 +163,7 @@ impl EqFilter {
let m0 = a * a;
let m1 = k * (1.0 - a) * a;
let m2 = 1.0 - a * a;
Coefficients {
Self {
a1,
a2,
a3,
Expand All @@ -115,59 +176,6 @@ impl EqFilter {
}
}

impl Effect for EqFilter {
fn on_start_processing(&mut self) {
if let Some(kind) = self.command_readers.set_kind.read() {
self.kind = kind;
}
read_commands_into_parameters!(self, frequency, gain, q);
}

fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) {
self.frequency.update(dt * input.len() as f64, info);
self.gain.update(dt * input.len() as f64, info);
self.q.update(dt * input.len() as f64, info);
let Coefficients {
a1,
a2,
a3,
m0,
m1,
m2,
} = self.calculate_coefficients(dt);

for frame in input {
let v3 = *frame - self.ic2eq;
let v1 = self.ic1eq * (a1 as f32) + v3 * (a2 as f32);
let v2 = self.ic2eq + self.ic1eq * (a2 as f32) + v3 * (a3 as f32);
self.ic1eq = v1 * 2.0 - self.ic1eq;
self.ic2eq = v2 * 2.0 - self.ic2eq;
*frame = *frame * (m0 as f32) + v1 * (m1 as f32) + v2 * (m2 as f32)
}
}
}

/// The shape of the frequency adjustment curve.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EqFilterKind {
/// Frequencies around the user-defined frequency are adjusted.
Bell,
/// Frequencies around and lower than the user-defined frequency are adjusted.
LowShelf,
/// Frequencies around and higher than the user-defined frequency are adjusted.
HighShelf,
}

struct Coefficients {
a1: f64,
a2: f64,
a3: f64,
m0: f64,
m1: f64,
m2: f64,
}

command_writers_and_readers! {
set_kind: EqFilterKind,
set_frequency: ValueChangeCommand<f64>,
Expand Down
24 changes: 16 additions & 8 deletions crates/kira/src/effect/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,23 @@ impl Effect for Filter {
self.cutoff.update(dt * input.len() as f64, info);
self.resonance.update(dt * input.len() as f64, info);
self.mix.update(dt * input.len() as f64, info);
let sample_rate = 1.0 / dt;
let g = (PI * (self.cutoff.value() / sample_rate)).tan();
let k = 2.0 - (1.9 * self.resonance.value().clamp(0.0, 1.0));
let a1 = 1.0 / (1.0 + (g * (g + k)));
let a2 = g * a1;
let a3 = g * a2;

for frame in input {
let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
let cutoff = self.cutoff.interpolated_value(time_in_chunk);
let resonance = self
.resonance
.interpolated_value(time_in_chunk)
.clamp(0.0, 1.0);
let mix = self.mix.interpolated_value(time_in_chunk).0;

let sample_rate = 1.0 / dt;
let g = (PI * (cutoff / sample_rate)).tan();
let k = 2.0 - (1.9 * resonance);
let a1 = 1.0 / (1.0 + (g * (g + k)));
let a2 = g * a1;
let a3 = g * a2;
let v3 = *frame - self.ic2eq;
let v1 = (self.ic1eq * (a1 as f32)) + (v3 * (a2 as f32));
let v2 = self.ic2eq + (self.ic1eq * (a2 as f32)) + (v3 * (a3 as f32));
Expand All @@ -92,7 +101,6 @@ impl Effect for Filter {
FilterMode::HighPass => *frame - v1 * (k as f32) - v2,
FilterMode::Notch => *frame - v1 * (k as f32),
};
let mix = self.mix.value().0;
*frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt()
}
}
Expand Down
6 changes: 4 additions & 2 deletions crates/kira/src/effect/panning_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ impl Effect for PanningControl {

fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) {
self.panning.update(dt * input.len() as f64, info);
for frame in input {
*frame = frame.panned(self.panning.value())
let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
*frame = frame.panned(self.panning.interpolated_value(time_in_chunk))
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions crates/kira/src/effect/reverb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,13 @@ impl Effect for Reverb {

let feedback = self.feedback.value() as f32;
let damping = self.damping.value() as f32;
let stereo_width = self.stereo_width.value() as f32;

for frame in input {
let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
let stereo_width = self.stereo_width.interpolated_value(time_in_chunk) as f32;
let mix = self.mix.interpolated_value(time_in_chunk).0;

let mut output = Frame::ZERO;
let mono_input = (frame.left + frame.right) * GAIN;
// accumulate comb filters in parallel
Expand All @@ -172,7 +176,6 @@ impl Effect for Reverb {
output.left * wet_1 + output.right * wet_2,
output.right * wet_1 + output.left * wet_2,
);
let mix = self.mix.value().0;
*frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt()
}
} else {
Expand Down
6 changes: 4 additions & 2 deletions crates/kira/src/effect/volume_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ impl Effect for VolumeControl {

fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) {
self.volume.update(dt * input.len() as f64, info);
for frame in input {
*frame *= self.volume.value().as_amplitude();
let num_frames = input.len();
for (i, frame) in input.iter_mut().enumerate() {
let time_in_chunk = (i + 1) as f64 / num_frames as f64;
*frame *= self.volume.interpolated_value(time_in_chunk).as_amplitude();
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion crates/kira/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* like [`Sound`](crate::sound::Sound) or [`Effect`](crate::effect::Effect).
*/

use glam::Vec3;
use glam::{Quat, Vec3};

use crate::{
arena::Arena,
Expand Down Expand Up @@ -106,6 +106,8 @@ impl<'a> Info<'a> {
listeners.get(listener_id.0).map(|listener| ListenerInfo {
position: listener.position.value().into(),
orientation: listener.orientation.value().into(),
previous_position: listener.position.previous_value().into(),
previous_orientation: listener.orientation.previous_value().into(),
})
}
InfoKind::Mock { listener_info, .. } => listener_info.get(listener_id.0).copied(),
Expand Down Expand Up @@ -158,6 +160,28 @@ pub struct ListenerInfo {
pub position: mint::Vector3<f32>,
/// The rotation of the listener.
pub orientation: mint::Quaternion<f32>,
/// The position of the listener prior to the last update.
pub previous_position: mint::Vector3<f32>,
/// The rotation of the listener prior to the last update.
pub previous_orientation: mint::Quaternion<f32>,
}

impl ListenerInfo {
/// Returns the interpolated position between the previous and current
/// position of the listener.
pub fn interpolated_position(self, amount: f32) -> mint::Vector3<f32> {
let position: Vec3 = self.position.into();
let previous_position: Vec3 = self.previous_position.into();
previous_position.lerp(position, amount).into()
}

/// Returns the interpolated orientation between the previous and current
/// orientation of the listener.
pub fn interpolated_orientation(self, amount: f32) -> mint::Quaternion<f32> {
let orientation: Quat = self.orientation.into();
let previous_orientation: Quat = self.previous_orientation.into();
previous_orientation.lerp(orientation, amount).into()
}
}

/// Generates a fake `Info` with arbitrary data. Useful for writing unit tests.
Expand Down
4 changes: 4 additions & 0 deletions crates/kira/src/playback_state_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ impl PlaybackStateManager {
self.volume_fade.value()
}

pub fn interpolated_fade_volume(&self, amount: f64) -> Decibels {
self.volume_fade.interpolated_value(amount)
}

pub fn playback_state(&self) -> PlaybackState {
match self.state {
State::Playing => PlaybackState::Playing,
Expand Down
Loading

0 comments on commit 51374a5

Please sign in to comment.