1use std::{
6 collections::{BTreeMap, BTreeSet},
7 sync::Arc,
8};
9
10use gled_model::{
11 asset::{
12 animation::Animation,
13 output_device::routing::{OutputRouting, OutputRoutings},
14 project::{Project, io::ArtnetConfig},
15 scene::grid::GridLocation,
16 },
17 asset_id::AssetId,
18 input::key_code::KeyCode,
19 patching::color_orders::ColorOrders,
20 sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
21 timing::BeatState,
22};
23use serde::{Deserialize, Serialize};
24use uuid::Uuid;
25
26use crate::{
27 action::UiAction,
28 assets::{AnyAsset, AnyAssetId},
29 bytes::SharedBytes,
30 midi::TestCommand,
31};
32
33#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
36pub struct OutputConfig {
37 pub universes: Arc<BTreeSet<u16>>,
38 pub routings: Arc<OutputRoutings>,
39 #[serde(default)]
41 pub color_orders: Arc<ColorOrders>,
42}
43
44#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
46pub struct RenderStats {
47 pub fps: f32,
48 pub max_frame_ms: f32,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
54#[serde(default)]
55pub struct ServerStatus {
56 pub storage_task: Option<String>,
58 pub storage_progress: Option<f32>,
60 pub storage_error: Option<String>,
61 pub storage_working: bool,
63 pub staged_files: usize,
65 pub branches: Vec<String>,
66 pub current_branch: Option<String>,
67 pub storage_dir: String,
69 pub network_in: f32,
71 pub network_out: f32,
72 pub temperature: Option<f32>,
75 pub host_name: String,
78 pub link_peers: u64,
80 pub settings: ServerSettings,
81 pub artnet: ArtnetConfig,
84 pub devices: Devices,
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
90#[serde(default)]
91pub struct Devices {
92 pub audio_inputs: Vec<(AudioDeviceId, String)>,
94 pub midi_outputs: Vec<String>,
95 pub enttec: Vec<String>,
97}
98
99pub const FPS_LIMIT_MAX: f32 = 333.0;
101
102#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
104#[serde(default)]
105pub struct ServerSettings {
106 pub fps_limit: f32,
109 pub ableton_link_read_only: bool,
111 pub git_url: String,
113 pub git_private_key: Option<String>,
115 pub git_use_passphrase: bool,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub enum StorageCommand {
121 Commit {
122 message: String,
123 },
124 Pull,
125 Push,
126 SwitchBranch(String),
127 Reclone,
129 SetGitPassphrase(String),
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
135pub struct EngineState {
136 pub project: Option<Arc<Project>>,
140 pub project_id: Option<AssetId<Project>>,
141 pub selected_scene_instance: GridLocation,
142 pub blackout: bool,
143 pub blackout_hold: bool,
144 pub midi_output_active: bool,
145 pub output_config: OutputConfig,
146 pub beat: BeatState,
147 pub tap_count: usize,
149 pub layout_generation: u64,
152 pub render: RenderStats,
153 pub status: ServerStatus,
154}
155
156impl EngineState {
157 pub fn without_project(&self) -> Self {
159 let Self {
160 project: _,
161 project_id,
162 selected_scene_instance,
163 blackout,
164 blackout_hold,
165 midi_output_active,
166 output_config,
167 beat,
168 tap_count,
169 layout_generation,
170 render,
171 status,
172 } = self;
173 Self {
174 project: None,
175 project_id: *project_id,
176 selected_scene_instance: *selected_scene_instance,
177 blackout: *blackout,
178 blackout_hold: *blackout_hold,
179 midi_output_active: *midi_output_active,
180 output_config: output_config.clone(),
181 beat: *beat,
182 tap_count: *tap_count,
183 layout_generation: *layout_generation,
184 render: *render,
185 status: status.clone(),
186 }
187 }
188
189 pub fn same_but_timing(&self, other: &Self) -> bool {
193 self.same_but_timing_and_project(other)
194 && match (self.project.as_deref(), other.project.as_deref()) {
195 (Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
197 (a, b) => a.is_none() && b.is_none(),
198 }
199 }
200
201 pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
204 let Self {
205 project: _,
206 project_id,
207 selected_scene_instance,
208 blackout,
209 blackout_hold,
210 midi_output_active,
211 output_config,
212 beat: _,
213 tap_count,
214 layout_generation,
215 render: _,
216 status,
217 } = self;
218 *project_id == other.project_id
219 && *selected_scene_instance == other.selected_scene_instance
220 && *blackout == other.blackout
221 && *blackout_hold == other.blackout_hold
222 && *midi_output_active == other.midi_output_active
223 && *tap_count == other.tap_count
224 && *layout_generation == other.layout_generation
225 && *output_config == other.output_config
226 && *status == other.status
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub enum ClientInput {
233 Action {
235 sequence: u64,
236 action: UiAction,
237 },
238 Keys(BTreeSet<KeyCode>),
240 SetBeatsPerMinute(f32),
242 SaveAsset(AnyAsset),
244 DeleteAsset(AnyAssetId),
246 Storage(StorageCommand),
247 SetServerSettings(ServerSettings),
248 HoverOutputRouting(Option<OutputRouting>),
251 WatchDevices(bool),
254 WatchMidi(bool),
257 MidiLearning(bool),
259 MidiTest(TestCommand),
260 WatchSoundTriggers(BTreeSet<Uuid>),
262}
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266pub enum EngineEvent {
267 ShaderReloaded(Option<AssetId<Animation>>),
269 Error(String),
271}
272
273#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
276#[serde(default)]
277pub struct SoundSnapshot {
278 pub levels: BTreeMap<Uuid, f32>,
279 pub impulses: BTreeMap<Uuid, f32>,
281 pub spectrum: SharedBytes,
283 pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
286}
287
288impl SoundSnapshot {
289 pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
292 let level = *self.levels.get(id)?;
293 let impulse = self.impulses.get(id).copied().unwrap_or_default();
294 let spectrum = self
295 .trigger_spectra
296 .get(id)
297 .map(|bytes| {
298 bytes
299 .0
300 .as_chunks::<4>()
301 .0
302 .iter()
303 .map(|chunk| f32::from_le_bytes(*chunk))
304 .collect()
305 })
306 .unwrap_or_default();
307 Some((impulse, level, spectrum))
308 }
309}
310
311impl SoundLevels for SoundSnapshot {
312 fn level(&self, id: &Uuid) -> f32 {
313 self.levels.get(id).copied().unwrap_or(0.0)
314 }
315
316 fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
317 let mut bytes = [0; FREQ_BINS * 4];
318 let len = self.spectrum.0.len().min(bytes.len());
319 bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
320 bytes
321 }
322}
323
324type InputRoute = Box<dyn Fn(ClientInput)>;
326
327thread_local! {
328 static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
331 const { std::cell::RefCell::new(None) };
332}
333
334pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
336 INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
337}
338
339impl ClientInput {
340 pub fn enqueue(self) {
342 INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
343 Some(route) => route(self),
344 None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
345 });
346 }
347}