Skip to content

Commit 072d9bd

Browse files
authored
Add Widgets for Stages (#11)
* Add Widgets for Stages * PR feedback, and step to slider widget
1 parent d865fbd commit 072d9bd

File tree

12 files changed

+791
-110
lines changed

12 files changed

+791
-110
lines changed

src/gui/amp.rs

Lines changed: 385 additions & 100 deletions
Large diffs are not rendered by default.

src/gui/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod amp;
2+
pub mod widgets;

src/gui/widgets/compressor.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use super::labeled_slider;
2+
use crate::gui::amp::{CompressorConfig, Message};
3+
use iced::widget::{column, container, row, text};
4+
use iced::{Element, Length};
5+
6+
pub fn compressor_widget(idx: usize, cfg: &CompressorConfig) -> Element<Message> {
7+
let header = row![
8+
text(format!("Compressor {}", idx + 1)),
9+
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
10+
]
11+
.spacing(10)
12+
.align_y(iced::Alignment::Center);
13+
14+
let body = column![
15+
labeled_slider(
16+
"Threshold",
17+
-60.0..=0.0,
18+
cfg.threshold_db,
19+
move |v| Message::CompressorThresholdChanged(idx, v),
20+
|v| format!("{:.1} dB", v),
21+
1.0
22+
),
23+
labeled_slider(
24+
"Ratio",
25+
1.0..=20.0,
26+
cfg.ratio,
27+
move |v| Message::CompressorRatioChanged(idx, v),
28+
|v| format!("{:.1}:1", v),
29+
1.0
30+
),
31+
labeled_slider(
32+
"Attack",
33+
0.1..=100.0,
34+
cfg.attack_ms,
35+
move |v| Message::CompressorAttackChanged(idx, v),
36+
|v| format!("{:.1} ms", v),
37+
1.0
38+
),
39+
labeled_slider(
40+
"Release",
41+
10.0..=1000.0,
42+
cfg.release_ms,
43+
move |v| Message::CompressorReleaseChanged(idx, v),
44+
|v| format!("{:.0} ms", v),
45+
1.0
46+
),
47+
labeled_slider(
48+
"Makeup",
49+
-12.0..=24.0,
50+
cfg.makeup_db,
51+
move |v| Message::CompressorMakeupChanged(idx, v),
52+
|v| format!("{:.2} dB", v),
53+
1.0
54+
),
55+
]
56+
.spacing(5);
57+
58+
container(column![header, body].spacing(5).padding(10))
59+
.width(Length::Fill)
60+
.style(|theme: &iced::Theme| {
61+
container::Style::default()
62+
.background(theme.palette().background)
63+
.border(iced::Border::default().rounded(5))
64+
})
65+
.into()
66+
}

src/gui/widgets/filter.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use super::labeled_slider;
2+
use crate::gui::amp::{FilterConfig, Message};
3+
use crate::sim::stages::filter::FilterType;
4+
use iced::widget::{column, container, pick_list, row, text};
5+
use iced::{Element, Length};
6+
7+
const FILTER_TYPES: [FilterType; 4] = [
8+
FilterType::Highpass,
9+
FilterType::Lowpass,
10+
FilterType::Bandpass,
11+
FilterType::Notch,
12+
];
13+
14+
pub fn filter_widget(idx: usize, cfg: &FilterConfig) -> Element<Message> {
15+
let header = row![
16+
text(format!("Filter {}", idx + 1)),
17+
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
18+
]
19+
.spacing(10)
20+
.align_y(iced::Alignment::Center);
21+
22+
let type_picker = row![
23+
text("Type:").width(Length::FillPortion(3)),
24+
pick_list(FILTER_TYPES, Some(cfg.filter_type), move |t| {
25+
Message::FilterTypeChanged(idx, t)
26+
})
27+
.width(Length::FillPortion(7)),
28+
]
29+
.spacing(10)
30+
.align_y(iced::Alignment::Center);
31+
32+
let body = column![
33+
type_picker,
34+
labeled_slider(
35+
"Cutoff",
36+
20.0..=20_000.0,
37+
cfg.cutoff_hz,
38+
move |v| Message::FilterCutoffChanged(idx, v),
39+
|v| format!("{:.0} Hz", v),
40+
0.05
41+
),
42+
labeled_slider(
43+
"Resonance",
44+
0.0..=1.0,
45+
cfg.resonance,
46+
move |v| Message::FilterResonanceChanged(idx, v),
47+
|v| format!("{:.2}", v),
48+
0.05
49+
),
50+
]
51+
.spacing(5);
52+
53+
container(column![header, body].spacing(5).padding(10))
54+
.width(Length::Fill)
55+
.style(|theme: &iced::Theme| {
56+
container::Style::default()
57+
.background(theme.palette().background)
58+
.border(iced::Border::default().rounded(5))
59+
})
60+
.into()
61+
}

src/gui/widgets/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
pub mod compressor;
2+
pub mod filter;
3+
pub mod poweramp;
4+
pub mod preamp;
5+
pub mod tonestack;
6+
7+
use crate::gui::amp::Message;
8+
use iced::{Element, Length};
9+
10+
pub fn labeled_slider<'a, F: 'a + Fn(f32) -> Message>(
11+
label: &'a str,
12+
range: std::ops::RangeInclusive<f32>,
13+
value: f32,
14+
on_change: F,
15+
format: impl Fn(f32) -> String + 'a,
16+
step: f32,
17+
) -> Element<'a, Message> {
18+
use iced::Alignment;
19+
use iced::widget::{row, slider, text};
20+
21+
row![
22+
text(label).width(Length::FillPortion(3)),
23+
slider(range, value, on_change)
24+
.width(Length::FillPortion(5))
25+
.step(step),
26+
text(format(value)).width(Length::FillPortion(2)),
27+
]
28+
.spacing(10)
29+
.align_y(Alignment::Center)
30+
.into()
31+
}

