#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Waveform {
Pulse,
Sawtooth,
Triangle,
Noise,
PulseSaw,
Sine,
}
impl Waveform {
pub const ALL: &'static [Waveform] = &[
Waveform::Pulse,
Waveform::Sawtooth,
Waveform::Triangle,
Waveform::Noise,
Waveform::PulseSaw,
Waveform::Sine,
];
#[must_use]
pub fn name(self) -> &'static str {
match self {
Waveform::Pulse => "Pulse",
Waveform::Sawtooth => "Saw",
Waveform::Triangle => "Tri",
Waveform::Noise => "Noise",
Waveform::PulseSaw => "Pls+Saw",
Waveform::Sine => "Sine",
}
}
#[must_use]
pub fn next(self) -> Self {
let idx = Self::ALL.iter().position(|&w| w == self).unwrap_or(0);
Self::ALL[(idx + 1) % Self::ALL.len()]
}
#[must_use]
pub fn prev(self) -> Self {
let idx = Self::ALL.iter().position(|&w| w == self).unwrap_or(0);
let len = Self::ALL.len();
Self::ALL[(idx + len - 1) % len]
}
}
#[allow(clippy::enum_variant_names)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum FilterMode {
LowPass,
BandPass,
HighPass,
}
impl FilterMode {
pub const ALL: &'static [FilterMode] = &[
FilterMode::LowPass,
FilterMode::BandPass,
FilterMode::HighPass,
];
#[must_use]
pub fn name(self) -> &'static str {
match self {
FilterMode::LowPass => "LP",
FilterMode::BandPass => "BP",
FilterMode::HighPass => "HP",
}
}
#[must_use]
pub fn next(self) -> Self {
let idx = Self::ALL.iter().position(|&m| m == self).unwrap_or(0);
Self::ALL[(idx + 1) % Self::ALL.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum LfoTarget {
Pitch,
PulseWidth,
Cutoff,
Volume,
}
impl LfoTarget {
pub const ALL: &'static [LfoTarget] = &[
LfoTarget::Pitch,
LfoTarget::PulseWidth,
LfoTarget::Cutoff,
LfoTarget::Volume,
];
#[must_use]
pub fn name(self) -> &'static str {
match self {
LfoTarget::Pitch => "Pitch",
LfoTarget::PulseWidth => "PW",
LfoTarget::Cutoff => "Cutoff",
LfoTarget::Volume => "Volume",
}
}
#[must_use]
pub fn next(self) -> Self {
let idx = Self::ALL.iter().position(|&t| t == self).unwrap_or(0);
Self::ALL[(idx + 1) % Self::ALL.len()]
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OscParams {
pub waveform: Waveform,
pub pulse_width: f32,
pub detune: f32,
pub noise_mix: f32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Osc2Params {
pub waveform: Waveform,
pub detune: f32,
pub osc2_mix: f32,
pub hard_sync: bool,
}
impl Default for Osc2Params {
fn default() -> Self {
Self {
waveform: Waveform::Sawtooth,
detune: 7.0,
osc2_mix: 0.0,
hard_sync: false,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EnvParams {
pub attack: f32,
pub decay: f32,
pub sustain: f32,
pub release: f32,
pub env_reverse: bool,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FilterParams {
pub filter_mode: FilterMode,
pub cutoff: f32,
pub resonance: f32,
pub drive: f32,
}
#[allow(clippy::struct_field_names)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LfoParams {
pub lfo_rate: f32,
pub lfo_depth: f32,
pub lfo_target: LfoTarget,
}
#[allow(clippy::struct_field_names)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FxParams {
pub reverb_mix: f32,
pub reverb_size: f32,
pub reverb_damping: f32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CrusherParams {
pub bits: f32,
pub rate: f32,
}
impl Default for CrusherParams {
fn default() -> Self {
Self {
bits: 16.0,
rate: 1.0,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DelayParams {
pub time_ms: f32,
pub feedback: f32,
pub mix: f32,
}
impl Default for DelayParams {
fn default() -> Self {
Self {
time_ms: 375.0,
feedback: 0.3,
mix: 0.0,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ChorusParams {
pub rate: f32,
pub depth_ms: f32,
pub mix: f32,
}
impl Default for ChorusParams {
fn default() -> Self {
Self {
rate: 0.5,
depth_ms: 3.0,
mix: 0.0,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GlobalParams {
pub volume: f32,
pub glide_time: f32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SynthParams {
pub osc: OscParams,
pub env: EnvParams,
pub filter: FilterParams,
pub lfo: LfoParams,
pub fx: FxParams,
#[cfg_attr(feature = "serde", serde(default))]
pub crusher: CrusherParams,
#[cfg_attr(feature = "serde", serde(default))]
pub chorus: ChorusParams,
#[cfg_attr(feature = "serde", serde(default))]
pub delay: DelayParams,
#[cfg_attr(feature = "serde", serde(default))]
pub osc2: Osc2Params,
pub global: GlobalParams,
}
impl Default for SynthParams {
fn default() -> Self {
Self {
osc: OscParams {
waveform: Waveform::Pulse,
pulse_width: 0.5,
detune: 0.0,
noise_mix: 0.0,
},
env: EnvParams {
attack: 0.01,
decay: 0.1,
sustain: 0.8,
release: 0.3,
env_reverse: false,
},
filter: FilterParams {
filter_mode: FilterMode::LowPass,
cutoff: 4000.0,
resonance: 0.3,
drive: 0.0,
},
lfo: LfoParams {
lfo_rate: 3.0,
lfo_depth: 0.0,
lfo_target: LfoTarget::Pitch,
},
fx: FxParams {
reverb_mix: 0.15,
reverb_size: 0.5,
reverb_damping: 0.5,
},
crusher: CrusherParams::default(),
chorus: ChorusParams::default(),
delay: DelayParams::default(),
osc2: Osc2Params::default(),
global: GlobalParams {
volume: 0.7,
glide_time: 0.05,
},
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Patch {
pub name: String,
pub params: SynthParams,
}
impl Patch {
pub fn new(name: impl Into<String>, params: SynthParams) -> Self {
Self {
name: name.into(),
params,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MidiNote(pub u8);
impl MidiNote {
pub const MIDDLE_C: Self = Self(60);
pub const A4: Self = Self(69);
#[must_use]
pub const fn new_clamped(v: u8) -> Self {
Self(if v > 127 { 127 } else { v })
}
#[must_use]
pub const fn as_u8(self) -> u8 {
self.0
}
}
impl From<u8> for MidiNote {
fn from(v: u8) -> Self {
Self(v)
}
}
impl Default for MidiNote {
fn default() -> Self {
Self::MIDDLE_C
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ChannelNo(pub u8);
impl ChannelNo {
pub const DEFAULT: Self = Self(0);
#[must_use]
pub const fn as_usize(self) -> usize {
self.0 as usize
}
}
impl From<u8> for ChannelNo {
fn from(v: u8) -> Self {
Self(v)
}
}
impl Default for ChannelNo {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrumHit {
Kick,
HiHatClosed,
HiHatOpen,
}
#[cfg(all(test, feature = "serde"))]
mod tests {
use super::*;
#[test]
fn synth_params_osc2_defaults_when_missing_from_json() {
let json = r#"{
"osc": {"waveform":"Sawtooth","pulse_width":0.5,"detune":0.0,"noise_mix":0.0},
"env": {"attack":0.01,"decay":0.1,"sustain":0.8,"release":0.3,"env_reverse":false},
"filter": {"filter_mode":"LowPass","cutoff":4000.0,"resonance":0.3,"drive":0.0},
"lfo": {"lfo_rate":3.0,"lfo_depth":0.0,"lfo_target":"Pitch"},
"fx": {"reverb_mix":0.15,"reverb_size":0.5,"reverb_damping":0.5},
"global": {"volume":0.7,"glide_time":0.05}
}"#;
let params: SynthParams =
serde_json::from_str(json).expect("old-style JSON must deserialise without osc2 key");
let def = Osc2Params::default();
assert_eq!(params.osc2.waveform, def.waveform);
assert!((params.osc2.detune - def.detune).abs() < f32::EPSILON);
assert!((params.osc2.osc2_mix - def.osc2_mix).abs() < f32::EPSILON);
assert_eq!(params.osc2.hard_sync, def.hard_sync);
}
}
#[derive(Default, Debug, Clone)]
pub enum AudioEvent {
#[default]
Panic,
NoteOn(MidiNote),
NoteOff(MidiNote),
LoadPatch(Box<SynthParams>),
Drum(DrumHit),
NoteOnChannel(ChannelNo, MidiNote),
NoteOffChannel(ChannelNo, MidiNote),
LoadPatchChannel(ChannelNo, Box<SynthParams>),
}