gled-model 2.32.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! Which effect a binding works on, and how an endless encoder reports a
//! turn.

use serde::{Deserialize, Serialize};
use strum::{Display, EnumDiscriminants, EnumIter};

/// The effect of a scene a binding works on: the one selected in the
/// interface, or a fixed position in the scene's effect stack.
///
/// A fixed position is saved as the plain index it always was, so
/// controllers saved before the selected effect existed load unchanged.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, EnumDiscriminants)]
#[serde(from = "MidiEffectTargetRepr", into = "MidiEffectTargetRepr")]
#[strum_discriminants(name(MidiEffectTargetKind), derive(Display, EnumIter))]
pub enum MidiEffectTarget {
    #[default]
    #[strum_discriminants(strum(serialize = "Selected Effect"))]
    Selected,
    #[strum_discriminants(strum(serialize = "Effect Index"))]
    Index { index: u8 },
}

impl MidiEffectTarget {
    /// The effect's position in the stack, given the selected one.
    pub fn resolve(&self, selected_effect: usize) -> usize {
        match self {
            Self::Selected => selected_effect,
            Self::Index { index } => usize::from(*index),
        }
    }
}

/// The saved form: a bare number for an index, `"Selected"` otherwise.
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum MidiEffectTargetRepr {
    Index(u8),
    Named(MidiEffectTargetName),
}

#[derive(Serialize, Deserialize)]
enum MidiEffectTargetName {
    Selected,
}

impl From<MidiEffectTargetRepr> for MidiEffectTarget {
    fn from(repr: MidiEffectTargetRepr) -> Self {
        match repr {
            MidiEffectTargetRepr::Index(index) => Self::Index { index },
            MidiEffectTargetRepr::Named(MidiEffectTargetName::Selected) => Self::Selected,
        }
    }
}

impl From<MidiEffectTarget> for MidiEffectTargetRepr {
    fn from(target: MidiEffectTarget) -> Self {
        match target {
            MidiEffectTarget::Selected => Self::Named(MidiEffectTargetName::Selected),
            MidiEffectTarget::Index { index } => Self::Index(index),
        }
    }
}

/// An endless encoder: how it encodes a turn, and how many of its steps
/// make one unit of what it changes (one scene, one effect, one BPM, 1/127
/// of the dimmer).
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub struct MidiEncoder {
    pub encoding: MidiRelativeEncoding,
    pub steps_per_unit: u8,
}

impl Default for MidiEncoder {
    fn default() -> Self {
        Self {
            encoding: MidiRelativeEncoding::default(),
            steps_per_unit: 1,
        }
    }
}

/// How an endless encoder encodes a turn in the value byte. Both send
/// 1–63 for a clockwise turn, by that many steps; they differ in the
/// counter-clockwise direction.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, Display, EnumIter)]
pub enum MidiRelativeEncoding {
    /// 127 is one step counter-clockwise, 126 two, down to 65 (APC40 and
    /// similar).
    #[default]
    #[strum(serialize = "Two's Complement")]
    TwosComplement,
    /// 65 is one step counter-clockwise, 66 two, up to 127 (Mackie Control
    /// encoders, such as the iCON P1-X).
    #[strum(serialize = "Sign Magnitude")]
    SignMagnitude,
}

impl MidiRelativeEncoding {
    /// The steps a value byte stands for, clockwise positive.
    pub fn steps(self, value: u8) -> i32 {
        let value = i32::from(value);
        if value < 64 {
            return value;
        }
        match self {
            Self::TwosComplement => value - 128,
            Self::SignMagnitude => 64 - value,
        }
    }
}

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

    #[test]
    fn a_fixed_index_is_saved_as_the_plain_number_it_was() {
        let target = MidiEffectTarget::Index { index: 3 };
        assert_eq!(serde_json::to_string(&target).expect("serializes"), "3");
        assert_eq!(
            serde_json::from_str::<MidiEffectTarget>("3").expect("loads"),
            target
        );
    }

    #[test]
    fn the_selected_effect_is_saved_by_name() {
        assert_eq!(
            serde_json::to_string(&MidiEffectTarget::Selected).expect("serializes"),
            "\"Selected\""
        );
        assert_eq!(
            serde_json::from_str::<MidiEffectTarget>("\"Selected\"").expect("loads"),
            MidiEffectTarget::Selected
        );
    }

    #[test]
    fn encodings_agree_on_clockwise_turns() {
        for encoding in [
            MidiRelativeEncoding::TwosComplement,
            MidiRelativeEncoding::SignMagnitude,
        ] {
            assert_eq!(encoding.steps(0), 0, "{encoding}");
            assert_eq!(encoding.steps(1), 1, "{encoding}");
            assert_eq!(encoding.steps(7), 7, "{encoding}");
        }
    }

    #[test]
    fn twos_complement_counts_down_from_127() {
        assert_eq!(MidiRelativeEncoding::TwosComplement.steps(127), -1);
        assert_eq!(MidiRelativeEncoding::TwosComplement.steps(125), -3);
    }

    #[test]
    fn sign_magnitude_counts_up_from_65() {
        assert_eq!(MidiRelativeEncoding::SignMagnitude.steps(65), -1);
        assert_eq!(MidiRelativeEncoding::SignMagnitude.steps(67), -3);
    }
}