gled-core 2.31.2

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
use std::collections::HashMap;
use std::net::UdpSocket;
use std::time::{Duration, Instant};

use rosc::{OscPacket, OscType};

use crate::storage::asset::scene::effect::Effect;
use crate::storage::asset::scene::grid::GridLocation;
use crate::storage::asset::{project::Project, scene::instance::SceneInstance};
use crate::storage::asset_id::AssetId;
use crate::storage::collections::Collections;

use gled_model::patching::layout::LayoutSelection;

use super::scene::layout_feedback;
use super::snapshot::broadcast_snapshot;
use super::*;

fn msg(addr: &str, args: Vec<OscType>) -> OscMessage {
    OscMessage {
        addr: addr.to_string(),
        args,
    }
}

#[test]
fn parses_client_interest_subscribe_command() {
    let message = msg("/osc/state/subscribe", vec![]);
    assert_eq!(message.addr.parse(), Ok(ClientInterestCommand::Subscribe));
}

#[test]
fn parses_client_interest_unsubscribe_command() {
    let message = msg("/osc/state/unsubscribe", vec![]);
    assert_eq!(message.addr.parse(), Ok(ClientInterestCommand::Unsubscribe));
}

#[test]
fn parses_client_interest_heartbeat_command() {
    let message = msg("/osc/state/heartbeat", vec![]);
    assert_eq!(message.addr.parse(), Ok(ClientInterestCommand::Heartbeat));
}

/// What the feedback reports, sent back unchanged, selects the same
/// layout: TouchOSC and the like echo it.
#[test]
fn effect_layout_feedback_round_trips_through_the_parser() {
    use crate::storage::asset::project::scene_instance_path::SceneInstanceUnion;
    use gled_model::patching::layout::LayoutRef;

    for selection in [
        None,
        Some(LayoutSelection::new(LayoutRef::Topology, 0)),
        Some(LayoutSelection::new(LayoutRef::Layout(0), 0)),
        Some(LayoutSelection::new(LayoutRef::Layout(4), 2)),
    ] {
        let message = msg(
            "/scene/selected/effect/0/layout",
            vec![layout_feedback(selection)],
        );
        match parse_message(&message) {
            Ok(UiAction::SetSceneEffectLayout(SceneInstanceUnion::Selected, 0, index)) => {
                assert_eq!(
                    index,
                    selection.map(|selection| selection.layout.stack_index()),
                    "{selection:?}"
                );
            }
            other => panic!("{selection:?} parsed as {other:?}"),
        }
    }
}

#[test]
fn broadcast_snapshot_sends_core_fields_to_subscriber() {
    let sender = UdpSocket::bind("127.0.0.1:0").expect("failed to bind test socket");
    let receiver = UdpSocket::bind("127.0.0.1:0").expect("failed to bind test socket");
    receiver
        .set_read_timeout(Some(Duration::from_millis(200)))
        .expect("failed to set read timeout");
    let receiver_addr = receiver.local_addr().expect("failed to get local addr");

    let mut subscribers = HashMap::new();
    subscribers.insert(receiver_addr, Instant::now());

    let snapshot = OscStateSnapshot {
        project: None,
        selected_scene_instance: GridLocation { col: 0, row: 0 },
        blackout: true,
        beats_per_minute: 120.0,
        beat_progression: 0.25,
    };
    broadcast_snapshot(&sender, &subscribers, &snapshot);

    let mut found_blackout = false;
    let mut found_bpm = false;
    let mut buf = [0u8; rosc::decoder::MTU];
    while let Ok((size, _)) = receiver.recv_from(&mut buf) {
        if let Ok((_, OscPacket::Message(m))) = rosc::decoder::decode_udp(&buf[..size]) {
            match m.addr.as_str() {
                "/project/blackout" => {
                    assert_eq!(m.args, vec![OscType::Bool(true)]);
                    found_blackout = true;
                }
                "/timing/bpm" => {
                    assert_eq!(m.args, vec![OscType::Float(120.0)]);
                    found_bpm = true;
                }
                _ => {}
            }
        }
        if found_blackout && found_bpm {
            break;
        }
    }
    assert!(found_blackout, "/project/blackout packet not received");
    assert!(found_bpm, "/timing/bpm packet not received");
}

