1use std::{
6 collections::{BTreeMap, BTreeSet},
7 path::PathBuf,
8 sync::Arc,
9};
10
11use gled_model::{
12 asset::{
13 animation::Animation,
14 midi_controller::{MidiEffectParameter, MidiModeHighlight},
15 output_device::routing::{OutputRouting, OutputRoutings},
16 project::{Project, io::ArtnetConfig},
17 scene::grid::GridLocation,
18 },
19 asset_id::AssetId,
20 input::key_code::KeyCode,
21 patching::color_orders::ColorOrders,
22 sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
23 timing::BeatState,
24};
25use serde::{Deserialize, Serialize};
26use uuid::Uuid;
27
28use crate::{
29 action::UiAction,
30 assets::{AnyAsset, AnyAssetId},
31 bytes::SharedBytes,
32 midi::TestCommand,
33};
34
35#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
38pub struct OutputConfig {
39 pub universes: Arc<BTreeSet<u16>>,
40 pub routings: Arc<OutputRoutings>,
41 #[serde(default)]
43 pub color_orders: Arc<ColorOrders>,
44}
45
46#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
48pub struct RenderStats {
49 pub fps: f32,
50 pub max_frame_ms: f32,
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
56#[serde(default)]
57pub struct ServerStatus {
58 pub storage_task: Option<String>,
60 pub storage_progress: Option<f32>,
62 pub storage_error: Option<String>,
63 pub storage_working: bool,
65 pub staged_files: usize,
67 pub branches: Vec<String>,
68 pub current_branch: Option<String>,
69 pub storage_dir: String,
71 pub network_in: f32,
73 pub network_out: f32,
74 pub temperature: Option<f32>,
77 pub host_name: String,
80 pub link_peers: u64,
82 pub settings: ServerSettings,
83 pub artnet: ArtnetConfig,
86 pub devices: Devices,
88}
89
90#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
92#[serde(default)]
93pub struct Devices {
94 pub audio_inputs: Vec<(AudioDeviceId, String)>,
96 pub midi_outputs: Vec<String>,
97 pub enttec: Vec<String>,
99}
100
101pub const FPS_LIMIT_MAX: f32 = 333.0;
103
104#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
106#[serde(default)]
107pub struct ServerSettings {
108 pub fps_limit: f32,
111 pub ableton_link_read_only: bool,
113 pub git_url: String,
115 pub git_private_key: Option<String>,
117 pub git_use_passphrase: bool,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub enum StorageCommand {
123 Commit {
124 message: String,
125 },
126 Pull,
127 Push,
128 SwitchBranch(String),
129 Reclone,
131 SetGitPassphrase(String),
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137pub enum GridLine {
138 Row,
139 Column,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145pub enum TouchedControl {
146 MainDimmer,
147 SceneOpacity(GridLocation),
148 EffectParameter {
149 scene: GridLocation,
150 effect: usize,
151 parameter: MidiEffectParameter,
152 },
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158pub struct ModeSwitch {
159 pub number: u64,
160 pub highlight: Option<MidiModeHighlight>,
161}
162
163#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
165pub struct EngineState {
166 pub project: Option<Arc<Project>>,
170 pub project_id: Option<AssetId<Project>>,
171 pub selected_scene_instance: GridLocation,
172 pub selected_scene_effect: usize,
175 pub controller_line: Option<GridLine>,
178 pub midi_modes: BTreeMap<String, u8>,
181 pub last_mode_switch: Option<ModeSwitch>,
182 pub touched_controls: Vec<TouchedControl>,
183 pub midi_inputs: Vec<String>,
185 pub blackout: bool,
186 pub blackout_hold: bool,
187 pub midi_output_active: bool,
188 pub output_config: OutputConfig,
189 pub beat: BeatState,
190 pub tap_count: usize,
192 pub layout_generation: u64,
195 pub render: RenderStats,
196 pub status: ServerStatus,
197}
198
199impl EngineState {
200 pub fn without_project(&self) -> Self {
202 let Self {
203 project: _,
204 project_id,
205 selected_scene_instance,
206 selected_scene_effect,
207 controller_line,
208 midi_modes,
209 last_mode_switch,
210 touched_controls,
211 midi_inputs,
212 blackout,
213 blackout_hold,
214 midi_output_active,
215 output_config,
216 beat,
217 tap_count,
218 layout_generation,
219 render,
220 status,
221 } = self;
222 Self {
223 project: None,
224 project_id: *project_id,
225 selected_scene_instance: *selected_scene_instance,
226 selected_scene_effect: *selected_scene_effect,
227 controller_line: *controller_line,
228 midi_modes: midi_modes.clone(),
229 last_mode_switch: *last_mode_switch,
230 touched_controls: touched_controls.clone(),
231 midi_inputs: midi_inputs.clone(),
232 blackout: *blackout,
233 blackout_hold: *blackout_hold,
234 midi_output_active: *midi_output_active,
235 output_config: output_config.clone(),
236 beat: *beat,
237 tap_count: *tap_count,
238 layout_generation: *layout_generation,
239 render: *render,
240 status: status.clone(),
241 }
242 }
243
244 pub fn same_but_timing(&self, other: &Self) -> bool {
248 self.same_but_timing_and_project(other)
249 && match (self.project.as_deref(), other.project.as_deref()) {
250 (Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
252 (a, b) => a.is_none() && b.is_none(),
253 }
254 }
255
256 pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
259 let Self {
260 project: _,
261 project_id,
262 selected_scene_instance,
263 selected_scene_effect,
264 controller_line,
265 midi_modes,
266 last_mode_switch,
267 touched_controls,
268 midi_inputs,
269 blackout,
270 blackout_hold,
271 midi_output_active,
272 output_config,
273 beat: _,
274 tap_count,
275 layout_generation,
276 render: _,
277 status,
278 } = self;
279 *project_id == other.project_id
280 && *selected_scene_instance == other.selected_scene_instance
281 && *selected_scene_effect == other.selected_scene_effect
282 && *controller_line == other.controller_line
283 && *midi_modes == other.midi_modes
284 && *last_mode_switch == other.last_mode_switch
285 && *touched_controls == other.touched_controls
286 && *midi_inputs == other.midi_inputs
287 && *blackout == other.blackout
288 && *blackout_hold == other.blackout_hold
289 && *midi_output_active == other.midi_output_active
290 && *tap_count == other.tap_count
291 && *layout_generation == other.layout_generation
292 && *output_config == other.output_config
293 && *status == other.status
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub enum ClientInput {
300 Action {
304 sequence: u64,
305 action: UiAction,
306 },
307 Keys(BTreeSet<KeyCode>),
309 SetBeatsPerMinute(f32),
311 SaveAsset(AnyAsset),
313 DeleteAsset(AnyAssetId),
315 Storage(StorageCommand),
316 SetServerSettings(ServerSettings),
317 HoverOutputRouting(Option<OutputRouting>),
320 WatchDevices(bool),
323 WatchMidi(bool),
326 MidiLearning(bool),
328 MidiTest(TestCommand),
329 WatchSoundTriggers(BTreeSet<Uuid>),
331 SavePicture {
334 path: PathBuf,
335 bytes: SharedBytes,
336 },
337 RequestPicture(PathBuf),
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub enum EngineEvent {
345 ShaderReloaded(Option<AssetId<Animation>>),
347 Error(String),
349 Picture {
351 path: PathBuf,
352 picture: Result<SharedBytes, String>,
353 },
354}
355
356#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
359#[serde(default)]
360pub struct SoundSnapshot {
361 pub levels: BTreeMap<Uuid, f32>,
362 pub impulses: BTreeMap<Uuid, f32>,
364 pub spectrum: SharedBytes,
366 pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
369}
370
371impl SoundSnapshot {
372 pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
375 let level = *self.levels.get(id)?;
376 let impulse = self.impulses.get(id).copied().unwrap_or_default();
377 let spectrum = self
378 .trigger_spectra
379 .get(id)
380 .map(|bytes| {
381 bytes
382 .0
383 .as_chunks::<4>()
384 .0
385 .iter()
386 .map(|chunk| f32::from_le_bytes(*chunk))
387 .collect()
388 })
389 .unwrap_or_default();
390 Some((impulse, level, spectrum))
391 }
392}
393
394impl SoundLevels for SoundSnapshot {
395 fn level(&self, id: &Uuid) -> f32 {
396 self.levels.get(id).copied().unwrap_or(0.0)
397 }
398
399 fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
400 let mut bytes = [0; FREQ_BINS * 4];
401 let len = self.spectrum.0.len().min(bytes.len());
402 bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
403 bytes
404 }
405}
406
407type InputRoute = Box<dyn Fn(ClientInput)>;
409
410thread_local! {
411 static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
414 const { std::cell::RefCell::new(None) };
415}
416
417pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
419 INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
420}
421
422impl ClientInput {
423 pub fn enqueue(self) {
425 INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
426 Some(route) => route(self),
427 None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
428 });
429 }
430}