gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
mod preview_shader;

use crate::ui::scoped_frame;
use egui::load::SizedTexture;
use egui::{Color32, Frame, Image, Layout, Ui, UiBuilder};
use egui_knob::{Knob, KnobStyle, LabelPosition};
use emath::{Align, Pos2, Rect, Vec2, pos2, remap_clamp, vec2};
use epaint::{PathShape, PathStroke, Stroke};
use gled_model::sound::trigger::{SoundTrigger, SoundTriggerParams};
use gled_model::sound::trigger_params::MAX_FREQ;
use gled_model::sound::trigger_ref::{SoundLevels, SoundTriggerRef};
use gled_protocol::state::SoundSnapshot;
use ndarray::Array1;
use preview_shader::{PreviewShader, preview_shader};

/// The full editor: spectrum with the frequency band, envelope and knobs.
/// Returns whether a setting changed.
pub(crate) fn sound_trigger_editor(
    ui: &mut Ui,
    trigger: &mut SoundTriggerRef,
    sound_data: &SoundSnapshot,
) -> bool {
    #[cfg(feature = "profiling")]
    puffin::profile_function!("sound_trigger_editor");
    let mut changed = false;
    Frame::new().inner_margin(5.).show(ui, |ui| {
        // The engine sends this trigger's spectrum while the editor shows.
        crate::ui::frame_requests::watch_sound_trigger(trigger.id);
        let (impulse, output_level, preview) = match sound_data.reading(&trigger.id) {
            Some((impulse, level, spectrum)) => {
                let mut spectrum = Array1::from_vec(spectrum);
                spectrum.map_inplace(|x| {
                    *x = (*x * 10.0 + 10.0).log10() - 1.0;
                });
                let preview = preview_shader(ui, trigger.id);
                if let Some(preview) = &preview
                    && !spectrum.is_empty()
                {
                    preview.draw_spectrum_texture(spectrum);
                }
                (impulse.clamp(0.0, 1.0), level, preview)
            }
            // Not running yet: it starts with the next frame.
            None => (0.0, 0.0, None),
        };

        changed |= draw_spectrum(ui, trigger, preview.as_deref());
        ui.separator();
        changed |= draw_adsr(ui, trigger, impulse, output_level);
    });
    changed
}

/// The small view next to a curve: the envelope and the current level.
pub(crate) fn sound_trigger_minified(
    ui: &mut Ui,
    rect: Rect,
    trigger: &SoundTriggerRef,
    sound_data: &SoundSnapshot,
) {
    #[cfg(feature = "profiling")]
    puffin::profile_function!("sound_trigger_minified");
    let level = sound_data.level(&trigger.id);
    scoped_frame(
        ui,
        UiBuilder::new().max_rect(rect),
        Frame::default(),
        |ui| {
            let to_screen = emath::RectTransform::from_to(
                Rect::from_min_size(Pos2::ZERO, Vec2::new(4.0, 1.0)),
                rect,
            );
            let painter = ui.painter().with_clip_rect(rect);

            for i in 0..=4 {
                let stroke = Stroke::new(
                    match i {
                        0 | 4 => 1.0,
                        _ => 0.5,
                    },
                    Color32::GRAY,
                );
                painter.add(PathShape::line(
                    vec![
                        to_screen.transform_pos(Pos2::new(i as f32, 0.0)),
                        to_screen.transform_pos(Pos2::new(i as f32, 1.0)),
                    ],
                    stroke,
                ));
                painter.add(PathShape::line(
                    vec![
                        to_screen.transform_pos(Pos2::new(0.0, i as f32 * 0.25)),
                        to_screen.transform_pos(Pos2::new(4.0, i as f32 * 0.25)),
                    ],
                    stroke,
                ));
            }

            draw_envelope(ui, rect, level, &trigger.params);
        },
    );
}

fn draw_adsr(
    ui: &mut Ui,
    trigger: &mut SoundTriggerRef,
    input_level: f32,
    output_level: f32,
) -> bool {
    let params = &mut trigger.params;
    let mut changed = false;

    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
        let meter_height = 200.0;
        let (_, rect) = ui.allocate_space(Vec2::new(40.0, meter_height));
        ui.painter().rect_filled(rect, 0., Color32::from_gray(100));

        let mut value_rect = rect;
        value_rect.min.y += meter_height * (1.0 - input_level);
        ui.painter()
            .rect_filled(value_rect, 0., Color32::LIGHT_GREEN);

        let threshold_line_y = rect.min.y + meter_height * (1.0 - params.gate_activation_threshold);
        ui.painter().line(
            vec![
                pos2(rect.min.x, threshold_line_y),
                pos2(rect.max.x, threshold_line_y),
            ],
            Stroke::new(2., Color32::BLUE),
        );

        let threshold_deac_line_y =
            rect.min.y + meter_height * (1.0 - params.gate_deactivation_threshold);
        ui.painter().line(
            vec![
                pos2(rect.min.x, threshold_deac_line_y),
                pos2(rect.max.x, threshold_deac_line_y),
            ],
            Stroke::new(2., Color32::BLUE),
        );

        let mut rect = ui.available_rect_before_wrap();
        rect.set_height(200.);
        let frame = Frame::new().inner_margin(5.0).fill(Color32::from_gray(60));
        scoped_frame(ui, UiBuilder::new().max_rect(rect), frame, |ui| {
            let rect = ui.available_rect_before_wrap();
            draw_envelope(ui, rect, output_level, params);
        });
    });
    scoped_frame(
        ui,
        UiBuilder::new()
            .max_rect(Rect::from_min_size(
                ui.cursor().min,
                vec2(ui.available_width(), 100.),
            ))
            .layout(Layout::left_to_right(Align::Max).with_cross_justify(true)),
        Frame::default().inner_margin(5.0),
        |ui| {
            Frame::new().inner_margin(5.).show(ui, |ui| {
                changed |= ui
                    .add(
                        knob_default(&mut params.attack_duration, 0.0, 2.0, 0.01)
                            .with_size(50.0)
                            .with_logarithmic_scaling()
                            .with_label("Attack", LabelPosition::Bottom),
                    )
                    .changed();
                changed |= ui
                    .add(
                        knob_default(&mut params.release_duration, 0.0, 10.0, 1.0)
                            .with_size(50.0)
                            .with_logarithmic_scaling()
                            .with_label("Release", LabelPosition::Bottom),
                    )
                    .changed();
                changed |= ui
                    .add(
                        knob_default(&mut params.gate_activation_threshold, 0.0, 1.0, 0.5)
                            .with_size(30.0)
                            .with_label("Threshold", LabelPosition::Bottom),
                    )
                    .changed();
            });
        },
    );

    changed
}