#[test]
fn broadcast_snapshot_sends_palette_fields() {
    use crate::storage::asset::palette::Palette;

    let sender = UdpSocket::bind("127.0.0.1:0").expect("failed to bind test socket");
    let receiver = UdpSocket::bind("127.0.0.1:0").expect("failed to bind test socket");
    receiver
        .set_read_timeout(Some(Duration::from_millis(200)))
        .expect("failed to set read timeout");
    let receiver_addr = receiver.local_addr().expect("failed to get local addr");

    let mut subscribers = HashMap::new();
    subscribers.insert(receiver_addr, Instant::now());

    let mut palette = Palette::default();
    *palette.primary.rgb_mut() = [1.0, 0.0, 0.0];

    let project = Project {
        palette: Some(palette),
        ..Default::default()
    };

    let snapshot = OscStateSnapshot {
        project: Some(std::sync::Arc::new(project)),
        selected_scene_instance: GridLocation { col: 0, row: 0 },
        blackout: false,
        beats_per_minute: 90.0,
        beat_progression: 0.0,
    };
    broadcast_snapshot(&sender, &subscribers, &snapshot);

    let mut found_primary = false;
    let mut buf = [0u8; rosc::decoder::MTU];
    while let Ok((size, _)) = receiver.recv_from(&mut buf) {
        if let Ok((_, OscPacket::Message(m))) = rosc::decoder::decode_udp(&buf[..size])
            && m.addr == "/project/palette/primary"
        {
            assert_eq!(
                m.args,
                vec![
                    OscType::Float(1.0),
                    OscType::Float(0.0),
                    OscType::Float(0.0)
                ]
            );
            found_primary = true;
            break;
        }
    }
    assert!(
        found_primary,
        "/project/palette/primary packet not received"
    );
}

#[test]
fn broadcast_snapshot_sends_extended_scene_fields_for_grid_and_quick() {
    let sender = UdpSocket::bind("127.0.0.1:0").expect("failed to bind test socket");
    let receiver = UdpSocket::bind("127.0.0.1:0").expect("failed to bind test socket");
    receiver
        .set_read_timeout(Some(Duration::from_millis(300)))
        .expect("failed to set read timeout");
    let receiver_addr = receiver.local_addr().expect("failed to get local addr");

    let mut subscribers = HashMap::new();
    subscribers.insert(receiver_addr, Instant::now());

    let mut project = Project::default();
    let location = GridLocation {
        col: 2,
        row: project.quick_row_index(),
    };
    let mut scene_instance =
        SceneInstance::from_scene_id(AssetId::default(), &Collections::default());
    scene_instance.input_dimmer = 0.42;
    scene_instance.ignore_main_dimmer = true;
    scene_instance.scene.effects.push(Effect::default());
    project.add_scene_instance(location, scene_instance);

    let snapshot = OscStateSnapshot {
        project: Some(std::sync::Arc::new(project)),
        selected_scene_instance: location,
        blackout: false,
        beats_per_minute: 90.0,
        beat_progression: 0.0,
    };
    broadcast_snapshot(&sender, &subscribers, &snapshot);

    let grid_input_dimmer_path =
        format!("/scene/grid/{}/{}/input_dimmer", location.col, location.row);
    let grid_effect_count_path =
        format!("/scene/grid/{}/{}/effect/count", location.col, location.row);
    let quick_ignore_main_dimmer_path = format!("/scene/quick/{}/ignore_main_dimmer", location.col);
    let quick_effect_opacity_path = format!("/scene/quick/{}/effect/0/opacity", location.col);

    let mut found_grid_input_dimmer = false;
    let mut found_grid_effect_count = false;
    let mut found_quick_ignore_main_dimmer = false;
    let mut found_quick_effect_opacity = false;

    let mut buf = [0u8; rosc::decoder::MTU];
    while let Ok((size, _)) = receiver.recv_from(&mut buf) {
        if let Ok((_, OscPacket::Message(m))) = rosc::decoder::decode_udp(&buf[..size]) {
            if m.addr == grid_input_dimmer_path {
                assert_eq!(m.args, vec![OscType::Float(0.42)]);
                found_grid_input_dimmer = true;
            }
            if m.addr == grid_effect_count_path {
                assert_eq!(m.args, vec![OscType::Int(1)]);
                found_grid_effect_count = true;
            }
            if m.addr == quick_ignore_main_dimmer_path {
                assert_eq!(m.args, vec![OscType::Bool(true)]);
                found_quick_ignore_main_dimmer = true;
            }
            if m.addr == quick_effect_opacity_path {
                assert_eq!(m.args, vec![OscType::Float(1.0)]);
                found_quick_effect_opacity = true;
            }
        }
        if found_grid_input_dimmer
            && found_grid_effect_count
            && found_quick_ignore_main_dimmer
            && found_quick_effect_opacity
        {
            break;
        }
    }

    assert!(
        found_grid_input_dimmer,
        "{grid_input_dimmer_path} packet not received"
    );
    assert!(
        found_grid_effect_count,
        "{grid_effect_count_path} packet not received"
    );
    assert!(
        found_quick_ignore_main_dimmer,
        "{quick_ignore_main_dimmer_path} packet not received"
    );
    assert!(
        found_quick_effect_opacity,
        "{quick_effect_opacity_path} packet not received"
    );
}