gled 2.28.5

gled is an application for creating animations and effects on artnet or dmx installations
use crate::app::persistent_state::PersistentState;
use egui::{Button, Color32, CornerRadius, Stroke, TextFormat, Ui, Vec2, text::LayoutJob};
use rusty_link::{AblLink, SessionState};
use std::{
    sync::atomic::{AtomicU64, Ordering::Relaxed},
    time::{Duration, Instant},
};
use tracing::debug;

pub static CONNECTED_PEERS: AtomicU64 = AtomicU64::new(0);
pub static LINK_ACTIVE_COLOR: Color32 = Color32::from_rgb(41, 116, 145);

pub struct Timing {
    link: AblLink,
    beats_per_minute: f32,
    previous_change_beats_per_minute: f32,
    pub change_beats_per_minute: f32,
    beat_progression: f32,
    avg_fps: Option<f32>,
    avg_fps_time: Instant,
    last_frame: Instant,
    frame_count: usize,
    total_frames: usize,
    max_frame_nanos: u128,
    taps: [Option<Instant>; 4],
    tap_count: usize,
}

impl std::fmt::Debug for Timing {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Timing")
            .field("beats_per_minute", &self.beats_per_minute)
            .field("change_beats_per_minute", &self.change_beats_per_minute)
            .finish_non_exhaustive()
    }
}

impl Default for Timing {
    fn default() -> Self {
        let link = AblLink::new(120.0);
        link.enable(true);

        Self {
            link,
            beats_per_minute: 120.0,
            previous_change_beats_per_minute: 120.0,
            change_beats_per_minute: 120.0,
            beat_progression: 0.0,
            avg_fps: None,
            avg_fps_time: Instant::now(),
            last_frame: Instant::now(),
            frame_count: 0,
            total_frames: 0,
            max_frame_nanos: 0,
            taps: [None; 4],
            tap_count: 0,
        }
    }
}

impl Timing {
    pub fn beat_progression(&self) -> f32 {
        self.beat_progression
    }

    pub fn beats_per_minute(&self) -> f32 {
        self.beats_per_minute
    }

