gled-core 2.31.1

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
use crate::runtime::persistent_state::PersistentState;
use rusty_link::{AblLink, SessionState};
use std::{
    sync::{
        Arc,
        atomic::{AtomicU64, Ordering::Relaxed},
    },
    time::{Duration, Instant},
};

pub(crate) static CONNECTED_PEERS: AtomicU64 = AtomicU64::new(0);

/// Offset added to the Link beat so `beat_progression` never goes negative.
const BEAT_PROGRESSION_OFFSET: f32 = 40.0;
/// Link quantum (beats per bar).
const LINK_QUANTUM: f64 = 4.0;

pub(crate) use gled_model::timing::BeatState;

/// Samples the current beat from Link's audio session state.
///
/// Realtime-safe (never blocks on the UI thread's tempo changes), but Link
/// allows it to be called from one thread only: the render thread.
pub(crate) fn sample_beat_state(link: &AblLink, framerate: f32) -> BeatState {
    let mut session_state = SessionState::default();
    link.capture_audio_session_state(&mut session_state);
    let now = link.clock_micros();

    BeatState {
        beat_progression: BEAT_PROGRESSION_OFFSET
            + session_state.beat_at_time(now, LINK_QUANTUM) as f32,
        beats_per_minute: session_state.tempo() as f32,
        framerate,
    }
}

pub(crate) struct Timing {
    link: Arc<AblLink>,
    beats_per_minute: f32,
    previous_change_beats_per_minute: f32,
    pub(crate) change_beats_per_minute: f32,
    beat_progression: f32,
    /// Frame rate of the render thread, reported back through its feedback channel.
    framerate: Option<f32>,
    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: Arc::new(link),
            beats_per_minute: 120.0,
            previous_change_beats_per_minute: 120.0,
            change_beats_per_minute: 120.0,
            beat_progression: 0.0,
            framerate: None,
            taps: [None; 4],
            tap_count: 0,
        }
    }
}

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

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

    pub(crate) fn set_framerate(&mut self, framerate: f32) {
        self.framerate = Some(framerate);
    }

    /// Beat timing as last sampled on the UI thread.
    pub(crate) fn beat_state(&self) -> BeatState {
        BeatState {
            beat_progression: self.beat_progression,
            beats_per_minute: self.beats_per_minute,
            framerate: self.framerate.unwrap_or_default(),
        }
    }

    /// Shared Link handle, used by the render thread to sample the beat itself.
    pub(crate) fn link(&self) -> Arc<AblLink> {
        Arc::clone(&self.link)
    }

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

    #[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 =
            BEAT_PROGRESSION_OFFSET + session_state.beat_at_time(now, LINK_QUANTUM) 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 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(crate) 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(crate) fn add_speed(&mut self, delta: f32, persistent_state: &PersistentState) {
        if persistent_state.ableton_link_read_only() {
            return;
        }

        self.change_beats_per_minute += delta;
    }

    /// Taps counted since the last pause, for the tap button's indicator.
    pub(crate) fn tap_count(&self) -> usize {
        self.tap_count
    }

    /// Sets the tempo, as the BPM slider does. Applied to Link on the next
    /// `tick` unless Link is read only.
    pub(crate) fn set_beats_per_minute(&mut self, beats_per_minute: f32) {
        self.change_beats_per_minute = beats_per_minute;
    }

    pub(crate) 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);
    }
}