use crate::{sanitize_str, sanitize_title, Rgba};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const AUDIO_WAVEFORM_MAX_SAMPLES: usize = 4096;
pub const AUDIO_SPECTRUM_MAX_BANDS: usize = 256;
pub const AUDIO_PLUGIN_CONTROL_MAX_ITEMS: usize = 128;
pub const AUDIO_MIXER_STRIP_MAX_PLUGINS: usize = 24;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum AudioChannelRole {
Input,
Instrument,
Aux,
Bus,
EffectReturn,
Master,
}
impl AudioChannelRole {
pub fn label(self) -> &'static str {
match self {
Self::Input => "input",
Self::Instrument => "instrument",
Self::Aux => "aux",
Self::Bus => "bus",
Self::EffectReturn => "effect_return",
Self::Master => "master",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MeterScale {
Peak,
Rms,
Loudness,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeterSegment {
pub from_db: f32,
pub to_db: f32,
pub color: Rgba,
}
impl MeterSegment {
pub fn sanitized(self) -> Self {
let from_db = finite_range(self.from_db, -160.0, 24.0, -60.0);
let to_db = finite_range(self.to_db, -160.0, 24.0, 0.0);
Self {
from_db: from_db.min(to_db),
to_db: to_db.max(from_db),
color: self.color.sanitized(),
}
}
}
pub fn standard_meter_segments() -> Vec<MeterSegment> {
vec![
MeterSegment {
from_db: -60.0,
to_db: -18.0,
color: Rgba::rgb8(81, 229, 147),
},
MeterSegment {
from_db: -18.0,
to_db: -6.0,
color: Rgba::rgb8(255, 206, 92),
},
MeterSegment {
from_db: -6.0,
to_db: 6.0,
color: Rgba::rgb8(255, 82, 95),
},
]
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioMeterModel {
pub id: String,
pub label: String,
pub level_db: f32,
pub peak_db: f32,
pub floor_db: f32,
pub ceiling_db: f32,
pub clipped: bool,
pub scale: MeterScale,
}
impl AudioMeterModel {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: sanitize_audio_id(&id.into()),
label: sanitize_audio_label(&label.into()),
level_db: -60.0,
peak_db: -60.0,
floor_db: -60.0,
ceiling_db: 6.0,
clipped: false,
scale: MeterScale::Peak,
}
}
pub fn with_level_db(mut self, level_db: f32, peak_db: f32) -> Self {
self.level_db = level_db;
self.peak_db = peak_db;
self.sanitized()
}
pub fn normalized_level(&self) -> f32 {
db_to_unit(self.level_db, self.floor_db, self.ceiling_db)
}
pub fn normalized_peak(&self) -> f32 {
db_to_unit(self.peak_db, self.floor_db, self.ceiling_db)
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
let (floor_db, ceiling_db) = sanitized_db_range(self.floor_db, self.ceiling_db);
self.floor_db = floor_db;
self.ceiling_db = ceiling_db;
self.level_db = finite_range(self.level_db, floor_db, ceiling_db, floor_db);
self.peak_db = finite_range(self.peak_db, floor_db, ceiling_db, self.level_db);
self.clipped = self.clipped || self.peak_db >= 0.0;
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioFaderModel {
pub id: String,
pub label: String,
pub value_db: f32,
pub min_db: f32,
pub max_db: f32,
pub unity_db: f32,
pub muted: bool,
pub solo: bool,
pub armed: bool,
pub role: AudioChannelRole,
}
impl AudioFaderModel {
pub fn new(id: impl Into<String>, label: impl Into<String>, role: AudioChannelRole) -> Self {
Self {
id: sanitize_audio_id(&id.into()),
label: sanitize_audio_label(&label.into()),
value_db: 0.0,
min_db: -60.0,
max_db: 12.0,
unity_db: 0.0,
muted: false,
solo: false,
armed: false,
role,
}
}
pub fn with_value_db(mut self, value_db: f32) -> Self {
self.value_db = value_db;
self.sanitized()
}
pub fn normalized_value(&self) -> f32 {
db_to_unit(self.value_db, self.min_db, self.max_db)
}
pub fn set_normalized_value(&mut self, normalized: f32) {
let normalized = finite_range(normalized, 0.0, 1.0, 0.0);
let (min_db, max_db) = sanitized_db_range(self.min_db, self.max_db);
self.value_db = min_db + (max_db - min_db) * normalized;
*self = self.clone().sanitized();
}
pub fn gain_scalar(&self) -> f32 {
if self.muted {
0.0
} else {
10.0_f32.powf(self.value_db / 20.0)
}
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
let (min_db, max_db) = sanitized_db_range(self.min_db, self.max_db);
self.min_db = min_db;
self.max_db = max_db;
self.unity_db = finite_range(self.unity_db, min_db, max_db, 0.0_f32.clamp(min_db, max_db));
self.value_db = finite_range(self.value_db, min_db, max_db, self.unity_db);
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioKnobModel {
pub id: String,
pub label: String,
pub value: f32,
pub min: f32,
pub max: f32,
pub center: f32,
pub unit: String,
pub bipolar: bool,
}
impl AudioKnobModel {
pub fn new(
id: impl Into<String>,
label: impl Into<String>,
min: f32,
max: f32,
unit: impl Into<String>,
) -> Self {
let (min, max) = sanitized_linear_range(min, max);
Self {
id: sanitize_audio_id(&id.into()),
label: sanitize_audio_label(&label.into()),
value: min,
min,
max,
center: min + (max - min) * 0.5,
unit: sanitize_audio_label(&unit.into()),
bipolar: false,
}
}
pub fn with_value(mut self, value: f32) -> Self {
self.value = value;
self.sanitized()
}
pub fn bipolar(mut self, bipolar: bool) -> Self {
self.bipolar = bipolar;
self
}
pub fn normalized_value(&self) -> f32 {
db_to_unit(self.value, self.min, self.max)
}
pub fn set_normalized_value(&mut self, normalized: f32) {
let normalized = finite_range(normalized, 0.0, 1.0, 0.0);
let (min, max) = sanitized_linear_range(self.min, self.max);
self.value = min + (max - min) * normalized;
*self = self.clone().sanitized();
}
pub fn display_value(&self) -> String {
let value = if self.unit == "Hz" && self.value >= 1000.0 {
format!("{:.1} kHz", self.value / 1000.0)
} else if self.unit.is_empty() {
format!("{:.2}", self.value)
} else {
format!("{:.2} {}", self.value, self.unit)
};
sanitize_title(&value, 64)
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
self.unit = sanitize_audio_label(&self.unit);
let (min, max) = sanitized_linear_range(self.min, self.max);
self.min = min;
self.max = max;
self.center = finite_range(self.center, min, max, min + (max - min) * 0.5);
self.value = finite_range(self.value, min, max, self.center);
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioWaveformModel {
pub id: String,
pub label: String,
pub samples: Vec<f32>,
pub sample_rate: u32,
pub zoom: f32,
}
impl AudioWaveformModel {
pub fn new(
id: impl Into<String>,
label: impl Into<String>,
samples: impl IntoIterator<Item = f32>,
) -> Self {
Self {
id: sanitize_audio_id(&id.into()),
label: sanitize_audio_label(&label.into()),
samples: samples
.into_iter()
.take(AUDIO_WAVEFORM_MAX_SAMPLES)
.map(sanitize_sample)
.collect(),
sample_rate: 48_000,
zoom: 1.0,
}
}
pub fn peak(&self) -> f32 {
self.samples
.iter()
.copied()
.map(f32::abs)
.fold(0.0, f32::max)
.clamp(0.0, 1.0)
}
pub fn rms(&self) -> f32 {
if self.samples.is_empty() {
return 0.0;
}
let sum = self
.samples
.iter()
.copied()
.map(|sample| sanitize_sample(sample).powi(2))
.sum::<f32>();
(sum / self.samples.len() as f32).sqrt().clamp(0.0, 1.0)
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
self.samples.truncate(AUDIO_WAVEFORM_MAX_SAMPLES);
self.samples
.iter_mut()
.for_each(|sample| *sample = sanitize_sample(*sample));
self.sample_rate = self.sample_rate.clamp(8_000, 384_000);
self.zoom = finite_range(self.zoom, 0.05, 128.0, 1.0);
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioSpectrumModel {
pub id: String,
pub label: String,
pub bands_db: Vec<f32>,
pub min_db: f32,
pub max_db: f32,
}
impl AudioSpectrumModel {
pub fn new(
id: impl Into<String>,
label: impl Into<String>,
bands_db: impl IntoIterator<Item = f32>,
) -> Self {
Self {
id: sanitize_audio_id(&id.into()),
label: sanitize_audio_label(&label.into()),
bands_db: bands_db
.into_iter()
.take(AUDIO_SPECTRUM_MAX_BANDS)
.map(|band| finite_range(band, -120.0, 24.0, -120.0))
.collect(),
min_db: -90.0,
max_db: 6.0,
}
.sanitized()
}
pub fn normalized_bands(&self) -> Vec<f32> {
self.bands_db
.iter()
.copied()
.map(|band| db_to_unit(band, self.min_db, self.max_db))
.collect()
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
let (min_db, max_db) = sanitized_db_range(self.min_db, self.max_db);
self.min_db = min_db;
self.max_db = max_db;
self.bands_db.truncate(AUDIO_SPECTRUM_MAX_BANDS);
self.bands_db
.iter_mut()
.for_each(|band| *band = finite_range(*band, min_db, max_db, min_db));
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum AudioTransportState {
Stopped,
Playing,
Recording,
Paused,
Looping,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioTransportModel {
pub state: AudioTransportState,
pub bpm: f32,
pub sample_rate: u32,
pub position_seconds: f64,
pub loop_start_seconds: f64,
pub loop_end_seconds: f64,
}
impl Default for AudioTransportModel {
fn default() -> Self {
Self {
state: AudioTransportState::Stopped,
bpm: 120.0,
sample_rate: 48_000,
position_seconds: 0.0,
loop_start_seconds: 0.0,
loop_end_seconds: 0.0,
}
}
}
impl AudioTransportModel {
pub fn sanitized(mut self) -> Self {
self.bpm = finite_range(self.bpm, 20.0, 320.0, 120.0);
self.sample_rate = self.sample_rate.clamp(8_000, 384_000);
self.position_seconds = finite_f64(self.position_seconds).max(0.0);
self.loop_start_seconds = finite_f64(self.loop_start_seconds).max(0.0);
self.loop_end_seconds = finite_f64(self.loop_end_seconds).max(self.loop_start_seconds);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum AudioControlKind {
Fader,
Knob,
Switch,
Button,
Selector,
Meter,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioPluginControl {
pub id: String,
pub label: String,
pub kind: AudioControlKind,
pub normalized_value: f32,
pub unit: String,
pub automation_lane: Option<String>,
pub disabled: bool,
}
impl AudioPluginControl {
pub fn new(id: impl Into<String>, label: impl Into<String>, kind: AudioControlKind) -> Self {
Self {
id: sanitize_audio_id(&id.into()),
label: sanitize_audio_label(&label.into()),
kind,
normalized_value: 0.0,
unit: String::new(),
automation_lane: None,
disabled: false,
}
}
pub fn with_value(mut self, normalized_value: f32, unit: impl Into<String>) -> Self {
self.normalized_value = normalized_value;
self.unit = unit.into();
self.sanitized()
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
self.normalized_value = finite_range(self.normalized_value, 0.0, 1.0, 0.0);
self.unit = sanitize_audio_label(&self.unit);
self.automation_lane = self
.automation_lane
.map(|lane| sanitize_audio_id(&lane))
.filter(|lane| !lane.is_empty());
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioPluginPanel {
pub id: String,
pub title: String,
pub bypassed: bool,
pub latency_samples: u32,
pub controls: Vec<AudioPluginControl>,
}
impl AudioPluginPanel {
pub fn new(
id: impl Into<String>,
title: impl Into<String>,
controls: impl IntoIterator<Item = AudioPluginControl>,
) -> Self {
Self {
id: sanitize_audio_id(&id.into()),
title: sanitize_audio_label(&title.into()),
bypassed: false,
latency_samples: 0,
controls: controls
.into_iter()
.take(AUDIO_PLUGIN_CONTROL_MAX_ITEMS)
.map(AudioPluginControl::sanitized)
.collect(),
}
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.title = sanitize_audio_label(&self.title);
self.latency_samples = self.latency_samples.min(1_000_000);
self.controls.truncate(AUDIO_PLUGIN_CONTROL_MAX_ITEMS);
self.controls
.iter_mut()
.for_each(|control| *control = control.clone().sanitized());
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioMixerStrip {
pub id: String,
pub label: String,
pub role: AudioChannelRole,
pub meter: AudioMeterModel,
pub fader: AudioFaderModel,
pub pan: AudioKnobModel,
pub plugins: Vec<AudioPluginControl>,
}
impl AudioMixerStrip {
pub fn new(id: impl Into<String>, label: impl Into<String>, role: AudioChannelRole) -> Self {
let id = sanitize_audio_id(&id.into());
let label = sanitize_audio_label(&label.into());
Self {
meter: AudioMeterModel::new(format!("{id}.meter"), &label),
fader: AudioFaderModel::new(format!("{id}.fader"), &label, role),
pan: AudioKnobModel::new(format!("{id}.pan"), "Pan", -1.0, 1.0, "").bipolar(true),
plugins: Vec::new(),
id,
label,
role,
}
}
pub fn with_plugins(mut self, plugins: impl IntoIterator<Item = AudioPluginControl>) -> Self {
self.plugins = plugins
.into_iter()
.take(AUDIO_MIXER_STRIP_MAX_PLUGINS)
.map(AudioPluginControl::sanitized)
.collect();
self
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_audio_id(&self.id);
self.label = sanitize_audio_label(&self.label);
self.meter = self.meter.sanitized();
self.fader = self.fader.sanitized();
self.pan = self.pan.sanitized();
self.plugins.truncate(AUDIO_MIXER_STRIP_MAX_PLUGINS);
self.plugins
.iter_mut()
.for_each(|plugin| *plugin = plugin.clone().sanitized());
self
}
}
pub fn demo_mixer_strips() -> Vec<AudioMixerStrip> {
vec![
AudioMixerStrip::new("drums", "Drums", AudioChannelRole::Bus)
.with_plugins([AudioPluginControl::new(
"compressor",
"Bus Comp",
AudioControlKind::Knob,
)
.with_value(0.64, "%")])
.sanitized(),
AudioMixerStrip::new("bass", "Bass DI", AudioChannelRole::Input)
.with_plugins([
AudioPluginControl::new("drive", "Drive", AudioControlKind::Knob)
.with_value(0.42, "%"),
])
.sanitized(),
AudioMixerStrip::new("vocal", "Lead Vox", AudioChannelRole::Input)
.with_plugins([AudioPluginControl::new(
"deesser",
"De-esser",
AudioControlKind::Switch,
)
.with_value(1.0, "")])
.sanitized(),
AudioMixerStrip::new("master", "Master", AudioChannelRole::Master)
.with_plugins([
AudioPluginControl::new("limiter", "Limiter", AudioControlKind::Meter)
.with_value(0.78, "LUFS"),
])
.sanitized(),
]
}
fn sanitize_audio_id(input: &str) -> String {
sanitize_str(input, 96).trim().replace(' ', "_")
}
fn sanitize_audio_label(input: &str) -> String {
sanitize_title(input, 96).trim().to_string()
}
fn finite_range(value: f32, min: f32, max: f32, fallback: f32) -> f32 {
if value.is_finite() {
value.clamp(min, max)
} else {
fallback
}
}
fn finite_f64(value: f64) -> f64 {
if value.is_finite() {
value
} else {
0.0
}
}
fn sanitized_db_range(min_db: f32, max_db: f32) -> (f32, f32) {
let min_db = finite_range(min_db, -160.0, 24.0, -60.0);
let max_db = finite_range(max_db, -160.0, 24.0, 12.0);
if max_db <= min_db {
(min_db.min(-60.0), min_db.min(-60.0) + 1.0)
} else {
(min_db, max_db)
}
}
fn sanitized_linear_range(min: f32, max: f32) -> (f32, f32) {
let min = finite_range(min, -1_000_000.0, 1_000_000.0, 0.0);
let max = finite_range(max, -1_000_000.0, 1_000_000.0, 1.0);
if max <= min {
(min, min + 1.0)
} else {
(min, max)
}
}
fn db_to_unit(value: f32, min_db: f32, max_db: f32) -> f32 {
let (min_db, max_db) = sanitized_db_range(min_db, max_db);
((finite_range(value, min_db, max_db, min_db) - min_db) / (max_db - min_db)).clamp(0.0, 1.0)
}
fn sanitize_sample(sample: f32) -> f32 {
finite_range(sample, -1.0, 1.0, 0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meter_and_fader_values_are_normalized_and_bounded() {
let meter = AudioMeterModel::new("mix", "Mix").with_level_db(-27.0, -6.0);
let mut fader = AudioFaderModel::new("mix.fader", "Mix", AudioChannelRole::Bus);
fader.set_normalized_value(0.5);
assert!(meter.normalized_level() > 0.0);
assert!(meter.normalized_peak() > meter.normalized_level());
assert!((0.0..=1.0).contains(&fader.normalized_value()));
assert!(fader.gain_scalar().is_finite());
fader.muted = true;
assert_eq!(fader.gain_scalar(), 0.0);
}
#[test]
fn waveform_and_spectrum_models_sanitize_public_samples() {
let waveform =
AudioWaveformModel::new(
"wave",
"Wave",
(0..(AUDIO_WAVEFORM_MAX_SAMPLES + 10)).map(|index| {
if index % 2 == 0 {
f32::NAN
} else {
2.0
}
}),
);
let spectrum = AudioSpectrumModel::new(
"spectrum",
"Spectrum",
(0..(AUDIO_SPECTRUM_MAX_BANDS + 10)).map(|_| f32::INFINITY),
);
assert_eq!(waveform.samples.len(), AUDIO_WAVEFORM_MAX_SAMPLES);
assert_eq!(waveform.samples[0], 0.0);
assert_eq!(waveform.samples[1], 1.0);
assert_eq!(waveform.peak(), 1.0);
assert!(waveform.rms().is_finite());
assert_eq!(spectrum.bands_db.len(), AUDIO_SPECTRUM_MAX_BANDS);
assert!(spectrum
.normalized_bands()
.iter()
.all(|band| (0.0..=1.0).contains(band)));
}
#[test]
fn plugin_panels_and_mixer_strips_cap_children() {
let controls = (0..(AUDIO_PLUGIN_CONTROL_MAX_ITEMS + 20)).map(|index| {
AudioPluginControl::new(format!("control {index}"), "Drive", AudioControlKind::Knob)
.with_value(f32::NAN, "%")
});
let panel = AudioPluginPanel::new("plugin", "Saturator", controls);
let strip = AudioMixerStrip::new("lead", "Lead", AudioChannelRole::Input).with_plugins(
(0..(AUDIO_MIXER_STRIP_MAX_PLUGINS + 10)).map(|index| {
AudioPluginControl::new(format!("slot {index}"), "EQ", AudioControlKind::Knob)
}),
);
assert_eq!(panel.controls.len(), AUDIO_PLUGIN_CONTROL_MAX_ITEMS);
assert_eq!(panel.controls[0].normalized_value, 0.0);
assert_eq!(strip.plugins.len(), AUDIO_MIXER_STRIP_MAX_PLUGINS);
assert!(strip.pan.bipolar);
}
#[test]
fn demo_mixer_strips_cover_common_roles() {
let strips = demo_mixer_strips();
assert!(strips
.iter()
.any(|strip| strip.role == AudioChannelRole::Input));
assert!(strips
.iter()
.any(|strip| strip.role == AudioChannelRole::Bus));
assert!(strips
.iter()
.any(|strip| strip.role == AudioChannelRole::Master));
}
#[cfg(feature = "serde")]
#[test]
fn audio_models_round_trip_through_serde() {
let strip = demo_mixer_strips().remove(0);
let encoded = serde_json::to_string(&strip).unwrap();
let decoded: AudioMixerStrip = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.id, strip.id);
assert_eq!(decoded.plugins.len(), strip.plugins.len());
}
}