#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::param::Param;
#[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,
];
pub const NAMES: &'static [&'static str] =
&["Pulse", "Sawtooth", "Triangle", "Noise", "PulseSaw", "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]
}
}
#[cfg(feature = "arp")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ArpMode {
#[default]
Up,
Down,
UpDown,
Random,
}
#[cfg(feature = "arp")]
impl ArpMode {
pub const ALL: &'static [ArpMode] =
&[ArpMode::Up, ArpMode::Down, ArpMode::UpDown, ArpMode::Random];
pub const NAMES: &'static [&'static str] = &["Up", "Down", "UpDown", "Random"];
}
#[cfg(feature = "arp")]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ArpParams {
pub enabled: bool,
pub rate: f32,
pub gate: f32,
pub mode: ArpMode,
pub notes: [MidiNote; 4],
pub count: u8,
}
#[cfg(feature = "arp")]
impl Default for ArpParams {
fn default() -> Self {
Self {
enabled: false,
rate: 10.0,
gate: 0.8,
mode: ArpMode::Up,
notes: [MidiNote::MIDDLE_C; 4],
count: 0,
}
}
}
#[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,
];
pub const NAMES: &'static [&'static str] = &["LowPass", "BandPass", "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,
];
pub const NAMES: &'static [&'static str] = &["Pitch", "PulseWidth", "Cutoff", "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()]
}
#[must_use]
pub fn prev(self) -> Self {
let idx = Self::ALL.iter().position(|&t| t == self).unwrap_or(0);
let len = Self::ALL.len();
Self::ALL[(idx + len - 1) % len]
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OscParams {
pub waveform: Waveform,
pub pulse_width: f32,
pub detune: f32,
pub noise_mix: f32,
}
impl Default for OscParams {
fn default() -> Self {
Self {
waveform: Waveform::Pulse,
pulse_width: 0.5,
detune: 0.0,
noise_mix: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RingModMode {
#[default]
Off,
Osc2ByOsc1Sign,
Osc1ByOsc2Sign,
Analog,
}
impl RingModMode {
pub const ALL: &'static [RingModMode] = &[
RingModMode::Off,
RingModMode::Osc2ByOsc1Sign,
RingModMode::Osc1ByOsc2Sign,
RingModMode::Analog,
];
pub const NAMES: &'static [&'static str] =
&["Off", "Osc2ByOsc1Sign", "Osc1ByOsc2Sign", "Analog"];
#[must_use]
pub fn name(self) -> &'static str {
match self {
RingModMode::Off => "Off",
RingModMode::Osc2ByOsc1Sign => "SID",
RingModMode::Osc1ByOsc2Sign => "Rev",
RingModMode::Analog => "Analog",
}
}
#[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()]
}
#[must_use]
pub fn prev(self) -> Self {
let idx = Self::ALL.iter().position(|&m| m == self).unwrap_or(0);
let len = Self::ALL.len();
Self::ALL[(idx + len - 1) % len]
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Osc2Params {
pub waveform: Waveform,
pub detune: f32,
#[cfg_attr(feature = "serde", serde(default))]
pub semitones: f32,
pub osc2_mix: f32,
pub hard_sync: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub ring_mod: RingModMode,
}
impl Default for Osc2Params {
fn default() -> Self {
Self {
waveform: Waveform::Sawtooth,
detune: 7.0,
semitones: 0.0,
osc2_mix: 0.0,
hard_sync: false,
ring_mod: RingModMode::Off,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[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,
}
impl Default for EnvParams {
fn default() -> Self {
Self {
attack: 0.01,
decay: 0.1,
sustain: 0.8,
release: 0.3,
env_reverse: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FilterParams {
pub filter_mode: FilterMode,
pub cutoff: f32,
pub resonance: f32,
pub drive: f32,
}
impl Default for FilterParams {
fn default() -> Self {
Self {
filter_mode: FilterMode::LowPass,
cutoff: 4000.0,
resonance: 0.3,
drive: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum LfoShape {
#[default]
Sine,
Square,
Sawtooth,
SampleHold,
}
impl LfoShape {
pub const ALL: &'static [LfoShape] = &[
LfoShape::Sine,
LfoShape::Square,
LfoShape::Sawtooth,
LfoShape::SampleHold,
];
pub const NAMES: &'static [&'static str] = &["Sine", "Square", "Sawtooth", "SampleHold"];
#[must_use]
pub fn name(self) -> &'static str {
match self {
LfoShape::Sine => "Sin",
LfoShape::Square => "Sqr",
LfoShape::Sawtooth => "Saw",
LfoShape::SampleHold => "S&H",
}
}
#[must_use]
pub fn next(self) -> Self {
let idx = Self::ALL.iter().position(|&s| s == self).unwrap_or(0);
Self::ALL[(idx + 1) % Self::ALL.len()]
}
#[must_use]
pub fn prev(self) -> Self {
let idx = Self::ALL.iter().position(|&s| s == self).unwrap_or(0);
Self::ALL[(idx + Self::ALL.len() - 1) % Self::ALL.len()]
}
}
#[allow(clippy::struct_field_names)] #[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LfoParams {
pub lfo_rate: f32,
pub lfo_depth: f32,
pub lfo_target: LfoTarget,
#[cfg_attr(feature = "serde", serde(default))]
pub lfo_shape: LfoShape,
}
impl Default for LfoParams {
fn default() -> Self {
Self {
lfo_rate: 3.0,
lfo_depth: 0.0,
lfo_target: LfoTarget::Pitch,
lfo_shape: LfoShape::Sine,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ModEnvParams {
pub attack: f32,
pub decay: f32,
pub sustain: f32,
pub release: f32,
pub depth: f32,
}
impl Default for ModEnvParams {
fn default() -> Self {
Self {
attack: 0.001,
decay: 0.1,
sustain: 0.0,
release: 0.1,
depth: 0.0,
}
}
}
#[allow(clippy::struct_field_names)] #[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FxParams {
pub reverb_mix: f32,
pub reverb_size: f32,
pub reverb_damping: f32,
}
impl Default for FxParams {
fn default() -> Self {
Self {
reverb_mix: 0.15,
reverb_size: 0.5,
reverb_damping: 0.5,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[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, PartialEq)]
#[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, PartialEq)]
#[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, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GlobalParams {
pub volume: f32,
pub glide_time: f32,
}
impl Default for GlobalParams {
fn default() -> Self {
Self {
volume: 0.7,
glide_time: 0.05,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[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,
#[cfg_attr(feature = "serde", serde(default))]
pub lfo2: LfoParams,
#[cfg_attr(feature = "serde", serde(default))]
pub filter_env: ModEnvParams,
#[cfg_attr(feature = "serde", serde(default))]
pub pitch_env: ModEnvParams,
#[cfg(feature = "arp")]
#[cfg_attr(feature = "serde", serde(default))]
pub arp: ArpParams,
pub global: GlobalParams,
}
impl SynthParams {
pub fn sanitize(&mut self) {
let default = Self::default();
for ¶m in Param::ALL {
let value = param.get(self);
let value = if value.is_finite() {
value
} else {
param.get(&default)
};
param.set(self, value);
}
#[cfg(feature = "arp")]
{
self.arp.count = self.arp.count.min(4);
for note in &mut self.arp.notes {
*note = MidiNote::new_clamped(note.0);
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[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)]
#[non_exhaustive]
pub enum DrumHit {
Kick,
Snare,
HiHatClosed,
HiHatOpen,
Crash,
TomLow,
TomMid,
TomHigh,
Clap,
}
impl DrumHit {
pub const ALL: [Self; 9] = [
Self::Kick,
Self::Snare,
Self::HiHatClosed,
Self::HiHatOpen,
Self::Crash,
Self::TomLow,
Self::TomMid,
Self::TomHigh,
Self::Clap,
];
}
#[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);
assert_eq!(params.osc2.ring_mod, def.ring_mod);
}
#[test]
fn osc2_semitones_defaults_to_zero_when_missing_from_json() {
let json = r#"{"waveform":"Sawtooth","detune":7.0,"osc2_mix":0.5,"hard_sync":false}"#;
let osc2: Osc2Params =
serde_json::from_str(json).expect("osc2 JSON must deserialise without semitones");
assert!(osc2.semitones.abs() < f32::EPSILON);
assert!((osc2.detune - 7.0).abs() < f32::EPSILON);
}
#[test]
fn synth_params_mod_fields_default_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 JSON must deserialise without mod fields");
assert_eq!(params.lfo.lfo_shape, LfoShape::Sine);
assert_eq!(params.lfo2.lfo_shape, LfoShape::Sine);
assert!((params.lfo2.lfo_depth).abs() < f32::EPSILON);
assert!((params.filter_env.depth).abs() < f32::EPSILON);
assert!((params.pitch_env.depth).abs() < f32::EPSILON);
}
#[cfg(feature = "arp")]
#[test]
fn synth_params_arp_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 JSON must deserialise without arp key");
assert!(!params.arp.enabled);
assert!((params.arp.rate - 10.0).abs() < f32::EPSILON);
assert_eq!(params.arp.count, 0);
}
}
#[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>),
SetParam(ChannelNo, Param, f32),
#[cfg(feature = "arp")]
ArpSetNotes(ChannelNo, [MidiNote; 4], u8),
#[cfg(feature = "arp")]
ArpEnabled(ChannelNo, bool),
}
#[cfg(test)]
mod default_tests {
use super::*;
#[test]
fn section_defaults_keep_the_default_patch() {
let p = SynthParams::default();
assert_eq!(
p.osc,
OscParams {
waveform: Waveform::Pulse,
pulse_width: 0.5,
detune: 0.0,
noise_mix: 0.0,
}
);
assert_eq!(
p.env,
EnvParams {
attack: 0.01,
decay: 0.1,
sustain: 0.8,
release: 0.3,
env_reverse: false,
}
);
assert_eq!(
p.filter,
FilterParams {
filter_mode: FilterMode::LowPass,
cutoff: 4000.0,
resonance: 0.3,
drive: 0.0,
}
);
assert_eq!(
p.lfo,
LfoParams {
lfo_rate: 3.0,
lfo_depth: 0.0,
lfo_target: LfoTarget::Pitch,
lfo_shape: LfoShape::Sine,
}
);
assert_eq!(
p.fx,
FxParams {
reverb_mix: 0.15,
reverb_size: 0.5,
reverb_damping: 0.5,
}
);
assert_eq!(
p.global,
GlobalParams {
volume: 0.7,
glide_time: 0.05,
}
);
}
}