| 604 |
604 |
|
.collect()
|
| 605 |
605 |
|
}
|
| 606 |
606 |
|
|
|
607 |
+ |
/// The granularity an envelope time moves in.
|
|
608 |
+ |
///
|
|
609 |
+ |
/// A millisecond, which is what the sliders' `max_decimals(3)` already showed
|
|
610 |
+ |
/// and what the shortest attack worth having is measured in. The step is in the
|
|
611 |
+ |
/// value's own units under either curve (makeover-layout 0.32.0), so it reads as
|
|
612 |
+ |
/// three decimals here exactly as it did before the curve existed.
|
|
613 |
+ |
const TIME_STEP: &str = "0.001";
|
|
614 |
+ |
|
|
615 |
+ |
/// The granularity the sustain level moves in: two decimals, as it was drawn.
|
|
616 |
+ |
const LEVEL_STEP: &str = "0.01";
|
|
617 |
+ |
|
|
618 |
+ |
/// One envelope time, as a described field on a constant-ratio track.
|
|
619 |
+ |
///
|
|
620 |
+ |
/// The three times differ only in their name, their help and their upper bound,
|
|
621 |
+ |
/// so they are one function rather than three copies of six lines. The value
|
|
622 |
+ |
/// round-trips through a string for `threshold_field`'s reason: that is what
|
|
623 |
+ |
/// the description carries and what every renderer of it takes, so a number
|
|
624 |
+ |
/// would be this renderer's convenience imposed on the other two.
|
|
625 |
+ |
#[expect(
|
|
626 |
+ |
clippy::too_many_arguments,
|
|
627 |
+ |
reason = "each argument is one fact about the question, and bundling them into a struct for \
|
|
628 |
+ |
three call sites in one file would be further to read, not less"
|
|
629 |
+ |
)]
|
|
630 |
+ |
fn envelope_field(
|
|
631 |
+ |
ui: &mut egui::Ui,
|
|
632 |
+ |
name: &str,
|
|
633 |
+ |
label: &str,
|
|
634 |
+ |
hint: &str,
|
|
635 |
+ |
step: &'static str,
|
|
636 |
+ |
min: f32,
|
|
637 |
+ |
max: f32,
|
|
638 |
+ |
value: &mut f32,
|
|
639 |
+ |
) {
|
|
640 |
+ |
let (min_text, max_text) = (format!("{min}"), format!("{max}"));
|
|
641 |
+ |
let described = makeover_layout::Field {
|
|
642 |
+ |
hint: Some(hint),
|
|
643 |
+ |
curve: makeover_layout::Curve::Logarithmic { step: Some(step) },
|
|
644 |
+ |
..makeover_layout::Field::range(name, label, &min_text, &max_text)
|
|
645 |
+ |
};
|
|
646 |
+ |
let mut text = format!("{value:.3}");
|
|
647 |
+ |
widgets::field(
|
|
648 |
+ |
ui,
|
|
649 |
+ |
&described,
|
|
650 |
+ |
makeover_immediate::Filling::Text(&mut text),
|
|
651 |
+ |
None,
|
|
652 |
+ |
);
|
|
653 |
+ |
if let Ok(parsed) = text.parse::<f32>() {
|
|
654 |
+ |
*value = parsed;
|
|
655 |
+ |
}
|
|
656 |
+ |
}
|
|
657 |
+ |
|
| 607 |
658 |
|
/// Draw ADSR envelope sliders, with a live envelope-shape preview above so the
|
| 608 |
659 |
|
/// effect of each parameter is visible before the user releases the slider.
|
| 609 |
660 |
|
fn draw_adsr_controls(ui: &mut egui::Ui, state: &mut BrowserState) {
|
| 649 |
700 |
|
envelope.release,
|
| 650 |
701 |
|
);
|
| 651 |
702 |
|
|
|
703 |
+ |
// The four described, at last. They were the last hand-rolled sliders in
|
|
704 |
+ |
// either crate and they were blocked on two things at once: a described
|
|
705 |
+ |
// range had no way to say its track was not linear, and it still has no way
|
|
706 |
+ |
// to say what its numbers are measured in.
|
|
707 |
+ |
//
|
|
708 |
+ |
// The first is closed -- makeover-layout 0.32.0's `Curve`, from Max's
|
|
709 |
+ |
// reframe that a slider's data is a fraction and a function taking numbers
|
|
710 |
+ |
// to numbers. Three of these are `Curve::Logarithmic` because an envelope
|
|
711 |
+ |
// time spans four orders of magnitude and the half that matters is the
|
|
712 |
+ |
// short one: a 5 ms attack and a 50 ms attack are audibly different
|
|
713 |
+ |
// instruments, and a linear track puts both inside its first one percent.
|
|
714 |
+ |
//
|
|
715 |
+ |
// The second is open (makeover-layout `32215e21`), so the unit is in the
|
|
716 |
+ |
// label. That is the convention three fields in this app already arrived at
|
|
717 |
+ |
// independently, and it is what the gap exists to ratify or replace.
|
|
718 |
+ |
//
|
|
719 |
+ |
// Two per row, each keeping its own label above its own bar: the
|
|
720 |
+ |
// classifier's paired thresholds, which is the shape this app uses whenever
|
|
721 |
+ |
// two fields are read across.
|
| 652 |
722 |
|
ui.horizontal(|ui| {
|
| 653 |
|
- |
ui.label(
|
| 654 |
|
- |
egui::RichText::new("A")
|
| 655 |
|
- |
.small()
|
| 656 |
|
- |
.color(theme::content_secondary()),
|
| 657 |
|
- |
)
|
| 658 |
|
- |
.on_hover_text("Attack: time to reach full volume after key press");
|
| 659 |
|
- |
let slider = egui::Slider::new(&mut envelope.attack, 0.001..=5.0)
|
| 660 |
|
- |
.logarithmic(true)
|
| 661 |
|
- |
.max_decimals(3)
|
| 662 |
|
- |
.suffix("s");
|
| 663 |
|
- |
ui.add(slider);
|
| 664 |
|
- |
|
| 665 |
|
- |
ui.label(
|
| 666 |
|
- |
egui::RichText::new("D")
|
| 667 |
|
- |
.small()
|
| 668 |
|
- |
.color(theme::content_secondary()),
|
| 669 |
|
- |
)
|
| 670 |
|
- |
.on_hover_text("Decay: time to fall from peak to sustain level");
|
| 671 |
|
- |
let slider = egui::Slider::new(&mut envelope.decay, 0.001..=5.0)
|
| 672 |
|
- |
.logarithmic(true)
|
| 673 |
|
- |
.max_decimals(3)
|
| 674 |
|
- |
.suffix("s");
|
| 675 |
|
- |
ui.add(slider);
|
|
723 |
+ |
envelope_field(
|
|
724 |
+ |
ui,
|
|
725 |
+ |
"attack",
|
|
726 |
+ |
"Attack (s)",
|
|
727 |
+ |
"Time to reach full volume after key press.",
|
|
728 |
+ |
TIME_STEP,
|
|
729 |
+ |
0.001,
|
|
730 |
+ |
5.0,
|
|
731 |
+ |
&mut envelope.attack,
|
|
732 |
+ |
);
|
|
733 |
+ |
envelope_field(
|
|
734 |
+ |
ui,
|
|
735 |
+ |
"decay",
|
|
736 |
+ |
"Decay (s)",
|
|
737 |
+ |
"Time to fall from peak to the sustain level.",
|
|
738 |
+ |
TIME_STEP,
|
|
739 |
+ |
0.001,
|
|
740 |
+ |
5.0,
|
|
741 |
+ |
&mut envelope.decay,
|
|
742 |
+ |
);
|
| 676 |
743 |
|
});
|
| 677 |
744 |
|
|
| 678 |
745 |
|
ui.horizontal(|ui| {
|
| 679 |
|
- |
ui.label(
|
| 680 |
|
- |
egui::RichText::new("S")
|
| 681 |
|
- |
.small()
|
| 682 |
|
- |
.color(theme::content_secondary()),
|
| 683 |
|
- |
)
|
| 684 |
|
- |
.on_hover_text("Sustain: held volume level while the key is down (0 to 1)");
|
| 685 |
|
- |
let slider = egui::Slider::new(&mut envelope.sustain, 0.0..=1.0).max_decimals(2);
|
| 686 |
|
- |
ui.add(slider);
|
|
746 |
+ |
// The one linear member, and the reason `Curve::Logarithmic` falls back
|
|
747 |
+ |
// rather than refusing: a sustain is a level between silence and full,
|
|
748 |
+ |
// so its low end is zero and a constant ratio is undefined there.
|
|
749 |
+ |
let described = makeover_layout::Field {
|
|
750 |
+ |
hint: Some("Held volume while the key is down."),
|
|
751 |
+ |
curve: makeover_layout::Curve::Linear {
|
|
752 |
+ |
step: Some(LEVEL_STEP),
|
|
753 |
+ |
},
|
|
754 |
+ |
..makeover_layout::Field::range("sustain", "Sustain", "0", "1")
|
|
755 |
+ |
};
|
|
756 |
+ |
let mut text = format!("{:.2}", envelope.sustain);
|
|
757 |
+ |
widgets::field(
|
|
758 |
+ |
ui,
|
|
759 |
+ |
&described,
|
|
760 |
+ |
makeover_immediate::Filling::Text(&mut text),
|
|
761 |
+ |
None,
|
|
762 |
+ |
);
|
|
763 |
+ |
if let Ok(parsed) = text.parse::<f32>() {
|
|
764 |
+ |
envelope.sustain = parsed;
|
|
765 |
+ |
}
|
| 687 |
766 |
|
|
| 688 |
|
- |
ui.label(
|
| 689 |
|
- |
egui::RichText::new("R")
|
| 690 |
|
- |
.small()
|
| 691 |
|
- |
.color(theme::content_secondary()),
|
| 692 |
|
- |
)
|
| 693 |
|
- |
.on_hover_text("Release: time to fade to silence after key release");
|
| 694 |
|
- |
let slider = egui::Slider::new(&mut envelope.release, 0.001..=10.0)
|
| 695 |
|
- |
.logarithmic(true)
|
| 696 |
|
- |
.max_decimals(3)
|
| 697 |
|
- |
.suffix("s");
|
| 698 |
|
- |
ui.add(slider);
|
|
767 |
+ |
envelope_field(
|
|
768 |
+ |
ui,
|
|
769 |
+ |
"release",
|
|
770 |
+ |
"Release (s)",
|
|
771 |
+ |
"Time to fade to silence after key release.",
|
|
772 |
+ |
TIME_STEP,
|
|
773 |
+ |
0.001,
|
|
774 |
+ |
10.0,
|
|
775 |
+ |
&mut envelope.release,
|
|
776 |
+ |
);
|
| 699 |
777 |
|
});
|
| 700 |
778 |
|
|
| 701 |
779 |
|
state.shared.instrument.lock().config.envelope = envelope;
|