| 75 |
75 |
|
});
|
| 76 |
76 |
|
}
|
| 77 |
77 |
|
|
|
78 |
+ |
/// How the equal-divisions count is offered, and what each option submits.
|
|
79 |
+ |
///
|
|
80 |
+ |
/// A fixed set with every member on screen, which is what makes it a described
|
|
81 |
+ |
/// [`makeover_layout::FieldKind::Radio`] rather than a select: five powers of
|
|
82 |
+ |
/// two are the whole question, and hiding four of them behind a closed control
|
|
83 |
+ |
/// would cost more than it saves.
|
|
84 |
+ |
const DIVISION_CHOICES: &[(&str, usize)] = &[("2", 2), ("4", 4), ("8", 8), ("16", 16), ("32", 32)];
|
|
85 |
+ |
|
|
86 |
+ |
/// Grid resolution per beat: what the option submits, and what it reads as.
|
|
87 |
+ |
///
|
|
88 |
+ |
/// The submitted value is the multiplier the grid is computed from and the
|
|
89 |
+ |
/// label is the note it means, which is why these are two strings rather than
|
|
90 |
+ |
/// one formatted at the draw: `4` and `1/16` are the same fact in the two
|
|
91 |
+ |
/// vocabularies this control sits between.
|
|
92 |
+ |
const SUBDIVISION_CHOICES: &[(&str, &str, u32)] =
|
|
93 |
+ |
&[("1", "1/4", 1), ("2", "1/8", 2), ("4", "1/16", 4)];
|
|
94 |
+ |
|
|
95 |
+ |
/// The granularity transient sensitivity moves in.
|
|
96 |
+ |
///
|
|
97 |
+ |
/// Two decimals, the classifier thresholds' granularity for their reason: with
|
|
98 |
+ |
/// no step egui's slider is continuous, and a detector re-run against
|
|
99 |
+ |
/// 0.4300000000000001 is a re-run against a value nobody chose.
|
|
100 |
+ |
const SENSITIVITY_STEP: &str = "0.01";
|
|
101 |
+ |
|
| 78 |
102 |
|
/// Chop controls: method + parameters, preview, and chop.
|
| 79 |
103 |
|
fn draw_chop_section(ui: &mut egui::Ui, state: &mut BrowserState) {
|
| 80 |
104 |
|
let disabled = state.forge.busy;
|
| 81 |
|
- |
ui.label(egui::RichText::new("Chop").strong());
|
|
105 |
+ |
let state_when_busy = disabled.then_some(makeover_layout::State::Disabled);
|
| 82 |
106 |
|
|
| 83 |
|
- |
ui.horizontal(|ui| {
|
| 84 |
|
- |
if ui
|
| 85 |
|
- |
.add_enabled(
|
| 86 |
|
- |
!disabled,
|
| 87 |
|
- |
egui::RadioButton::new(state.forge.chop_mode == ChopMode::Transient, "Transient"),
|
| 88 |
|
- |
)
|
| 89 |
|
- |
.clicked()
|
| 90 |
|
- |
{
|
| 91 |
|
- |
state.forge.chop_mode = ChopMode::Transient;
|
| 92 |
|
- |
state.forge.slice_marks.clear();
|
| 93 |
|
- |
}
|
| 94 |
|
- |
if ui
|
| 95 |
|
- |
.add_enabled(
|
| 96 |
|
- |
!disabled,
|
| 97 |
|
- |
egui::RadioButton::new(state.forge.chop_mode == ChopMode::Equal, "Divisions"),
|
| 98 |
|
- |
)
|
| 99 |
|
- |
.clicked()
|
| 100 |
|
- |
{
|
| 101 |
|
- |
state.forge.chop_mode = ChopMode::Equal;
|
| 102 |
|
- |
state.forge.slice_marks.clear();
|
| 103 |
|
- |
}
|
| 104 |
|
- |
if ui
|
| 105 |
|
- |
.add_enabled(
|
| 106 |
|
- |
!disabled,
|
| 107 |
|
- |
egui::RadioButton::new(state.forge.chop_mode == ChopMode::Bpm, "BPM grid"),
|
| 108 |
|
- |
)
|
| 109 |
|
- |
.clicked()
|
| 110 |
|
- |
{
|
| 111 |
|
- |
state.forge.chop_mode = ChopMode::Bpm;
|
| 112 |
|
- |
state.forge.slice_marks.clear();
|
| 113 |
|
- |
}
|
| 114 |
|
- |
});
|
|
107 |
+ |
// The method question, described. It was three `RadioButton`s built by
|
|
108 |
+ |
// hand, which is what kept this section invisible to five sweeps: every one
|
|
109 |
+ |
// of them matched `ui.radio(`, and these are `ui.add_enabled` with a widget
|
|
110 |
+ |
// argument, the one hole the 2026-08-16 frequency table named and did not
|
|
111 |
+ |
// close.
|
|
112 |
+ |
//
|
|
113 |
+ |
// The section's own `strong` "Chop" line is gone, the conform picker's
|
|
114 |
+ |
// precedent: the label belongs to the question, and a heading repeating it
|
|
115 |
+ |
// above the control is the same words twice.
|
|
116 |
+ |
let mode_options = [
|
|
117 |
+ |
makeover_layout::Choice::new(ChopMode::Transient.as_value(), "Transient"),
|
|
118 |
+ |
makeover_layout::Choice::new(ChopMode::Equal.as_value(), "Divisions"),
|
|
119 |
+ |
makeover_layout::Choice::new(ChopMode::Bpm.as_value(), "BPM grid"),
|
|
120 |
+ |
];
|
|
121 |
+ |
let described = makeover_layout::Field::radio("chop_mode", "Chop", &mode_options);
|
|
122 |
+ |
let was = state.forge.chop_mode;
|
|
123 |
+ |
let mut value = was.as_value().to_owned();
|
|
124 |
+ |
widgets::field(
|
|
125 |
+ |
ui,
|
|
126 |
+ |
&described,
|
|
127 |
+ |
makeover_immediate::Filling::Text(&mut value),
|
|
128 |
+ |
state_when_busy,
|
|
129 |
+ |
);
|
|
130 |
+ |
// `from_value` returning `None` is drift between the options and this
|
|
131 |
+ |
// pairing, and leaves the mode alone rather than guessing at one.
|
|
132 |
+ |
if let Some(mode) = ChopMode::from_value(&value)
|
|
133 |
+ |
&& mode != was
|
|
134 |
+ |
{
|
|
135 |
+ |
state.forge.chop_mode = mode;
|
|
136 |
+ |
state.forge.slice_marks.clear();
|
|
137 |
+ |
}
|
| 115 |
138 |
|
|
| 116 |
139 |
|
match state.forge.chop_mode {
|
| 117 |
140 |
|
ChopMode::Transient => {
|
| 118 |
|
- |
ui.horizontal(|ui| {
|
| 119 |
|
- |
ui.label("Sensitivity:");
|
| 120 |
|
- |
if ui
|
| 121 |
|
- |
.add_enabled(
|
| 122 |
|
- |
!disabled,
|
| 123 |
|
- |
egui::Slider::new(&mut state.forge.sensitivity, 0.0..=1.0),
|
| 124 |
|
- |
)
|
| 125 |
|
- |
.changed()
|
| 126 |
|
- |
{
|
| 127 |
|
- |
state.forge.slice_marks.clear();
|
| 128 |
|
- |
}
|
| 129 |
|
- |
});
|
|
141 |
+ |
// A described `Range`: 0 is every dip in the waveform and 1 is only
|
|
142 |
+ |
// the loudest onsets, so the two ends are the question and a bare
|
|
143 |
+ |
// 0.43 in a well would not be a quieter version of this control.
|
|
144 |
+ |
let described = makeover_layout::Field {
|
|
145 |
+ |
step: Some(SENSITIVITY_STEP),
|
|
146 |
+ |
..makeover_layout::Field::range("sensitivity", "Sensitivity", "0", "1")
|
|
147 |
+ |
};
|
|
148 |
+ |
let mut text = format!("{:.2}", state.forge.sensitivity);
|
|
149 |
+ |
widgets::field(
|
|
150 |
+ |
ui,
|
|
151 |
+ |
&described,
|
|
152 |
+ |
makeover_immediate::Filling::Text(&mut text),
|
|
153 |
+ |
state_when_busy,
|
|
154 |
+ |
);
|
|
155 |
+ |
if let Ok(parsed) = text.parse::<f32>()
|
|
156 |
+ |
&& (parsed - state.forge.sensitivity).abs() > f32::EPSILON
|
|
157 |
+ |
{
|
|
158 |
+ |
state.forge.sensitivity = parsed;
|
|
159 |
+ |
state.forge.slice_marks.clear();
|
|
160 |
+ |
}
|
| 130 |
161 |
|
}
|
| 131 |
162 |
|
ChopMode::Equal => {
|
| 132 |
|
- |
ui.horizontal(|ui| {
|
| 133 |
|
- |
ui.label("Slices:");
|
| 134 |
|
- |
for n in [2usize, 4, 8, 16, 32] {
|
| 135 |
|
- |
if ui
|
| 136 |
|
- |
.add_enabled(
|
| 137 |
|
- |
!disabled,
|
| 138 |
|
- |
egui::Button::selectable(state.forge.divisions == n, n.to_string()),
|
| 139 |
|
- |
)
|
| 140 |
|
- |
.clicked()
|
| 141 |
|
- |
{
|
| 142 |
|
- |
state.forge.divisions = n;
|
| 143 |
|
- |
state.forge.slice_marks.clear();
|
| 144 |
|
- |
}
|
| 145 |
|
- |
}
|
| 146 |
|
- |
});
|
|
163 |
+ |
let options: Vec<makeover_layout::Choice<'_>> = DIVISION_CHOICES
|
|
164 |
+ |
.iter()
|
|
165 |
+ |
.map(|(value, _)| makeover_layout::Choice::new(value, value))
|
|
166 |
+ |
.collect();
|
|
167 |
+ |
let described = makeover_layout::Field::radio("divisions", "Slices", &options);
|
|
168 |
+ |
let mut value = state.forge.divisions.to_string();
|
|
169 |
+ |
widgets::field(
|
|
170 |
+ |
ui,
|
|
171 |
+ |
&described,
|
|
172 |
+ |
makeover_immediate::Filling::Text(&mut value),
|
|
173 |
+ |
state_when_busy,
|
|
174 |
+ |
);
|
|
175 |
+ |
if let Some((_, chosen)) = DIVISION_CHOICES.iter().find(|(v, _)| *v == value)
|
|
176 |
+ |
&& *chosen != state.forge.divisions
|
|
177 |
+ |
{
|
|
178 |
+ |
state.forge.divisions = *chosen;
|
|
179 |
+ |
state.forge.slice_marks.clear();
|
|
180 |
+ |
}
|
| 147 |
181 |
|
}
|
| 148 |
182 |
|
ChopMode::Bpm => {
|
|
183 |
+ |
// The tempo stays a `DragValue` and is the one control in this
|
|
184 |
+ |
// section that is not described. It is a validated number rather
|
|
185 |
+ |
// than a range -- 20 and 300 are guard rails on an answer people
|
|
186 |
+ |
// arrive with, not the two ends the question means -- and a
|
|
187 |
+ |
// described `Number` is a text well here, which needs a buffer that
|
|
188 |
+ |
// survives frames. `state.forge.bpm` is an `f32`, so a half-typed
|
|
189 |
+ |
// "2" on the way to "200" would re-grid the sample at 2 BPM.
|
| 149 |
190 |
|
ui.horizontal(|ui| {
|
| 150 |
191 |
|
ui.label("BPM:");
|
| 151 |
192 |
|
if ui
|
| 159 |
200 |
|
{
|
| 160 |
201 |
|
state.forge.slice_marks.clear();
|
| 161 |
202 |
|
}
|
| 162 |
|
- |
ui.label("Per beat:");
|
| 163 |
|
- |
for n in [1u32, 2, 4] {
|
| 164 |
|
- |
let label = match n {
|
| 165 |
|
- |
1 => "1/4",
|
| 166 |
|
- |
2 => "1/8",
|
| 167 |
|
- |
_ => "1/16",
|
| 168 |
|
- |
};
|
| 169 |
|
- |
if ui
|
| 170 |
|
- |
.add_enabled(
|
| 171 |
|
- |
!disabled,
|
| 172 |
|
- |
egui::Button::selectable(state.forge.subdivisions == n, label),
|
| 173 |
|
- |
)
|
| 174 |
|
- |
.clicked()
|
| 175 |
|
- |
{
|
| 176 |
|
- |
state.forge.subdivisions = n;
|
| 177 |
|
- |
state.forge.slice_marks.clear();
|
| 178 |
|
- |
}
|
| 179 |
|
- |
}
|
| 180 |
203 |
|
});
|
|
204 |
+ |
|
|
205 |
+ |
let options: Vec<makeover_layout::Choice<'_>> = SUBDIVISION_CHOICES
|
|
206 |
+ |
.iter()
|
|
207 |
+ |
.map(|(value, label, _)| makeover_layout::Choice::new(value, label))
|
|
208 |
+ |
.collect();
|
|
209 |
+ |
let described = makeover_layout::Field::radio("subdivisions", "Per beat", &options);
|
|
210 |
+ |
let mut value = state.forge.subdivisions.to_string();
|
|
211 |
+ |
widgets::field(
|
|
212 |
+ |
ui,
|
|
213 |
+ |
&described,
|
|
214 |
+ |
makeover_immediate::Filling::Text(&mut value),
|
|
215 |
+ |
state_when_busy,
|
|
216 |
+ |
);
|
|
217 |
+ |
if let Some((_, _, chosen)) = SUBDIVISION_CHOICES.iter().find(|(v, _, _)| *v == value)
|
|
218 |
+ |
&& *chosen != state.forge.subdivisions
|
|
219 |
+ |
{
|
|
220 |
+ |
state.forge.subdivisions = *chosen;
|
|
221 |
+ |
state.forge.slice_marks.clear();
|
|
222 |
+ |
}
|
| 181 |
223 |
|
}
|
| 182 |
224 |
|
}
|
| 183 |
225 |
|
|
| 302 |
344 |
|
);
|
| 303 |
345 |
|
}
|
| 304 |
346 |
|
|
|
347 |
+ |
/// The granularity the trim threshold moves in.
|
|
348 |
+ |
///
|
|
349 |
+ |
/// A whole decibel, which is what the `DragValue`'s `speed(1.0)` already meant
|
|
350 |
+ |
/// and what the threshold is stored and compared at. Without it egui's own
|
|
351 |
+ |
/// granularity is continuous, so a drag writes back -63.417 and the batch runs
|
|
352 |
+ |
/// against a floor nobody chose.
|
|
353 |
+ |
const TRIM_THRESHOLD_STEP: &str = "1";
|
|
354 |
+ |
|
| 305 |
355 |
|
/// Batch section: trim silence across the current multi-selection. Batch
|
| 306 |
356 |
|
/// normalize/gain live in the Sample Editor's batch section.
|
| 307 |
357 |
|
fn draw_batch_section(ui: &mut egui::Ui, state: &mut BrowserState) {
|
| 316 |
366 |
|
return;
|
| 317 |
367 |
|
}
|
| 318 |
368 |
|
|
| 319 |
|
- |
ui.horizontal(|ui| {
|
| 320 |
|
- |
ui.label("Threshold:");
|
| 321 |
|
- |
ui.add(
|
| 322 |
|
- |
egui::DragValue::new(&mut state.forge.trim_threshold_db)
|
| 323 |
|
- |
.speed(1.0)
|
| 324 |
|
- |
.range(-96.0..=-20.0)
|
| 325 |
|
- |
.suffix(" dBFS"),
|
| 326 |
|
- |
);
|
| 327 |
|
- |
});
|
|
369 |
+ |
// A described `Range`, on the classifier thresholds' reasoning: the two
|
|
370 |
+ |
// ends are what the question means. -96 dBFS is the noise floor of a
|
|
371 |
+ |
// 16-bit file and -20 is loud enough to eat a quiet tail, so a bare -63
|
|
372 |
+ |
// says nothing without both of them beside it, and there is no answer
|
|
373 |
+ |
// outside the pair to be told about afterwards.
|
|
374 |
+ |
//
|
|
375 |
+ |
// The unit is in the label because a described field has nowhere else to
|
|
376 |
+ |
// put one: there is no `suffix`, and a dBFS reading with no unit is a
|
|
377 |
+ |
// number in the wrong scale for anyone who reads it as a percentage.
|
|
378 |
+ |
let described = makeover_layout::Field {
|
|
379 |
+ |
step: Some(TRIM_THRESHOLD_STEP),
|
|
380 |
+ |
..makeover_layout::Field::range("trim_threshold", "Threshold (dBFS)", "-96", "-20")
|
|
381 |
+ |
};
|
|
382 |
+ |
let mut text = format!("{:.0}", state.forge.trim_threshold_db);
|
|
383 |
+ |
widgets::field(
|
|
384 |
+ |
ui,
|
|
385 |
+ |
&described,
|
|
386 |
+ |
makeover_immediate::Filling::Text(&mut text),
|
|
387 |
+ |
None,
|
|
388 |
+ |
);
|
|
389 |
+ |
if let Ok(parsed) = text.parse::<f64>() {
|
|
390 |
+ |
state.forge.trim_threshold_db = parsed;
|
|
391 |
+ |
}
|
| 328 |
392 |
|
let threshold = state.forge.trim_threshold_db;
|
| 329 |
393 |
|
if ui
|
| 330 |
394 |
|
.button(format!("Trim silence on {count} samples"))
|