use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use gled_model::{
asset::{
animation::Animation,
output_device::routing::{OutputRouting, OutputRoutings},
project::{Project, io::ArtnetConfig},
scene::grid::GridLocation,
},
asset_id::AssetId,
input::key_code::KeyCode,
patching::color_orders::ColorOrders,
sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
timing::BeatState,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
action::UiAction,
assets::{AnyAsset, AnyAssetId},
bytes::SharedBytes,
midi::TestCommand,
};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct OutputConfig {
pub universes: Arc<BTreeSet<u16>>,
pub routings: Arc<OutputRoutings>,
#[serde(default)]
pub color_orders: Arc<ColorOrders>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct RenderStats {
pub fps: f32,
pub max_frame_ms: f32,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerStatus {
pub storage_task: Option<String>,
pub storage_progress: Option<f32>,
pub storage_error: Option<String>,
pub storage_working: bool,
pub staged_files: usize,
pub branches: Vec<String>,
pub current_branch: Option<String>,
pub storage_dir: String,
pub network_in: f32,
pub network_out: f32,
pub temperature: Option<f32>,
pub host_name: String,
pub link_peers: u64,
pub settings: ServerSettings,
pub artnet: ArtnetConfig,
pub devices: Devices,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Devices {
pub audio_inputs: Vec<(AudioDeviceId, String)>,
pub midi_outputs: Vec<String>,
pub enttec: Vec<String>,
}
pub const FPS_LIMIT_MAX: f32 = 333.0;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerSettings {
pub fps_limit: f32,
pub ableton_link_read_only: bool,
pub git_url: String,
pub git_private_key: Option<String>,
pub git_use_passphrase: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StorageCommand {
Commit {
message: String,
},
Pull,
Push,
SwitchBranch(String),
Reclone,
SetGitPassphrase(String),
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EngineState {
pub project: Option<Arc<Project>>,
pub project_id: Option<AssetId<Project>>,
pub selected_scene_instance: GridLocation,
pub blackout: bool,
pub blackout_hold: bool,
pub midi_output_active: bool,
pub output_config: OutputConfig,
pub beat: BeatState,
pub tap_count: usize,
pub layout_generation: u64,
pub render: RenderStats,
pub status: ServerStatus,
}
impl EngineState {
pub fn without_project(&self) -> Self {
let Self {
project: _,
project_id,
selected_scene_instance,
blackout,
blackout_hold,
midi_output_active,
output_config,
beat,
tap_count,
layout_generation,
render,
status,
} = self;
Self {
project: None,
project_id: *project_id,
selected_scene_instance: *selected_scene_instance,
blackout: *blackout,
blackout_hold: *blackout_hold,
midi_output_active: *midi_output_active,
output_config: output_config.clone(),
beat: *beat,
tap_count: *tap_count,
layout_generation: *layout_generation,
render: *render,
status: status.clone(),
}
}
pub fn same_but_timing(&self, other: &Self) -> bool {
self.same_but_timing_and_project(other)
&& match (self.project.as_deref(), other.project.as_deref()) {
(Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
(a, b) => a.is_none() && b.is_none(),
}
}
pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
let Self {
project: _,
project_id,
selected_scene_instance,
blackout,
blackout_hold,
midi_output_active,
output_config,
beat: _,
tap_count,
layout_generation,
render: _,
status,
} = self;
*project_id == other.project_id
&& *selected_scene_instance == other.selected_scene_instance
&& *blackout == other.blackout
&& *blackout_hold == other.blackout_hold
&& *midi_output_active == other.midi_output_active
&& *tap_count == other.tap_count
&& *layout_generation == other.layout_generation
&& *output_config == other.output_config
&& *status == other.status
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ClientInput {
Action {
sequence: u64,
action: UiAction,
},
Keys(BTreeSet<KeyCode>),
SetBeatsPerMinute(f32),
SaveAsset(AnyAsset),
DeleteAsset(AnyAssetId),
Storage(StorageCommand),
SetServerSettings(ServerSettings),
HoverOutputRouting(Option<OutputRouting>),
WatchDevices(bool),
WatchMidi(bool),
MidiLearning(bool),
MidiTest(TestCommand),
WatchSoundTriggers(BTreeSet<Uuid>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EngineEvent {
ShaderReloaded(Option<AssetId<Animation>>),
Error(String),
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct SoundSnapshot {
pub levels: BTreeMap<Uuid, f32>,
pub impulses: BTreeMap<Uuid, f32>,
pub spectrum: SharedBytes,
pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
}
impl SoundSnapshot {
pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
let level = *self.levels.get(id)?;
let impulse = self.impulses.get(id).copied().unwrap_or_default();
let spectrum = self
.trigger_spectra
.get(id)
.map(|bytes| {
bytes
.0
.as_chunks::<4>()
.0
.iter()
.map(|chunk| f32::from_le_bytes(*chunk))
.collect()
})
.unwrap_or_default();
Some((impulse, level, spectrum))
}
}
impl SoundLevels for SoundSnapshot {
fn level(&self, id: &Uuid) -> f32 {
self.levels.get(id).copied().unwrap_or(0.0)
}
fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
let mut bytes = [0; FREQ_BINS * 4];
let len = self.spectrum.0.len().min(bytes.len());
bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
bytes
}
}
type InputRoute = Box<dyn Fn(ClientInput)>;
thread_local! {
static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
const { std::cell::RefCell::new(None) };
}
pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
}
impl ClientInput {
pub fn enqueue(self) {
INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
Some(route) => route(self),
None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
});
}
}