gled-core 2.31.1

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! Scanning for devices while an interface shows a device list: audio inputs
//! on a thread of their own, as scanning takes a moment. The MIDI ports come
//! from the engine's MIDI host, which follows them anyway.

use std::{
    collections::BTreeSet,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering::Relaxed},
    },
    thread::{self, JoinHandle},
    time::Duration,
};

use gled_protocol::state::Devices;
use kanal::{Receiver, Sender, unbounded};
use tracing::warn;

use super::ClientId;
use crate::{
    audio::{audio_device_labels, device_id::audio_device_id, scan_audio_inputs},
    pipeline::output_sender::enttec_usb_pro,
};

/// Time between two scans.
const SCAN_INTERVAL: Duration = Duration::from_secs(1);

#[derive(Debug)]
pub(super) struct DeviceScan {
    watchers: BTreeSet<ClientId>,
    scanning: Arc<AtomicBool>,
    thread: Option<JoinHandle<()>>,
    results: (Sender<Devices>, Receiver<Devices>),
    latest: Devices,
}

impl Default for DeviceScan {
    fn default() -> Self {
        Self {
            watchers: BTreeSet::new(),
            scanning: Arc::default(),
            thread: None,
            results: unbounded(),
            latest: Devices::default(),
        }
    }
}

impl DeviceScan {
    pub(super) fn watch(&mut self, id: ClientId, watch: bool) {
        if watch {
            self.watchers.insert(id);
        } else {
            self.watchers.remove(&id);
        }
        self.update();
    }

    pub(super) fn disconnect(&mut self, id: ClientId) {
        if self.watchers.remove(&id) {
            self.update();
        }
    }

    fn update(&mut self) {
        let wanted = !self.watchers.is_empty();
        self.scanning.store(wanted, Relaxed);
        let running = self
            .thread
            .as_ref()
            .is_some_and(|thread| !thread.is_finished());
        if wanted && !running {
            let scanning = self.scanning.clone();
            let results = self.results.0.clone();
            self.thread = thread::Builder::new()
                .name("gled:devices".to_string())
                .spawn(move || scan(&scanning, &results))
                .map_err(|err| warn!("Could not start the device scan: {err}"))
                .ok();
        }
    }

    /// The devices found last.
    pub(super) fn devices(&mut self) -> Devices {
        while let Ok(Some(devices)) = self.results.1.try_recv() {
            self.latest = devices;
        }
        if self.watchers.is_empty() {
            return Devices::default();
        }
        Devices {
            enttec: enttec_usb_pro::serial_numbers(),
            ..self.latest.clone()
        }
    }
}

fn scan(scanning: &AtomicBool, results: &Sender<Devices>) {
    while scanning.load(Relaxed) {
        let audio_inputs = audio_device_labels(&scan_audio_inputs())
            .into_iter()
            .map(|(id, label)| (audio_device_id(&id), label))
            .collect();
        let devices = Devices {
            audio_inputs,
            ..Devices::default()
        };
        if results.send(devices).is_err() {
            return;
        }
        thread::sleep(SCAN_INTERVAL);
    }
}