src/gui/widgets/poweramp.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use super::labeled_slider;
2+
use crate::gui::amp::{Message, PowerAmpConfig};
3+
use crate::sim::stages::poweramp::PowerAmpType;
4+
use iced::widget::{column, container, pick_list, row, text};
5+
use iced::{Element, Length};
6+
7+
const POWER_AMP_TYPES: [PowerAmpType; 3] = [
8+
PowerAmpType::ClassA,
9+
PowerAmpType::ClassAB,
10+
PowerAmpType::ClassB,
11+
];
12+
13+
pub fn poweramp_widget(idx: usize, cfg: &PowerAmpConfig) -> Element<Message> {
14+
let header = row![
15+
text(format!("Power Amp {}", idx + 1)),
16+
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
17+
]
18+
.spacing(10)
19+
.align_y(iced::Alignment::Center);
20+
21+
let type_picker = row![
22+
text("Type:").width(Length::FillPortion(3)),
23+
pick_list(POWER_AMP_TYPES, Some(cfg.amp_type), move |t| {
24+
Message::PowerAmpTypeChanged(idx, t)
25+
})
26+
.width(Length::FillPortion(7)),
27+
]
28+
.spacing(10)
29+
.align_y(iced::Alignment::Center);
30+
31+
let body = column![
32+
type_picker,
33+
labeled_slider(
34+
"Drive",
35+
0.0..=1.0,
36+
cfg.drive,
37+
move |v| Message::PowerAmpDriveChanged(idx, v),
38+
|v| format!("{:.2}", v),
39+
0.1
40+
),
41+
labeled_slider(
42+
"Sag",
43+
0.0..=1.0,
44+
cfg.sag,
45+
move |v| Message::PowerAmpSagChanged(idx, v),
46+
|v| format!("{:.2}", v),
47+
0.1
48+
),
49+
]
50+
.spacing(5);
51+
52+
container(column![header, body].spacing(5).padding(10))
53+
.width(Length::Fill)
54+
.style(|theme: &iced::Theme| {
55+
container::Style::default()
56+
.background(theme.palette().background)
57+
.border(iced::Border::default().rounded(5))
58+
})
59+
.into()
60+
}

src/gui/widgets/preamp.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use super::labeled_slider;
2+
use crate::gui::amp::{Message, PreampConfig};
3+
use crate::sim::stages::clipper::ClipperType;
4+
use iced::widget::{column, container, pick_list, row, text};
5+
use iced::{Element, Length};
6+
7+
const CLIPPER_TYPES: [ClipperType; 5] = [
8+
ClipperType::Soft,
9+
ClipperType::Medium,
10+
ClipperType::Hard,
11+
ClipperType::Asymmetric,
12+
ClipperType::ClassA,
13+
];
14+
15+
pub fn preamp_widget(idx: usize, cfg: &PreampConfig) -> Element<Message> {
16+
let header = row![
17+
text(format!("Preamp {}", idx + 1)),
18+
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
19+
]
20+
.spacing(10)
21+
.align_y(iced::Alignment::Center);
22+
23+
let clipper_picker = row![
24+
text("Clipper:").width(Length::FillPortion(3)),
25+
pick_list(CLIPPER_TYPES, Some(cfg.clipper_type), move |t| {
26+
Message::PreampClipperChanged(idx, t)
27+
})
28+
.width(Length::FillPortion(7)),
29+
]
30+
.spacing(10)
31+
.align_y(iced::Alignment::Center);
32+
33+
let body = column![
34+
clipper_picker,
35+
labeled_slider(
36+
"Gain",
37+
0.0..=10.0,
38+
cfg.gain,
39+
move |v| Message::PreampGainChanged(idx, v),
40+
|v| format!("{:.1}", v),
41+
1.0
42+
),
43+
labeled_slider(
44+
"Bias",
45+
-1.0..=1.0,
46+
cfg.bias,
47+
move |v| Message::PreampBiasChanged(idx, v),
48+
|v| format!("{:.2}", v),
49+
0.1
50+
),
51+
]
52+
.spacing(5);
53+
54+
container(column![header, body].spacing(5).padding(10))
55+
.width(Length::Fill)
56+
.style(|theme: &iced::Theme| {
57+
container::Style::default()
58+
.background(theme.palette().background)
59+
.border(iced::Border::default().rounded(5))
60+
})
61+
.into()
62+
}

