Check and Windows preview / check (anvil) (push) Canceled after 0s
Check and Windows preview / check (braze) (push) Canceled after 0s
Check and Windows preview / check (crucible) (push) Canceled after 0s
Check and Windows preview / check (knurl) (push) Canceled after 0s
Check and Windows preview / check (lathe) (push) Canceled after 0s
Check and Windows preview / check (patina) (push) Canceled after 0s
Check and Windows preview / check (planish) (push) Canceled after 0s
Check and Windows preview / check (rivet) (push) Canceled after 0s
Check and Windows preview / check (temper) (push) Canceled after 0s
Check and Windows preview / check (weld) (push) Canceled after 0s
Check and Windows preview / previews (push) Canceled after 0s
Check and Windows preview / windows (push) Canceled after 0s
Anvil gets a sidechain input named Kick. With Kick lock on, kicks are found on the undelayed sidechain, so with the lookahead each hit ducks the sub (Duck, Release) when the delayed kick plays and restarts the sub at Phase, shifted by Offset (-30 to +30 ms, also before the kick). The Audition steps the offset from -30 to +30 ms, 4 kicks each, and measures how kick and sub sum below 150 Hz after each hit; Keep takes the offset heard. On a 55 Hz kick burst against a 55 Hz sub the sum peaks at 0 ms (+2.4 dB) and nearly again one period away (+/-20 ms), so the bars check the choice by ear rather than make it.
145 lines
4.8 KiB
Rust
145 lines
4.8 KiB
Rust
//! Host-independent controls. Levels in dB, times in milliseconds, frequencies in Hz.
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||
pub enum Source {
|
||
/// Pitch and note starts are detected from the incoming bass.
|
||
#[default]
|
||
Audio,
|
||
/// Notes come from MIDI note input; the audio only supplies the band above the split.
|
||
Midi,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||
#[serde(deny_unknown_fields)]
|
||
pub struct Settings {
|
||
pub source: Source,
|
||
/// Peak level of the new sub in dBFS, the same for every note.
|
||
pub level: f32,
|
||
/// Crossover frequency: below it the original sub is replaced.
|
||
pub split: f32,
|
||
/// 0 % holds a constant level per note; 100 % follows the original low band's shape.
|
||
pub dynamics: f32,
|
||
/// Soft saturation of the sine, adding upper harmonics for small speakers.
|
||
pub drive: f32,
|
||
/// Ghost harmonics: adds the sub's harmonics 2–10 above `ghost_from`, so small
|
||
/// speakers hear the note. Only harmonics the patch lacks are filled. 0 is off.
|
||
#[serde(default)]
|
||
pub ghost: f32,
|
||
/// Lowest frequency Ghost harmonics reach down to, in Hz (never below `split`).
|
||
#[serde(default = "default_ghost_from")]
|
||
pub ghost_from: f32,
|
||
pub octave: i32,
|
||
/// Pitch slide time between overlapping notes.
|
||
pub glide: f32,
|
||
/// Start phase in degrees on every note start.
|
||
pub phase: f32,
|
||
/// Detection threshold in dBFS for note starts (Audio source).
|
||
pub gate: f32,
|
||
/// Amount of the original low band that is kept under the new sub.
|
||
pub keep: f32,
|
||
pub output: f32,
|
||
/// Restart the sine at `phase` on every note, so each note and kick hit lines up the same.
|
||
pub retrigger: bool,
|
||
/// Round detected pitch to the nearest semitone.
|
||
pub snap: bool,
|
||
pub bypass: bool,
|
||
/// Kick lock: on each kick in the sidechain, duck the sub and restart its phase.
|
||
#[serde(default)]
|
||
pub kick_lock: bool,
|
||
/// How far the sub dips on each kick, in dB.
|
||
#[serde(default = "default_kick_duck")]
|
||
pub kick_duck: f32,
|
||
/// Time for the sub to come back after a kick, in ms.
|
||
#[serde(default = "default_kick_release")]
|
||
pub kick_release: f32,
|
||
/// When the sub restarts against the kick's start, in ms. Negative: before the kick.
|
||
#[serde(default)]
|
||
pub kick_offset: f32,
|
||
}
|
||
const GHOST_FROM: f32 = 300.;
|
||
const KICK_DUCK: f32 = 6.;
|
||
const KICK_RELEASE: f32 = 80.;
|
||
/// Range of the kick offset, in ms either side of the kick.
|
||
pub const KICK_OFFSET_MAX: f32 = 30.;
|
||
fn default_kick_duck() -> f32 {
|
||
KICK_DUCK
|
||
}
|
||
fn default_kick_release() -> f32 {
|
||
KICK_RELEASE
|
||
}
|
||
fn default_ghost_from() -> f32 {
|
||
GHOST_FROM
|
||
}
|
||
impl Default for Settings {
|
||
fn default() -> Self {
|
||
Self {
|
||
source: Source::Audio,
|
||
level: -9.,
|
||
split: 90.,
|
||
dynamics: 0.,
|
||
drive: 0.,
|
||
ghost: 0.,
|
||
ghost_from: GHOST_FROM,
|
||
octave: 0,
|
||
glide: 0.,
|
||
phase: 0.,
|
||
gate: -42.,
|
||
keep: 0.,
|
||
output: 0.,
|
||
retrigger: true,
|
||
snap: true,
|
||
bypass: false,
|
||
kick_lock: false,
|
||
kick_duck: KICK_DUCK,
|
||
kick_release: KICK_RELEASE,
|
||
kick_offset: 0.,
|
||
}
|
||
}
|
||
}
|
||
impl Settings {
|
||
pub fn validate(&self) -> Result<(), &'static str> {
|
||
let finite = [
|
||
self.level,
|
||
self.split,
|
||
self.dynamics,
|
||
self.drive,
|
||
self.ghost,
|
||
self.ghost_from,
|
||
self.glide,
|
||
self.phase,
|
||
self.gate,
|
||
self.keep,
|
||
self.output,
|
||
self.kick_duck,
|
||
self.kick_release,
|
||
self.kick_offset,
|
||
]
|
||
.iter()
|
||
.all(|v| v.is_finite());
|
||
if !finite {
|
||
return Err("non-finite setting");
|
||
}
|
||
let ok = (-30.0..=0.).contains(&self.level)
|
||
&& (40.0..=200.).contains(&self.split)
|
||
&& (0.0..=100.).contains(&self.dynamics)
|
||
&& (0.0..=100.).contains(&self.drive)
|
||
&& (0.0..=100.).contains(&self.ghost)
|
||
&& (150.0..=600.).contains(&self.ghost_from)
|
||
&& (-2..=1).contains(&self.octave)
|
||
&& (0.0..=200.).contains(&self.glide)
|
||
&& (0.0..=360.).contains(&self.phase)
|
||
&& (-60.0..=-20.).contains(&self.gate)
|
||
&& (0.0..=100.).contains(&self.keep)
|
||
&& (-12.0..=12.).contains(&self.output)
|
||
&& (0.0..=24.).contains(&self.kick_duck)
|
||
&& (10.0..=300.).contains(&self.kick_release)
|
||
&& (-KICK_OFFSET_MAX..=KICK_OFFSET_MAX).contains(&self.kick_offset);
|
||
if ok {
|
||
Ok(())
|
||
} else {
|
||
Err("setting out of range")
|
||
}
|
||
}
|
||
}
|