fn draw_spectrum(
    ui: &mut Ui,
    trigger: &mut SoundTriggerRef,
    preview: Option<&PreviewShader>,
) -> bool {
    #[cfg(feature = "profiling")]
    puffin::profile_function!("draw_spectrum");
    let mut changed = false;
    let size = vec2(ui.available_width(), 190.);
    let mut spectrum_rect = Rect::from_min_size(ui.cursor().min, size);

    if let Some(preview) = preview {
        ui.add(Image::new(SizedTexture::new(
            preview.spectrum_texture_id.0,
            size,
        )));
    }

    let lower_f = trigger.f_center - trigger.f_radius;
    let upper_f = trigger.f_center + trigger.f_radius;
    let max_frequency_range = 0.0..=MAX_FREQ;

    let ui_position_range = spectrum_rect.left()..=spectrum_rect.right();
    spectrum_rect.min.x = remap_clamp(
        lower_f,
        max_frequency_range.clone(),
        ui_position_range.clone(),
    );
    spectrum_rect.max.x = remap_clamp(upper_f, max_frequency_range, ui_position_range);

    ui.painter()
        .rect_filled(spectrum_rect, 0., Color32::from_white_alpha(100));

    scoped_frame(
        ui,
        UiBuilder::new()
            .max_rect(Rect::from_min_size(
                ui.cursor().min,
                vec2(ui.available_width(), 100.),
            ))
            .layout(Layout::left_to_right(Align::Max)),
        Frame::default().inner_margin(5.0),
        |ui| {
            let mut filter_tune_changed = ui
                .add(
                    knob_default(&mut trigger.f_center, 0., 24_000., 100.0)
                        .with_size(50.0)
                        .with_logarithmic_scaling()
                        .with_label("Frequency", LabelPosition::Bottom),
                )
                .changed();
            filter_tune_changed |= ui
                .add(
                    knob_default(&mut trigger.f_radius, 100., 24_000., 50.0)
                        .with_size(50.0)
                        .with_logarithmic_scaling()
                        .with_label("Range", LabelPosition::Bottom),
                )
                .changed();
            changed |= ui
                .add(
                    knob_default(&mut trigger.params.sensitivity, 0.01, 10.0, 1.0)
                        .with_size(50.0)
                        .with_logarithmic_scaling()
                        .with_label("Sensitivity", LabelPosition::Bottom),
                )
                .changed();

            if filter_tune_changed {
                trigger
                    .params
                    .set_filter_tune(trigger.f_center, trigger.f_radius);
                changed = true;
            }
        },
    );

    changed
}

fn draw_envelope(ui: &mut Ui, rect: Rect, output_level: f32, params: &SoundTriggerParams) {
    let n = 100;

    let to_screen =
        emath::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, 0.0..=1.0), rect);

    let mut level_rect = rect;
    level_rect.min.y += (1.0 - (output_level)) * rect.height();
    ui.painter()
        .rect_filled(level_rect, 0., Color32::LIGHT_GREEN);

    let mut sound_trigger = SoundTrigger::new(*params, 100.);
    let points: Vec<Pos2> = (0..=n)
        .map(|i| {
            let t = i as f32 / (n as f32);
            let input = if 0.2 < t
                && t < (0.4
                    + sound_trigger.params.decay_duration * 0.4
                    + sound_trigger.params.attack_duration * 0.4)
            {
                1.0
            } else {
                0.0
            };
            let y = -sound_trigger.tick_adsr(input) + 1.;
            to_screen * pos2(t, y)
        })
        .collect();

    let shape = epaint::Shape::line(points, PathStroke::new(1.0, Color32::WHITE));
    ui.painter().add(shape);
}

fn knob_default<'a>(value: &'a mut f32, min: f32, max: f32, reset: f32) -> Knob<'a> {
    Knob::new(value, min, max, KnobStyle::Wiper)
        .with_font_size(12.0)
        .with_knob_color(Color32::GRAY)
        .with_line_color(Color32::WHITE)
        .with_text_color(Color32::WHITE)
        .with_stroke_width(3.0)
        .with_double_click_reset(reset)
}