gled-model 2.34.0

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! A sound trigger as part of a curve: its settings and a stable id. The
//! settings live in the project, so they are saved, cloned and synced like
//! every other value. The running trigger state (filtered spectrum, envelope)
//! lives in `SoundData`, keyed by the id, and follows these settings through
//! `SoundData::sync_triggers`.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::trigger_params::{FREQ_BINS, SoundTriggerParams};

/// Settings of one sound trigger. Projects saved before triggers had an id get
/// a fresh one when they are loaded.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(from = "SavedTrigger")]
pub struct SoundTriggerRef {
    pub id: Uuid,
    pub params: SoundTriggerParams,
    pub f_center: f32,
    pub f_radius: f32,
    pub(crate) averaging_time: f32,
}

/// A trigger as saved. Builds of the `aislop` branch saved the band as
/// `center_bin`/`window_bins` (log-spaced bins) instead of `f_center` and
/// `f_radius`; such a trigger gets its band back from `params`.
#[derive(Deserialize)]
struct SavedTrigger {
    #[serde(default = "Uuid::new_v4")]
    id: Uuid,
    params: SoundTriggerParams,
    f_center: Option<f32>,
    f_radius: Option<f32>,
    averaging_time: f32,
}

impl From<SavedTrigger> for SoundTriggerRef {
    fn from(saved: SavedTrigger) -> Self {
        let (f_center, f_radius) = saved.params.filter_tune();
        Self {
            id: saved.id,
            params: saved.params,
            f_center: saved.f_center.unwrap_or(f_center),
            f_radius: saved.f_radius.unwrap_or(f_radius),
            averaging_time: saved.averaging_time,
        }
    }
}

impl Default for SoundTriggerRef {
    fn default() -> Self {
        Self {
            id: Uuid::new_v4(),
            params: SoundTriggerParams::default(),
            f_center: 8200.,
            f_radius: 990.,
            averaging_time: 0.1,
        }
    }
}

/// The current output of the running sound triggers, as curves read it. On
/// the machine with the audio input this is `SoundData`; a remote client gets
/// the levels from the server.
pub trait SoundLevels {
    /// Output level of a trigger in 0..=1, 0 while it is not running.
    fn level(&self, id: &Uuid) -> f32;
    /// The current spectrum, as the little-endian `f32` bytes effects read.
    fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4];
}

/// The settings of every sound trigger a value uses, by id.
pub type SoundTriggerSet = HashMap<Uuid, SoundTriggerParams>;

/// Values that can contain sound triggers.
pub trait CollectSoundTriggers {
    /// Adds the triggers of `self` to `triggers`. When two triggers share an
    /// id, the first one wins; copies of a value share their trigger.
    fn collect_sound_triggers(&self, triggers: &mut SoundTriggerSet);
}

impl CollectSoundTriggers for SoundTriggerRef {
    fn collect_sound_triggers(&self, triggers: &mut SoundTriggerSet) {
        triggers.entry(self.id).or_insert(self.params);
    }
}

impl<T: CollectSoundTriggers> CollectSoundTriggers for Option<T> {
    fn collect_sound_triggers(&self, triggers: &mut SoundTriggerSet) {
        if let Some(value) = self {
            value.collect_sound_triggers(triggers);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn loads_triggers_saved_without_an_id() {
        let json = serde_json::to_string(&SoundTriggerRef::default()).expect("serializes");
        let mut value: serde_json::Value = serde_json::from_str(&json).expect("json");
        value.as_object_mut().expect("object").remove("id");
        let old: SoundTriggerRef = serde_json::from_value(value).expect("loads without id");
        assert_eq!(old.f_center, 8200.);
    }

    #[test]
    fn loads_triggers_saved_with_bins() {
        let json = serde_json::to_string(&SoundTriggerRef::default()).expect("serializes");
        let mut value: serde_json::Value = serde_json::from_str(&json).expect("json");
        let object = value.as_object_mut().expect("object");
        object.remove("f_center");
        object.remove("f_radius");
        object.insert("center_bin".to_string(), 134.29.into());
        object.insert("window_bins".to_string(), 16.0.into());
        let old: SoundTriggerRef = serde_json::from_value(value).expect("loads with bins");
        let mut params = SoundTriggerParams::default();
        params.set_filter_tune(old.f_center, old.f_radius);
        assert_eq!(params, old.params);
    }

    #[test]
    fn copies_share_their_trigger() {
        let trigger = SoundTriggerRef::default();
        let mut set = SoundTriggerSet::new();
        trigger.collect_sound_triggers(&mut set);
        trigger.clone().collect_sound_triggers(&mut set);
        assert_eq!(set.len(), 1);
    }
}