1use std::{
6 collections::{BTreeMap, BTreeSet},
7 path::PathBuf,
8 sync::Arc,
9};
10
11use gled_model::{
12 asset::{
13 animation::Animation,
14 output_device::routing::{OutputRouting, OutputRoutings},
15 project::{Project, io::ArtnetConfig},
16 scene::grid::GridLocation,
17 },
18 asset_id::AssetId,
19 input::key_code::KeyCode,
20 patching::color_orders::ColorOrders,
21 sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
22 timing::BeatState,
23};
24use serde::{Deserialize, Serialize};
25use uuid::Uuid;
26
27use crate::{
28 action::UiAction,
29 assets::{AnyAsset, AnyAssetId},
30 bytes::SharedBytes,
31 midi::TestCommand,
32};
33
34#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
37pub struct OutputConfig {
38 pub universes: Arc<BTreeSet<u16>>,
39 pub routings: Arc<OutputRoutings>,
40 #[serde(default)]
42 pub color_orders: Arc<ColorOrders>,
43}
44
45#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
47pub struct RenderStats {
48 pub fps: f32,
49 pub max_frame_ms: f32,
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
55#[serde(default)]
56pub struct ServerStatus {
57 pub storage_task: Option<String>,
59 pub storage_progress: Option<f32>,
61 pub storage_error: Option<String>,
62 pub storage_working: bool,
64 pub staged_files: usize,
66 pub branches: Vec<String>,
67 pub current_branch: Option<String>,
68 pub storage_dir: String,
70 pub network_in: f32,
72 pub network_out: f32,
73 pub temperature: Option<f32>,
76 pub host_name: String,
79 pub link_peers: u64,
81 pub settings: ServerSettings,
82 pub artnet: ArtnetConfig,
85 pub devices: Devices,
87}
88
89#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
91#[serde(default)]
92pub struct Devices {
93 pub audio_inputs: Vec<(AudioDeviceId, String)>,
95 pub midi_outputs: Vec<String>,
96 pub enttec: Vec<String>,
98}
99
100pub const FPS_LIMIT_MAX: f32 = 333.0;
102
103#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
105#[serde(default)]
106pub struct ServerSettings {
107 pub fps_limit: f32,
110 pub ableton_link_read_only: bool,
112 pub git_url: String,
114 pub git_private_key: Option<String>,
116 pub git_use_passphrase: bool,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub enum StorageCommand {
122 Commit {
123 message: String,
124 },
125 Pull,
126 Push,
127 SwitchBranch(String),
128 Reclone,
130 SetGitPassphrase(String),
132}
133
134#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
136pub struct EngineState {
137 pub project: Option<Arc<Project>>,
141 pub project_id: Option<AssetId<Project>>,
142 pub selected_scene_instance: GridLocation,
143 pub selected_scene_effect: usize,
146 pub blackout: bool,
147 pub blackout_hold: bool,
148 pub midi_output_active: bool,
149 pub output_config: OutputConfig,
150 pub beat: BeatState,
151 pub tap_count: usize,
153 pub layout_generation: u64,
156 pub render: RenderStats,
157 pub status: ServerStatus,
158}
159
160impl EngineState {
161 pub fn without_project(&self) -> Self {
163 let Self {
164 project: _,
165 project_id,
166 selected_scene_instance,
167 selected_scene_effect,
168 blackout,
169 blackout_hold,
170 midi_output_active,
171 output_config,
172 beat,
173 tap_count,
174 layout_generation,
175 render,
176 status,
177 } = self;
178 Self {
179 project: None,
180 project_id: *project_id,
181 selected_scene_instance: *selected_scene_instance,
182 selected_scene_effect: *selected_scene_effect,
183 blackout: *blackout,
184 blackout_hold: *blackout_hold,
185 midi_output_active: *midi_output_active,
186 output_config: output_config.clone(),
187 beat: *beat,
188 tap_count: *tap_count,
189 layout_generation: *layout_generation,
190 render: *render,
191 status: status.clone(),
192 }
193 }
194
195 pub fn same_but_timing(&self, other: &Self) -> bool {
199 self.same_but_timing_and_project(other)
200 && match (self.project.as_deref(), other.project.as_deref()) {
201 (Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
203 (a, b) => a.is_none() && b.is_none(),
204 }
205 }
206
207 pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
210 let Self {
211 project: _,
212 project_id,
213 selected_scene_instance,
214 selected_scene_effect,
215 blackout,
216 blackout_hold,
217 midi_output_active,
218 output_config,
219 beat: _,
220 tap_count,
221 layout_generation,
222 render: _,
223 status,
224 } = self;
225 *project_id == other.project_id
226 && *selected_scene_instance == other.selected_scene_instance
227 && *selected_scene_effect == other.selected_scene_effect
228 && *blackout == other.blackout
229 && *blackout_hold == other.blackout_hold
230 && *midi_output_active == other.midi_output_active
231 && *tap_count == other.tap_count
232 && *layout_generation == other.layout_generation
233 && *output_config == other.output_config
234 && *status == other.status
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub enum ClientInput {
241 Action {
245 sequence: u64,
246 action: UiAction,
247 },
248 Keys(BTreeSet<KeyCode>),
250 SetBeatsPerMinute(f32),
252 SaveAsset(AnyAsset),
254 DeleteAsset(AnyAssetId),
256 Storage(StorageCommand),
257 SetServerSettings(ServerSettings),
258 HoverOutputRouting(Option<OutputRouting>),
261 WatchDevices(bool),
264 WatchMidi(bool),
267 MidiLearning(bool),
269 MidiTest(TestCommand),
270 WatchSoundTriggers(BTreeSet<Uuid>),
272 SavePicture {
275 path: PathBuf,
276 bytes: SharedBytes,
277 },
278 RequestPicture(PathBuf),
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub enum EngineEvent {
286 ShaderReloaded(Option<AssetId<Animation>>),
288 Error(String),
290 Picture {
292 path: PathBuf,
293 picture: Result<SharedBytes, String>,
294 },
295}
296
297#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
300#[serde(default)]
301pub struct SoundSnapshot {
302 pub levels: BTreeMap<Uuid, f32>,
303 pub impulses: BTreeMap<Uuid, f32>,
305 pub spectrum: SharedBytes,
307 pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
310}
311
312impl SoundSnapshot {
313 pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
316 let level = *self.levels.get(id)?;
317 let impulse = self.impulses.get(id).copied().unwrap_or_default();
318 let spectrum = self
319 .trigger_spectra
320 .get(id)
321 .map(|bytes| {
322 bytes
323 .0
324 .as_chunks::<4>()
325 .0
326 .iter()
327 .map(|chunk| f32::from_le_bytes(*chunk))
328 .collect()
329 })
330 .unwrap_or_default();
331 Some((impulse, level, spectrum))
332 }
333}
334
335impl SoundLevels for SoundSnapshot {
336 fn level(&self, id: &Uuid) -> f32 {
337 self.levels.get(id).copied().unwrap_or(0.0)
338 }
339
340 fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
341 let mut bytes = [0; FREQ_BINS * 4];
342 let len = self.spectrum.0.len().min(bytes.len());
343 bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
344 bytes
345 }
346}
347
348type InputRoute = Box<dyn Fn(ClientInput)>;
350
351thread_local! {
352 static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
355 const { std::cell::RefCell::new(None) };
356}
357
358pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
360 INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
361}
362
363impl ClientInput {
364 pub fn enqueue(self) {
366 INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
367 Some(route) => route(self),
368 None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
369 });
370 }
371}