    pub fn framerate(&self) -> Option<f32> {
        self.avg_fps
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn tick(&mut self, fps_limit: f32, persistent_state: &PersistentState) {
        self.limit_fps(fps_limit);
        self.set_link_values(persistent_state);
        self.get_link_values();
        self.calculate_avg_fps();
        self.remove_old_taps();
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn limit_fps(&mut self, fps_limit: f32) {
        let target_frame_time_nanos = 1e+9f32 / fps_limit;
        while target_frame_time_nanos > (self.last_frame.elapsed().as_nanos() as f32) {
            std::thread::sleep(std::time::Duration::from_nanos(100));
        }
        // Full wall-clock time of the previous frame, including the time eframe
        // spent acquiring the surface texture and presenting outside of our own
        // update code. The maximum over a sampling window is a proxy for the
        // worst-case surface wait / stall. The first 10 frames are skipped
        // because startup (window creation, swapchain setup) makes them much
        // slower than steady-state frames.
        self.total_frames += 1;
        if self.total_frames > 10 {
            self.max_frame_nanos = self
                .max_frame_nanos
                .max(self.last_frame.elapsed().as_nanos());
        }
        self.last_frame = Instant::now();
    }

    /// Re-sample the current Ableton Link beat position without running a full
    /// `tick` (no fps limiting, no fps accounting). Used when rendering more than
    /// once per displayed frame so each extra render reflects the freshest beat
    /// position instead of repeating the same instant.
    pub fn refresh_beat(&mut self) {
        self.get_link_values();
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn get_link_values(&mut self) {
        CONNECTED_PEERS.store(self.link.num_peers(), Relaxed);

        let mut session_state = SessionState::default();
        self.link.capture_app_session_state(&mut session_state);
        let now = self.link.clock_micros();
        self.beat_progression = 40.0 + session_state.beat_at_time(now, 4.0) as f32;
        self.beats_per_minute = session_state.tempo() as f32;
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn set_link_values(&mut self, persistent_state: &PersistentState) {
        if persistent_state.ableton_link_read_only() {
            self.previous_change_beats_per_minute = self.beats_per_minute;
            self.change_beats_per_minute = self.beats_per_minute;
            return;
        }

        if self.previous_change_beats_per_minute == self.change_beats_per_minute {
            self.previous_change_beats_per_minute = self.beats_per_minute;
            self.change_beats_per_minute = self.beats_per_minute;
            return;
        }

        let mut session_state = SessionState::default();
        self.link.capture_app_session_state(&mut session_state);
        let now = self.link.clock_micros();
        session_state.set_tempo(self.change_beats_per_minute as f64, now);
        self.link.commit_app_session_state(&session_state);
        self.previous_change_beats_per_minute = self.change_beats_per_minute;
    }

    fn calculate_avg_fps(&mut self) {
        self.frame_count += 1;
        let now = Instant::now();
        if now.duration_since(self.avg_fps_time).as_millis() > 1000 {
            let avg_frame_time = self.avg_fps_time.elapsed() / self.frame_count as u32;
            let fps = 1e+9f32 / (avg_frame_time.as_nanos() as f32);
            self.avg_fps = Some(fps);
            let max_frame_ms = self.max_frame_nanos as f32 / 1e6f32;
            debug!("frame_stats: fps={fps:.1} max_frame_ms={max_frame_ms:.3}");
            self.avg_fps_time = now;
            self.frame_count = 0;
            self.max_frame_nanos = 0;
        }
    }

    fn remove_old_taps(&mut self) {
        let mut reset_tap_count = true;
        for tap in self.taps.iter_mut() {
            if let Some(date) = tap {
                if date.elapsed() > Duration::from_secs(10) {
                    tap.take();
                } else {
                    reset_tap_count = false;
                }
            }
        }

        if reset_tap_count {
            self.tap_count = 0;
        }
    }

    pub fn half_button(
        &mut self,
        ui: &mut Ui,
        tap_input: bool,
        persistent_state: &PersistentState,
    ) {
        let ableton_link_read_only = persistent_state.ableton_link_read_only();
        if ui
            .add_enabled(!ableton_link_read_only, Button::new(""))
            .clicked()
            || (tap_input && !ableton_link_read_only)
        {
            self.multiply_speed(0.5, persistent_state);
        }
    }

    pub fn double_button(
        &mut self,
        ui: &mut Ui,
        tap_input: bool,
        persistent_state: &PersistentState,
    ) {
        let ableton_link_read_only = persistent_state.ableton_link_read_only();
        if ui
            .add_enabled(!ableton_link_read_only, Button::new("x2"))
            .clicked()
            || (tap_input && !ableton_link_read_only)
        {
            self.multiply_speed(2.0, persistent_state);
        }
    }

    pub fn multiply_speed(&mut self, multiplier: f32, persistent_state: &PersistentState) {
        if persistent_state.ableton_link_read_only() {
            return;
        }

        self.change_beats_per_minute *= multiplier;
    }

    pub fn add_speed(&mut self, delta: f32, persistent_state: &PersistentState) {
        if persistent_state.ableton_link_read_only() {
            return;
        }

        self.change_beats_per_minute += delta;
    }

    pub fn tap_button(
        &mut self,
        ui: &mut Ui,
        menu_button_size: Vec2,
        tap_input: bool,
        persistent_state: &PersistentState,
    ) {
        let ableton_link_read_only = persistent_state.ableton_link_read_only();
        ui.scope(|ui| {
            ui.style_mut().visuals.widgets.inactive.bg_stroke =
                ui.style().visuals.widgets.noninteractive.bg_stroke;

            let underlined = TextFormat {
                underline: Stroke::new(1.0, Color32::GRAY),
                ..Default::default()
            };
            let mut tap_text = LayoutJob::default();
            tap_text.append("T", 0.0, underlined);
            tap_text.append(
                &format!(
                    "ap{}",
                    match (self.tap_count, self.tap_count % 4) {
                        (0, _) => "",
                        (_, 0) => "/",
                        (_, 1) => "",
                        (_, 2) => "\\",
                        _ => "|",
                    }
                ),
                0.0,
                TextFormat::default(),
            );
            let response = ui.add_enabled(
                !ableton_link_read_only,
                Button::new(tap_text).min_size(menu_button_size),
            );

            let mut alpha = None;
            let bar_progression = self.beat_progression % 1.0;
            if bar_progression < 0.10 {
                alpha = Some(30.0);
            } else if bar_progression < 0.20 {
                alpha = Some(20.0 - ((bar_progression - 0.1) * 200.0));
            } else if bar_progression > 0.9 {
                alpha = Some((bar_progression - 0.9) * 200.0);
            }
            if let Some(alpha) = alpha {
                ui.painter().rect_filled(
                    match self.beat_flank() {
                        0 =>
                        // top left
                        {
                            response
                                .rect
                                .split_left_right_at_fraction(0.5)
                                .0
                                .split_top_bottom_at_fraction(0.5)
                                .0
                        }
                        1 =>
                        // top right
                        {
                            response
                                .rect
                                .split_left_right_at_fraction(0.5)
                                .1
                                .split_top_bottom_at_fraction(0.5)
                                .0
                        }
                        2 =>
                        // bottom left
                        {
                            response
                                .rect
                                .split_left_right_at_fraction(0.5)
                                .0
                                .split_top_bottom_at_fraction(0.5)
                                .1
                        }
                        3 =>
                        // bottom right
                        {
                            response
                                .rect
                                .split_left_right_at_fraction(0.5)
                                .1
                                .split_top_bottom_at_fraction(0.5)
                                .1
                        }
                        _ => unreachable!(),
                    }
                    .shrink(1.0),
                    CornerRadius::default(),
                    Color32::from_white_alpha(alpha as u8),
                );
            }

            let tapped = response.clicked() || (tap_input && !ableton_link_read_only);

            if tapped {
                self.tap(persistent_state);
            }
        });
    }

    pub fn beat_flank(&self) -> u8 {
        match self.beat_progression % 4.0 {
            0.5..1.5 => 1,
            1.5..2.5 => 2,
            2.5..3.5 => 3,
            _ => 0,
        }
    }

    pub fn tap(&mut self, persistent_state: &PersistentState) {
        if persistent_state.ableton_link_read_only() {
            return;
        }

        let link_now = self.link.clock_micros();

        self.tap_count += 1;
        let now = Instant::now();
        self.taps[0] = Some(now);
        self.taps.sort();

        let first = self
            .taps
            .iter()
            .filter_map(|tap| *tap)
            .next()
            .expect("There must be at least one entry");
        if first == now {
            return;
        }

        let beat_time_first = now.duration_since(first);
        let avg_beat_time = (beat_time_first.as_nanos())
            / (self.taps.iter().filter(|tap| tap.is_some()).count() as u128 - 1);

        let new_beats_per_minute = (60e+9f64 / f64::from(avg_beat_time as u32)) as f32;

        // adjust beat progression timing to last tap
        let offset = self.beat_progression % 4.0;
        let goal_offset = (self.tap_count - 1) as f32 % 4.0;
        let mut session_state = SessionState::default();
        self.link.capture_app_session_state(&mut session_state);
        session_state.set_tempo(new_beats_per_minute as f64, link_now);
        session_state.request_beat_at_time(
            (self.beat_progression + goal_offset - offset) as f64,
            link_now,
            4.0,
        );
        self.link.commit_app_session_state(&session_state);
    }
}