src/gui/widgets/tonestack.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use super::labeled_slider;
2+
use crate::gui::amp::{Message, ToneStackConfig};
3+
use crate::sim::stages::tonestack::ToneStackModel;
4+
use iced::widget::{column, container, pick_list, row, text};
5+
use iced::{Element, Length};
6+
7+
const TONE_STACK_MODELS: [ToneStackModel; 4] = [
8+
ToneStackModel::Modern,
9+
ToneStackModel::British,
10+
ToneStackModel::American,
11+
ToneStackModel::Flat,
12+
];
13+
14+
pub fn tonestack_widget(idx: usize, cfg: &ToneStackConfig) -> Element<Message> {
15+
let header = row![
16+
text(format!("Tone Stack {}", idx + 1)),
17+
iced::widget::button("x").on_press(Message::RemoveStage(idx)),
18+
]
19+
.spacing(10)
20+
.align_y(iced::Alignment::Center);
21+
22+
let model_picker = row![
23+
text("Model:").width(Length::FillPortion(3)),
24+
pick_list(TONE_STACK_MODELS, Some(cfg.model), move |m| {
25+
Message::ToneStackModelChanged(idx, m)
26+
})
27+
.width(Length::FillPortion(7)),
28+
]
29+
.spacing(10)
30+
.align_y(iced::Alignment::Center);
31+
32+
let body = column![
33+
model_picker,
34+
labeled_slider(
35+
"Bass",
36+
0.0..=1.0,
37+
cfg.bass,
38+
move |v| Message::ToneStackBassChanged(idx, v),
39+
|v| format!("{:.2}", v),
40+
0.1
41+
),
42+
labeled_slider(
43+
"Mid",
44+
0.0..=1.0,
45+
cfg.mid,
46+
move |v| Message::ToneStackMidChanged(idx, v),
47+
|v| format!("{:.2}", v),
48+
0.1
49+
),
50+
labeled_slider(
51+
"Treble",
52+
0.0..=1.0,
53+
cfg.treble,
54+
move |v| Message::ToneStackTrebleChanged(idx, v),
55+
|v| format!("{:.2}", v),
56+
0.1
57+
),
58+
labeled_slider(
59+
"Presence",
60+
0.0..=1.0,
61+
cfg.presence,
62+
move |v| Message::ToneStackPresenceChanged(idx, v),
63+
|v| format!("{:.2}", v),
64+
0.1
65+
),
66+
]
67+
.spacing(5);
68+
69+
container(column![header, body].spacing(5).padding(10))
70+
.width(Length::Fill)
71+
.style(|theme: &iced::Theme| {
72+
container::Style::default()
73+
.background(theme.palette().background)
74+
.border(iced::Border::default().rounded(5))
75+
})
76+
.into()
77+
}

src/sim/stages/clipper.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use clap::ValueEnum;
22
use serde::{Deserialize, Serialize};
33
use std::f32::consts::PI;
44

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

14+
impl std::fmt::Display for ClipperType {
15+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16+
match self {
17+
ClipperType::Soft => write!(f, "Soft Clipping"),
18+
ClipperType::Medium => write!(f, "Medium Clipping"),
19+
ClipperType::Hard => write!(f, "Hard Clipping"),
20+
ClipperType::Asymmetric => write!(f, "Asymmetric Clipping"),
21+
ClipperType::ClassA => write!(f, "Class A Tube Preamp"),
22+
}
23+
}
24+
}
25+
1426
impl ClipperType {
1527
pub fn process(&self, input: f32, drive: f32) -> f32 {
1628
let driven = input * drive;

0 commit comments

Comments
 (0)