max / audiofiles
2 files changed,
+188 insertions,
-315 deletions
| @@ -4257,7 +4257,7 @@ | |||
| 4257 | 4257 | ||
| 4258 | 4258 | [[package]] | |
| 4259 | 4259 | name = "quasi-declare" | |
| 4260 | - | version = "0.1.3" | |
| 4260 | + | version = "0.1.4" | |
| 4261 | 4261 | dependencies = [ | |
| 4262 | 4262 | "proc-macro2", | |
| 4263 | 4263 | "quote", | |
| @@ -7566,6 +7566,22 @@ | |||
| 7566 | 7566 | "winnow 1.0.4", | |
| 7567 | 7567 | ] | |
| 7568 | 7568 | ||
| 7569 | + | [[patch.unused]] | |
| 7570 | + | name = "kberg" | |
| 7571 | + | version = "0.1.0" | |
| 7572 | + | ||
| 7573 | + | [[patch.unused]] | |
| 7574 | + | name = "ops-status" | |
| 7575 | + | version = "0.1.0" | |
| 7576 | + | ||
| 7577 | + | [[patch.unused]] | |
| 7578 | + | name = "painhours" | |
| 7579 | + | version = "0.1.0" | |
| 7580 | + | ||
| 7581 | + | [[patch.unused]] | |
| 7582 | + | name = "quasi-type" | |
| 7583 | + | version = "0.1.3" | |
| 7584 | + | ||
| 7569 | 7585 | [[patch.unused]] | |
| 7570 | 7586 | name = "quasi-axum" | |
| 7571 | 7587 | version = "0.101.1" | |
| @@ -7593,19 +7609,3 @@ | |||
| 7593 | 7609 | [[patch.unused]] | |
| 7594 | 7610 | name = "quasi-webview" | |
| 7595 | 7611 | version = "0.101.1" | |
| 7596 | - | ||
| 7597 | - | [[patch.unused]] | |
| 7598 | - | name = "quasi-type" | |
| 7599 | - | version = "0.1.3" | |
| 7600 | - | ||
| 7601 | - | [[patch.unused]] | |
| 7602 | - | name = "kberg" | |
| 7603 | - | version = "0.1.0" | |
| 7604 | - | ||
| 7605 | - | [[patch.unused]] | |
| 7606 | - | name = "ops-status" | |
| 7607 | - | version = "0.1.0" | |
| 7608 | - | ||
| 7609 | - | [[patch.unused]] | |
| 7610 | - | name = "painhours" | |
| 7611 | - | version = "0.1.0" |
| @@ -96,11 +96,9 @@ | |||
| 96 | 96 | //! Described as `Act::disabled` on what an edit would start, which says the | |
| 97 | 97 | //! same thing without naming a colour. | |
| 98 | 98 | ||
| 99 | - | use quasi_router::layout::{FieldKind, Tone}; | |
| 100 | - | use quasi_router::{ | |
| 101 | - | Act, Action, Choice, Consult, Field, Figure, Node, RegionKind, Request, Response, RouteError, | |
| 102 | - | Router, Screen, Slot, | |
| 103 | - | }; | |
| 99 | + | use quasi_declare::declare; | |
| 100 | + | use quasi_router::layout::Tone; | |
| 101 | + | use quasi_router::{Action, Choice, Consult, Request, Response, RouteError, Router}; | |
| 104 | 102 | ||
| 105 | 103 | use audiofiles_core::edit::FadeCurve; | |
| 106 | 104 | ||
| @@ -173,18 +171,7 @@ | |||
| 173 | 171 | ||
| 174 | 172 | /// `GET /edit` | |
| 175 | 173 | fn screen(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 176 | - | Ok(editor(state)?.into()) | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | /// The whole editor, at whichever of its two shapes it is in. | |
| 180 | - | fn editor(state: &Panels<'_>) -> Result<Screen, RouteError> { | |
| 181 | - | let sample = subject(state)?; | |
| 182 | - | let body = if sample.asking { | |
| 183 | - | asking(&sample) | |
| 184 | - | } else { | |
| 185 | - | editing(&sample) | |
| 186 | - | }; | |
| 187 | - | Ok(Screen::sidebar_content("Sample Editor").with(body)) | |
| 174 | + | showing(state) | |
| 188 | 175 | } | |
| 189 | 176 | ||
| 190 | 177 | /// What is being edited, or a refusal. | |
| @@ -199,274 +186,183 @@ | |||
| 199 | 186 | .ok_or_else(|| RouteError::not_found("nothing is being edited")) | |
| 200 | 187 | } | |
| 201 | 188 | ||
| 189 | + | /// The editor, read and then described. | |
| 190 | + | pub(super) fn showing(state: &Panels<'_>) -> Result<Response, RouteError> { | |
| 191 | + | Ok(Response::from(editor(&read(state)?))) | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | /// What the editor draws, read off the app. | |
| 195 | + | struct Edit { | |
| 196 | + | /// The question a finished edit is waiting on, while one is waiting. | |
| 197 | + | asking: Option<Asking>, | |
| 198 | + | /// The editor proper, while nothing is waiting. | |
| 199 | + | working_on: Option<Sample>, | |
| 200 | + | } | |
| 201 | + | ||
| 202 | 202 | /// The question a finished edit is waiting on. | |
| 203 | - | /// | |
| 204 | - | /// A form rather than the shipped three buttons, and the reason is a gap: | |
| 205 | - | /// "Replace Original" and "Create Sibling" each need to carry the answer to | |
| 206 | - | /// "Remember my choice", and an `Act` cannot carry a value another control is | |
| 207 | - | /// holding. That is makeover-layout `28a777df` (a control carries an action but | |
| 208 | - | /// no computed payload), and this is a consumer of it. Discard stays an act | |
| 209 | - | /// because it carries nothing. | |
| 210 | - | fn asking(sample: &Editing) -> Slot { | |
| 211 | - | Slot::new(BODY, RegionKind::Pane) | |
| 212 | - | .with(Node::page("Edit Result")) | |
| 213 | - | .with(Node::text("How should the edited sample be handled?")) | |
| 214 | - | .with(Node::Form { | |
| 215 | - | fields: vec![ | |
| 216 | - | Field::radio(RESULT, "Result", result_modes()).value( | |
| 217 | - | sample | |
| 218 | - | .result | |
| 219 | - | .clone() | |
| 220 | - | .unwrap_or_else(|| EditResultMode::Sibling.as_value().to_owned()), | |
| 221 | - | ), | |
| 222 | - | // The shipped checkbox is ticked when a standing answer exists, | |
| 223 | - | // which is the same fact this reads. | |
| 224 | - | Field::new(FieldKind::Checkbox, REMEMBER, "Remember my choice") | |
| 225 | - | .value(if sample.result.is_some() { "on" } else { "" }), | |
| 226 | - | ], | |
| 227 | - | submit: "Use this".to_owned(), | |
| 228 | - | action: Action::post("/edit/result/choose"), | |
| 229 | - | }) | |
| 230 | - | .with(Node::Act( | |
| 231 | - | Act::new("Discard edit", Action::post("/edit/result/discard")) | |
| 232 | - | .tone(Tone::Danger) | |
| 233 | - | .confirm("Throw this edit away?"), | |
| 234 | - | )) | |
| 203 | + | struct Asking { | |
| 204 | + | /// The two answers to "Result". | |
| 205 | + | modes: Vec<Choice>, | |
| 206 | + | /// The standing answer, or the one this screen defaults to. | |
| 207 | + | chosen: String, | |
| 208 | + | /// Whether a standing answer exists, which is what the checkbox reads. | |
| 209 | + | /// | |
| 210 | + | /// The shipped checkbox is ticked when a standing answer exists, which is | |
| 211 | + | /// the same fact this reads. | |
| 212 | + | remembered: &'static str, | |
| 235 | 213 | } | |
| 236 | 214 | ||
| 237 | - | /// The editor proper. | |
| 238 | - | fn editing(sample: &Editing) -> Slot { | |
| 239 | - | let mut body = Slot::new(BODY, RegionKind::Pane).with(Node::page(sample.name.clone())); | |
| 240 | - | ||
| 241 | - | // What the shipped info line says, as facts rather than one muted string. | |
| 242 | - | body = body.with(Node::Figure(Figure::new( | |
| 243 | - | sample.sample_rate.to_string(), | |
| 244 | - | "Hz", | |
| 245 | - | ))); | |
| 246 | - | if let Some(duration) = sample.duration { | |
| 247 | - | body = body.with(Node::Figure(Figure::new( | |
| 248 | - | format!("{duration:.3}"), | |
| 249 | - | "seconds", | |
| 250 | - | ))); | |
| 251 | - | } | |
| 252 | - | if let Some(peak) = sample.peak_db { | |
| 253 | - | body = body.with(Node::Figure(Figure::new(format!("{peak:.1}"), "dBFS"))); | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | if sample.working { | |
| 257 | - | body = body | |
| 258 | - | .with(Node::banner(Tone::Info, "Applying edit...")) | |
| 259 | - | .with(Node::Act(Act::new("Cancel", Action::post("/edit/cancel")))); | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | body = transport(body, sample); | |
| 263 | - | body = trim_section(body); | |
| 264 | - | body = levels(body, sample); | |
| 265 | - | body = transform(body, sample); | |
| 266 | - | body = silence(body, sample); | |
| 267 | - | body = result(body, sample); | |
| 268 | - | batch(body, sample) | |
| 269 | - | } | |
| 270 | - | ||
| 271 | - | /// Play, pause and stop, independent of the main list's selection. | |
| 272 | - | fn transport(body: Slot, sample: &Editing) -> Slot { | |
| 273 | - | body.with(Node::Act( | |
| 274 | - | Act::new( | |
| 275 | - | if sample.playing { "Pause" } else { "Play" }, | |
| 276 | - | Action::post("/edit/play"), | |
| 277 | - | ) | |
| 278 | - | .key("space"), | |
| 279 | - | )) | |
| 280 | - | .with(Node::Act(Act::new("Stop", Action::post("/edit/stop")))) | |
| 281 | - | } | |
| 282 | - | ||
| 283 | - | /// The span to keep. | |
| 284 | - | fn trim_section(body: Slot) -> Slot { | |
| 285 | - | body.with(Node::section("Trim")).with(Node::Form { | |
| 286 | - | // Two ranges over a fraction of the sample, which is what the shipped | |
| 287 | - | // sliders are. The seconds the shipped panel prints beside each is the | |
| 288 | - | // same number in the sample's units, and a description that carried both | |
| 289 | - | // would be describing a label. See the module header on the interval | |
| 290 | - | // gap: these two constrain each other and cannot say so. | |
| 291 | - | fields: vec![span(START, "Start"), span(END, "End").value("1")], | |
| 292 | - | submit: "Trim".to_owned(), | |
| 293 | - | action: Action::post("/edit/trim"), | |
| 294 | - | }) | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | /// One end of the trim, as a fraction of the whole. | |
| 298 | - | fn span(name: &str, label: &str) -> Field { | |
| 299 | - | Field::range(name, label, "0", "1").step("0.001").value("0") | |
| 300 | - | } | |
| 301 | - | ||
| 302 | - | /// Gain and normalise. | |
| 303 | - | fn levels(body: Slot, sample: &Editing) -> Slot { | |
| 304 | - | let mut body = body.with(Node::section("Levels")); | |
| 305 | - | ||
| 306 | - | body = body | |
| 307 | - | .with(Node::Form { | |
| 308 | - | fields: vec![ | |
| 309 | - | Field::range(GAIN, "Gain", "-24", "24") | |
| 310 | - | .step("0.1") | |
| 311 | - | .value("0") | |
| 312 | - | // The clipping consequence, which the field cannot carry | |
| 313 | - | // itself. See the module header, `5672cad4`. A question | |
| 314 | - | // rather than a write: nothing is applied until the form | |
| 315 | - | // is submitted. The wait is what keeps a drag from asking | |
| 316 | - | // once a frame. | |
| 317 | - | .consulting(Consult::new(Action::post("/edit/gain/preview"))), | |
| 318 | - | ], | |
| 319 | - | submit: "Apply gain".to_owned(), | |
| 320 | - | action: Action::post("/edit/gain"), | |
| 321 | - | }) | |
| 322 | - | .with(Node::Region( | |
| 323 | - | Slot::new(CLIPPING, RegionKind::Group).with(clips(sample.peak_db, 0.0)), | |
| 324 | - | )); | |
| 325 | - | ||
| 326 | - | // Peak and LUFS have different ranges (-24..0 dBFS against -24..-6 LUFS) and | |
| 327 | - | // different defaults, and the shipped panel resets the target when the mode | |
| 328 | - | // changes because "the carried-over value is meaningless across modes". Both | |
| 329 | - | // facts are about the pair rather than about either control, and neither is | |
| 330 | - | // sayable: the widest range is described and the route refuses what falls | |
| 331 | - | // outside the chosen mode's half. | |
| 332 | - | body.with(Node::Form { | |
| 333 | - | fields: vec![ | |
| 334 | - | Field::radio( | |
| 335 | - | MODE, | |
| 336 | - | "Normalize by", | |
| 337 | - | vec![ | |
| 338 | - | Choice::new(PEAK, "Peak"), | |
| 339 | - | Choice::new(LUFS, "Loudness (LUFS)"), | |
| 340 | - | ], | |
| 341 | - | ) | |
| 342 | - | .value(PEAK), | |
| 343 | - | Field::range(TARGET, "Target", "-24", "0") | |
| 344 | - | .step("0.1") | |
| 345 | - | .value("-1"), | |
| 346 | - | ], | |
| 347 | - | submit: "Normalize".to_owned(), | |
| 348 | - | action: Action::post("/edit/normalize"), | |
| 349 | - | }) | |
| 350 | - | } | |
| 351 | - | ||
| 352 | - | /// What the gain about to be applied would do to the peak. | |
| 353 | - | /// | |
| 354 | - | /// Its own node so `Field::consults` can answer it as a fragment while the | |
| 355 | - | /// control is being moved, which is the rename preview's arrangement. | |
| 356 | - | fn clips(peak: Option<f64>, gain: f64) -> Node { | |
| 357 | - | let Some(peak) = peak else { | |
| 358 | - | return Node::empty("Peak unknown until this sample is analysed."); | |
| 359 | - | }; | |
| 360 | - | let predicted = peak + gain; | |
| 361 | - | if predicted <= 0.0 { | |
| 362 | - | return Node::text(format!("Peak: {peak:.1} dB -> {predicted:.1} dB")); | |
| 363 | - | } | |
| 364 | - | Node::banner( | |
| 365 | - | Tone::Danger, | |
| 366 | - | format!("Peak: {peak:.1} dB -> {predicted:.1} dB (clips!)"), | |
| 367 | - | ) | |
| 368 | - | } | |
| 369 | - | ||
| 370 | - | /// Reverse and fade. | |
| 371 | - | fn transform(body: Slot, sample: &Editing) -> Slot { | |
| 372 | - | body.with(Node::section("Transform")) | |
| 373 | - | .with(Node::Act(disable_while( | |
| 374 | - | Act::new("Reverse", Action::post("/edit/reverse")), | |
| 375 | - | sample, | |
| 376 | - | ))) | |
| 377 | - | .with(Node::Form { | |
| 378 | - | // The fade row was left out of the forms conversion as "a slider, a | |
| 379 | - | // chooser and an Apply composing one operation", which was the right | |
| 380 | - | // call about a *field* and is what a `Form` is for: several answers | |
| 381 | - | // and one submit that uses them together. | |
| 382 | - | fields: vec![ | |
| 383 | - | Field::radio( | |
| 384 | - | FADE_IN, | |
| 385 | - | "Fade", | |
| 386 | - | vec![Choice::new("in", "In"), Choice::new("out", "Out")], | |
| 387 | - | ) | |
| 388 | - | .value("in"), | |
| 389 | - | Field::range(LENGTH, "Length", "10", "10000") | |
| 390 | - | .step("10") | |
| 391 | - | .value("100"), | |
| 392 | - | Field::select( | |
| 393 | - | CURVE, | |
| 394 | - | "Curve", | |
| 395 | - | FadeCurve::all() | |
| 396 | - | .into_iter() | |
| 397 | - | .map(|curve| Choice::new(curve.as_value(), curve.label())) | |
| 398 | - | .collect(), | |
| 399 | - | ) | |
| 400 | - | .value(FadeCurve::Linear.as_value()), | |
| 401 | - | ], | |
| 402 | - | submit: "Apply fade".to_owned(), | |
| 403 | - | action: Action::post("/edit/fade"), | |
| 404 | - | }) | |
| 405 | - | } | |
| 406 | - | ||
| 407 | - | /// Insert and remove. | |
| 408 | - | fn silence(body: Slot, sample: &Editing) -> Slot { | |
| 409 | - | // The shipped drag values clamp to the sample's length where analysis has | |
| 410 | - | // said what it is. A described `max` says the same thing, and where the | |
| 411 | - | // length is unknown there is nothing to say rather than a made-up ceiling. | |
| 412 | - | let cap = sample.duration.map(|seconds| seconds * 1000.0); | |
| 413 | - | ||
| 414 | - | body.with(Node::section("Silence")) | |
| 415 | - | .with(Node::Form { | |
| 416 | - | fields: vec![ | |
| 417 | - | milliseconds(AT, "Insert at", cap).value("0"), | |
| 418 | - | milliseconds(LENGTH, "Duration", Some(60_000.0)).value("100"), | |
| 419 | - | ], | |
| 420 | - | submit: "Insert".to_owned(), | |
| 421 | - | action: Action::post("/edit/silence/insert"), | |
| 422 | - | }) | |
| 423 | - | .with(Node::Form { | |
| 424 | - | fields: vec![ | |
| 425 | - | milliseconds(FROM, "Remove from", cap).value("0"), | |
| 426 | - | milliseconds(TO, "to", cap).value("0"), | |
| 427 | - | ], | |
| 428 | - | submit: "Remove".to_owned(), | |
| 429 | - | action: Action::post("/edit/silence/remove"), | |
| 430 | - | }) | |
| 431 | - | } | |
| 432 | - | ||
| 433 | - | /// A number of milliseconds, bounded where the app knows the bound. | |
| 434 | - | fn milliseconds(name: &str, label: &str, cap: Option<f64>) -> Field { | |
| 435 | - | // `min` and `max` are fields rather than builders on this type, where | |
| 436 | - | // `step` is a builder. Set directly, the way the settings screen sets a | |
| 437 | - | // value. | |
| 438 | - | let mut field = Field::new(FieldKind::Number, name, label).step("10"); | |
| 439 | - | field.min = Some("0".to_owned()); | |
| 440 | - | field.max = cap.map(|cap| format!("{cap:.0}")); | |
| 441 | - | field | |
| 215 | + | /// The sample being edited. | |
| 216 | + | struct Sample { | |
| 217 | + | /// What it is called. | |
| 218 | + | name: String, | |
| 219 | + | /// What the shipped info line says, as facts rather than one muted string. | |
| 220 | + | rate: String, | |
| 221 | + | /// How long it runs, where analysis has said. | |
| 222 | + | duration: Option<String>, | |
| 223 | + | /// Its peak, where analysis has said. | |
| 224 | + | peak: Option<String>, | |
| 225 | + | /// Whether this sample is the preview that is playing. | |
| 226 | + | playing: bool, | |
| 227 | + | /// Whether an edit is being applied right now. | |
| 228 | + | busy: bool, | |
| 229 | + | /// What the clipping region says about the gain as it stands. | |
| 230 | + | clipping: Clipping, | |
| 231 | + | /// The longest silence position the app can vouch for, where it knows one. | |
| 232 | + | /// | |
| 233 | + | /// The shipped drag values clamp to the sample's length where analysis has | |
| 234 | + | /// said what it is. A described `max` says the same thing, and where the | |
| 235 | + | /// length is unknown there is nothing to say rather than a made-up ceiling. | |
| 236 | + | cap: Option<String>, | |
| 237 | + | /// What happens to the edited sample. | |
| 238 | + | result: Handling, | |
| 239 | + | /// Everything chosen at once, where enough is chosen for that to mean | |
| 240 | + | /// anything. | |
| 241 | + | batch: Option<Batch>, | |
| 442 | 242 | } | |
| 443 | 243 | ||
| 444 | 244 | /// What happens to the edited sample, and what happened to the last one. | |
| 445 | - | fn result(body: Slot, sample: &Editing) -> Slot { | |
| 446 | - | let mut field = | |
| 447 | - | Field::radio(RESULT, "Result", result_modes()).writes(Action::post("/edit/result")); | |
| 448 | - | if let Some(chosen) = &sample.result { | |
| 449 | - | field = field.value(chosen.clone()); | |
| 450 | - | } | |
| 245 | + | struct Handling { | |
| 246 | + | /// The two answers. | |
| 247 | + | modes: Vec<Choice>, | |
| 248 | + | /// The standing answer, where there is one. | |
| 249 | + | chosen: Option<String>, | |
| 250 | + | /// Whether that answer is the one that removes the original. | |
| 251 | + | replacing: bool, | |
| 252 | + | /// The last edit, while it is still reversible, by the name it goes under. | |
| 253 | + | undoing: Option<String>, | |
| 254 | + | } | |
| 451 | 255 | ||
| 452 | - | let mut body = body | |
| 453 | - | .with(Node::section("Result")) | |
| 454 | - | .with(Node::Field(Box::new(field))); | |
| 256 | + | /// Everything chosen at once. | |
| 257 | + | struct Batch { | |
| 258 | + | /// The heading, which counts them. | |
| 259 | + | heading: String, | |
| 260 | + | /// What normalising them all reads. | |
| 261 | + | normalize: String, | |
| 262 | + | /// What applying gain to them all reads. | |
| 263 | + | gain: String, | |
| 264 | + | /// What reversing them all reads. | |
| 265 | + | reverse: String, | |
| 266 | + | /// What reversing them asks first, where there is enough to regret. | |
| 267 | + | /// | |
| 268 | + | /// The shipped threshold, kept with its reasoning: single-sample Reverse is | |
| 269 | + | /// its own undo (click it again), and on a large selection that trick | |
| 270 | + | /// requires remembering it ran at all. | |
| 271 | + | confirm: Option<String>, | |
| 272 | + | } | |
| 455 | 273 | ||
| 456 | - | if sample.result.as_deref() == Some(EditResultMode::Replace.as_value()) { | |
| 457 | - | body = body.with(Node::banner(Tone::Warning, "Replace mode: the original is removed from this vault. Use Create sibling to keep both, or Undo below to revert.")); | |
| 458 | - | } | |
| 274 | + | /// What the gain about to be applied would do to the peak. | |
| 275 | + | struct Clipping { | |
| 276 | + | /// Which of the three things there is to say. | |
| 277 | + | says: Says, | |
| 278 | + | /// The sentence, where the peak is known. | |
| 279 | + | said: String, | |
| 280 | + | } | |
| 459 | 281 | ||
| 460 | - | // The standing undo. See the module header: this is an act rather than | |
| 461 | - | // `Response::undoable`, and the ten-second timeout the shipped panel keeps | |
| 462 | - | // (with an `egui::Id` round trip and a `request_repaint_after` to land it) | |
| 463 | - | // is renderer policy that no longer has anywhere to be written down. | |
| 464 | - | if let Some(last) = &sample.undoing { | |
| 465 | - | body = body | |
| 466 | - | .with(Node::text(format!("Last edit: {last}"))) | |
| 467 | - | .with(Node::Act(Act::new("Undo", Action::post("/edit/undo")))); | |
| 282 | + | /// What there is to say about a predicted peak. | |
| 283 | + | enum Says { | |
| 284 | + | /// The sample has not been analysed, so there is no peak to predict from. | |
| 285 | + | Unknown, | |
| 286 | + | /// It stays under zero. | |
| 287 | + | Under, | |
| 288 | + | /// It does not. | |
| 289 | + | Clips, | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | /// What the editor draws, read off the app. | |
| 293 | + | fn read(state: &Panels<'_>) -> Result<Edit, RouteError> { | |
| 294 | + | let sample = subject(state)?; | |
| 295 | + | Ok(if sample.asking { | |
| 296 | + | Edit { | |
| 297 | + | asking: Some(Asking { | |
| 298 | + | modes: result_modes(), | |
| 299 | + | chosen: sample | |
| 300 | + | .result | |
| 301 | + | .clone() | |
| 302 | + | .unwrap_or_else(|| EditResultMode::Sibling.as_value().to_owned()), | |
| 303 | + | remembered: if sample.result.is_some() { "on" } else { "" }, | |
| 304 | + | }), | |
| 305 | + | working_on: None, | |
| 306 | + | } | |
| 307 | + | } else { | |
| 308 | + | Edit { | |
| 309 | + | asking: None, | |
| 310 | + | working_on: Some(sample_read(&sample)), | |
| 311 | + | } | |
| 312 | + | }) | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | /// The editor proper, read off the sample. | |
| 316 | + | fn sample_read(sample: &Editing) -> Sample { | |
| 317 | + | Sample { | |
| 318 | + | name: sample.name.clone(), | |
| 319 | + | rate: sample.sample_rate.to_string(), | |
| 320 | + | duration: sample.duration.map(|seconds| format!("{seconds:.3}")), | |
| 321 | + | peak: sample.peak_db.map(|peak| format!("{peak:.1}")), | |
| 322 | + | playing: sample.playing, | |
| 323 | + | busy: sample.working, | |
| 324 | + | clipping: clipping_read(sample.peak_db, 0.0), | |
| 325 | + | cap: sample | |
| 326 | + | .duration | |
| 327 | + | .map(|seconds| format!("{:.0}", seconds * 1000.0)), | |
| 328 | + | result: Handling { | |
| 329 | + | modes: result_modes(), | |
| 330 | + | chosen: sample.result.clone(), | |
| 331 | + | replacing: sample.result.as_deref() == Some(EditResultMode::Replace.as_value()), | |
| 332 | + | undoing: sample.undoing.clone(), | |
| 333 | + | }, | |
| 334 | + | batch: (sample.chosen >= 2).then(|| Batch { | |
| 335 | + | heading: format!("Batch: {} samples", sample.chosen), | |
| 336 | + | normalize: format!("Normalize {} samples", sample.chosen), | |
| 337 | + | gain: format!("Apply gain to {} samples", sample.chosen), | |
| 338 | + | reverse: format!("Reverse {} samples", sample.chosen), | |
| 339 | + | confirm: (sample.chosen > REGRET) | |
| 340 | + | .then(|| format!("Reverse {} samples?", sample.chosen)), | |
| 341 | + | }), | |
| 342 | + | } | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | /// What a gain would do to the peak, read off what analysis found. | |
| 346 | + | fn clipping_read(peak: Option<f64>, gain: f64) -> Clipping { | |
| 347 | + | let Some(peak) = peak else { | |
| 348 | + | return Clipping { | |
| 349 | + | says: Says::Unknown, | |
| 350 | + | said: String::new(), | |
| 351 | + | }; | |
| 352 | + | }; | |
| 353 | + | let predicted = peak + gain; | |
| 354 | + | Clipping { | |
| 355 | + | says: if predicted <= 0.0 { | |
| 356 | + | Says::Under | |
| 357 | + | } else { | |
| 358 | + | Says::Clips | |
| 359 | + | }, | |
| 360 | + | said: if predicted <= 0.0 { | |
| 361 | + | format!("Peak: {peak:.1} dB -> {predicted:.1} dB") | |
| 362 | + | } else { | |
| 363 | + | format!("Peak: {peak:.1} dB -> {predicted:.1} dB (clips!)") | |
| 364 | + | }, | |
| 468 | 365 | } | |
| 469 | - | body | |
| 470 | 366 | } | |
| 471 | 367 | ||
| 472 | 368 | /// The two answers to "Result". | |
| @@ -477,76 +373,343 @@ | |||
| 477 | 373 | ] | |
| 478 | 374 | } | |
| 479 | 375 | ||
| 480 | - | /// Everything chosen at once. | |
| 481 | - | fn batch(body: Slot, sample: &Editing) -> Slot { | |
| 482 | - | if sample.chosen < 2 { | |
| 483 | - | return body; | |
| 484 | - | } | |
| 485 | - | let chosen = sample.chosen; | |
| 486 | - | ||
| 487 | - | body.with(Node::section(format!("Batch: {chosen} samples"))) | |
| 488 | - | .with(Node::text("Applies to every chosen sample at once.")) | |
| 489 | - | .with(Node::Form { | |
| 490 | - | // Its own value rather than the single-sample slider's. See the | |
| 491 | - | // module header on the piggyback this deletes. | |
| 492 | - | fields: vec![ | |
| 493 | - | Field::radio( | |
| 494 | - | MODE, | |
| 495 | - | "Normalize by", | |
| 496 | - | vec![ | |
| 497 | - | Choice::new(PEAK, "Peak"), | |
| 498 | - | Choice::new(LUFS, "Loudness (LUFS)"), | |
| 499 | - | ], | |
| 500 | - | ) | |
| 501 | - | .value(PEAK), | |
| 502 | - | Field::range(TARGET, "Target", "-24", "0") |
Lines truncated