Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
485 changes: 385 additions & 100 deletions src/gui/amp.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/gui/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod amp;
pub mod widgets;
61 changes: 61 additions & 0 deletions src/gui/widgets/compressor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use super::labeled_slider;
use crate::gui::amp::{CompressorConfig, Message};
use iced::widget::{column, container, row, text};
use iced::{Element, Length};

pub fn compressor_widget(idx: usize, cfg: &CompressorConfig) -> Element<Message> {
let header = row![
text(format!("Compressor {}", idx + 1)),
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let body = column![
labeled_slider(
"Threshold",
-60.0..=0.0,
cfg.threshold_db,
move |v| Message::CompressorThresholdChanged(idx, v),
|v| format!("{:.1} dB", v)
),
labeled_slider(
"Ratio",
1.0..=20.0,
cfg.ratio,
move |v| Message::CompressorRatioChanged(idx, v),
|v| format!("{:.1}:1", v)
),
labeled_slider(
"Attack",
0.1..=100.0,
cfg.attack_ms,
move |v| Message::CompressorAttackChanged(idx, v),
|v| format!("{:.1} ms", v)
),
labeled_slider(
"Release",
10.0..=1000.0,
cfg.release_ms,
move |v| Message::CompressorReleaseChanged(idx, v),
|v| format!("{:.0} ms", v)
),
labeled_slider(
"Makeup",
-12.0..=24.0,
cfg.makeup_db,
move |v| Message::CompressorMakeupChanged(idx, v),
|v| format!("{:.1} dB", v)
),
]
.spacing(5);

container(column![header, body].spacing(5).padding(10))
.width(Length::Fill)
.style(|theme: &iced::Theme| {
container::Style::default()
.background(theme.palette().background)
.border(iced::Border::default().rounded(5))
})
.into()
}
59 changes: 59 additions & 0 deletions src/gui/widgets/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use super::labeled_slider;
use crate::gui::amp::{FilterConfig, Message};
use crate::sim::stages::filter::FilterType;
use iced::widget::{column, container, pick_list, row, text};
use iced::{Element, Length};

pub fn filter_widget(idx: usize, cfg: &FilterConfig) -> Element<Message> {
let header = row![
text(format!("Filter {}", idx + 1)),
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let filter_types = vec![
FilterType::Highpass,
FilterType::Lowpass,
FilterType::Bandpass,
FilterType::Notch,
];

let type_picker = row![
text("Type:").width(Length::FillPortion(3)),
pick_list(filter_types, Some(cfg.filter_type), move |t| {
Message::FilterTypeChanged(idx, t)
})
.width(Length::FillPortion(7)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let body = column![
type_picker,
labeled_slider(
"Cutoff",
20.0..=20_000.0,
cfg.cutoff_hz,
move |v| Message::FilterCutoffChanged(idx, v),
|v| format!("{:.0} Hz", v)
),
labeled_slider(
"Resonance",
0.0..=1.0,
cfg.resonance,
move |v| Message::FilterResonanceChanged(idx, v),
|v| format!("{:.2}", v)
),
]
.spacing(5);

container(column![header, body].spacing(5).padding(10))
.width(Length::Fill)
.style(|theme: &iced::Theme| {
container::Style::default()
.background(theme.palette().background)
.border(iced::Border::default().rounded(5))
})
.into()
}
28 changes: 28 additions & 0 deletions src/gui/widgets/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
pub mod compressor;
pub mod filter;
pub mod poweramp;
pub mod preamp;
pub mod tonestack;

use crate::gui::amp::Message;
use iced::{Element, Length};

pub fn labeled_slider<'a, F: 'a + Fn(f32) -> Message>(
label: &'a str,
range: std::ops::RangeInclusive<f32>,
value: f32,
on_change: F,
format: impl Fn(f32) -> String + 'a,
) -> Element<'a, Message> {
use iced::Alignment;
use iced::widget::{row, slider, text};

row![
text(label).width(Length::FillPortion(3)),
slider(range, value, on_change).width(Length::FillPortion(5)),
text(format(value)).width(Length::FillPortion(2)),
]
.spacing(10)
.align_y(Alignment::Center)
.into()
}
58 changes: 58 additions & 0 deletions src/gui/widgets/poweramp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use super::labeled_slider;
use crate::gui::amp::{Message, PowerAmpConfig};
use crate::sim::stages::poweramp::PowerAmpType;
use iced::widget::{column, container, pick_list, row, text};
use iced::{Element, Length};

pub fn poweramp_widget(idx: usize, cfg: &PowerAmpConfig) -> Element<Message> {
let header = row![
text(format!("Power Amp {}", idx + 1)),
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let amp_types = vec![
PowerAmpType::ClassA,
PowerAmpType::ClassAB,
PowerAmpType::ClassB,
];

let type_picker = row![
text("Type:").width(Length::FillPortion(3)),
pick_list(amp_types, Some(cfg.amp_type), move |t| {
Message::PowerAmpTypeChanged(idx, t)
})
.width(Length::FillPortion(7)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let body = column![
type_picker,
labeled_slider(
"Drive",
0.0..=1.0,
cfg.drive,
move |v| Message::PowerAmpDriveChanged(idx, v),
|v| format!("{:.2}", v)
),
labeled_slider(
"Sag",
0.0..=1.0,
cfg.sag,
move |v| Message::PowerAmpSagChanged(idx, v),
|v| format!("{:.2}", v)
),
]
.spacing(5);

container(column![header, body].spacing(5).padding(10))
.width(Length::Fill)
.style(|theme: &iced::Theme| {
container::Style::default()
.background(theme.palette().background)
.border(iced::Border::default().rounded(5))
})
.into()
}
60 changes: 60 additions & 0 deletions src/gui/widgets/preamp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use super::labeled_slider;
use crate::gui::amp::{Message, PreampConfig};
use crate::sim::stages::clipper::ClipperType;
use iced::widget::{column, container, pick_list, row, text};
use iced::{Element, Length};

pub fn preamp_widget(idx: usize, cfg: &PreampConfig) -> Element<Message> {
let header = row![
text(format!("Preamp {}", idx + 1)),
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let clipper_types = vec![
ClipperType::Soft,
ClipperType::Medium,
ClipperType::Hard,
ClipperType::Asymmetric,
ClipperType::ClassA,
];

let clipper_picker = row![
text("Clipper:").width(Length::FillPortion(3)),
pick_list(clipper_types, Some(cfg.clipper_type), move |t| {
Message::PreampClipperChanged(idx, t)
})
.width(Length::FillPortion(7)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let body = column![
clipper_picker,
labeled_slider(
"Gain",
0.0..=10.0,
cfg.gain,
move |v| Message::PreampGainChanged(idx, v),
|v| format!("{:.1}", v)
),
labeled_slider(
"Bias",
-1.0..=1.0,
cfg.bias,
move |v| Message::PreampBiasChanged(idx, v),
|v| format!("{:.2}", v)
),
]
.spacing(5);

container(column![header, body].spacing(5).padding(10))
.width(Length::Fill)
.style(|theme: &iced::Theme| {
container::Style::default()
.background(theme.palette().background)
.border(iced::Border::default().rounded(5))
})
.into()
}
73 changes: 73 additions & 0 deletions src/gui/widgets/tonestack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use super::labeled_slider;
use crate::gui::amp::{Message, ToneStackConfig};
use crate::sim::stages::tonestack::ToneStackModel;
use iced::widget::{column, container, pick_list, row, text};
use iced::{Element, Length};

pub fn tonestack_widget(idx: usize, cfg: &ToneStackConfig) -> Element<Message> {
let header = row![
text(format!("Tone Stack {}", idx + 1)),
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let models = vec![
ToneStackModel::Modern,
ToneStackModel::British,
ToneStackModel::American,
ToneStackModel::Flat,
];

let model_picker = row![
text("Model:").width(Length::FillPortion(3)),
pick_list(models, Some(cfg.model), move |m| {
Message::ToneStackModelChanged(idx, m)
})
.width(Length::FillPortion(7)),
]
.spacing(10)
.align_y(iced::Alignment::Center);

let body = column![
model_picker,
labeled_slider(
"Bass",
0.0..=1.0,
cfg.bass,
move |v| Message::ToneStackBassChanged(idx, v),
|v| format!("{:.2}", v)
),
labeled_slider(
"Mid",
0.0..=1.0,
cfg.mid,
move |v| Message::ToneStackMidChanged(idx, v),
|v| format!("{:.2}", v)
),
labeled_slider(
"Treble",
0.0..=1.0,
cfg.treble,
move |v| Message::ToneStackTrebleChanged(idx, v),
|v| format!("{:.2}", v)
),
labeled_slider(
"Presence",
0.0..=1.0,
cfg.presence,
move |v| Message::ToneStackPresenceChanged(idx, v),
|v| format!("{:.2}", v)
),
]
.spacing(5);

container(column![header, body].spacing(5).padding(10))
.width(Length::Fill)
.style(|theme: &iced::Theme| {
container::Style::default()
.background(theme.palette().background)
.border(iced::Border::default().rounded(5))
})
.into()
}
14 changes: 13 additions & 1 deletion src/sim/stages/clipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;

#[derive(ValueEnum, Copy, Clone, Debug, Serialize, Deserialize)]
#[derive(ValueEnum, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum ClipperType {
Soft, // Smooth, tube-like saturation (similar to Tanh)
Medium, // Balanced clipping (similar to ArcTan)
Expand All @@ -11,6 +11,18 @@ pub enum ClipperType {
ClassA, // Classic Class A tube preamp behavior
}

impl std::fmt::Display for ClipperType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClipperType::Soft => write!(f, "Soft Clipping"),
ClipperType::Medium => write!(f, "Medium Clipping"),
ClipperType::Hard => write!(f, "Hard Clipping"),
ClipperType::Asymmetric => write!(f, "Asymmetric Clipping"),
ClipperType::ClassA => write!(f, "Class A Tube Preamp"),
}
}
}

impl ClipperType {
pub fn process(&self, input: f32, drive: f32) -> f32 {
let driven = input * drive;
Expand Down
Loading