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 selected_scene_effect: usize,
145 pub blackout: bool,
146 pub blackout_hold: bool,
147 pub midi_output_active: bool,
148 pub output_config: OutputConfig,
149 pub beat: BeatState,
150 pub tap_count: usize,
152 pub layout_generation: u64,
155 pub render: RenderStats,
156 pub status: ServerStatus,
157}
158
159impl EngineState {
160 pub fn without_project(&self) -> Self {
162 let Self {
163 project: _,
164 project_id,
165 selected_scene_instance,
166 selected_scene_effect,
167 blackout,
168 blackout_hold,
169 midi_output_active,
170 output_config,
171 beat,
172 tap_count,
173 layout_generation,
174 render,
175 status,
176 } = self;
177 Self {
178 project: None,
179 project_id: *project_id,
180 selected_scene_instance: *selected_scene_instance,
181 selected_scene_effect: *selected_scene_effect,
182 blackout: *blackout,
183 blackout_hold: *blackout_hold,
184 midi_output_active: *midi_output_active,
185 output_config: output_config.clone(),
186 beat: *beat,
187 tap_count: *tap_count,
188 layout_generation: *layout_generation,
189 render: *render,
190 status: status.clone(),
191 }
192 }
193
194 pub fn same_but_timing(&self, other: &Self) -> bool {
198 self.same_but_timing_and_project(other)
199 && match (self.project.as_deref(), other.project.as_deref()) {
200 (Some(a), Some(b)) => std::ptr::eq(a, b) || a == b,
202 (a, b) => a.is_none() && b.is_none(),
203 }
204 }
205
206 pub fn same_but_timing_and_project(&self, other: &Self) -> bool {
209 let Self {
210 project: _,
211 project_id,
212 selected_scene_instance,
213 selected_scene_effect,
214 blackout,
215 blackout_hold,
216 midi_output_active,
217 output_config,
218 beat: _,
219 tap_count,
220 layout_generation,
221 render: _,
222 status,
223 } = self;
224 *project_id == other.project_id
225 && *selected_scene_instance == other.selected_scene_instance
226 && *selected_scene_effect == other.selected_scene_effect
227 && *blackout == other.blackout
228 && *blackout_hold == other.blackout_hold
229 && *midi_output_active == other.midi_output_active
230 && *tap_count == other.tap_count
231 && *layout_generation == other.layout_generation
232 && *output_config == other.output_config
233 && *status == other.status
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub enum ClientInput {
240 Action {
244 sequence: u64,
245 action: UiAction,
246 },
247 Keys(BTreeSet<KeyCode>),
249 SetBeatsPerMinute(f32),
251 SaveAsset(AnyAsset),
253 DeleteAsset(AnyAssetId),
255 Storage(StorageCommand),
256 SetServerSettings(ServerSettings),
257 HoverOutputRouting(Option<OutputRouting>),
260 WatchDevices(bool),
263 WatchMidi(bool),
266 MidiLearning(bool),
268 MidiTest(TestCommand),
269 WatchSoundTriggers(BTreeSet<Uuid>),
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub enum EngineEvent {
276 ShaderReloaded(Option<AssetId<Animation>>),
278 Error(String),
280}
281
282#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
285#[serde(default)]
286pub struct SoundSnapshot {
287 pub levels: BTreeMap<Uuid, f32>,
288 pub impulses: BTreeMap<Uuid, f32>,
290 pub spectrum: SharedBytes,
292 pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
295}
296
297impl SoundSnapshot {
298 pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
301 let level = *self.levels.get(id)?;
302 let impulse = self.impulses.get(id).copied().unwrap_or_default();
303 let spectrum = self
304 .trigger_spectra
305 .get(id)
306 .map(|bytes| {
307 bytes
308 .0
309 .as_chunks::<4>()
310 .0
311 .iter()
312 .map(|chunk| f32::from_le_bytes(*chunk))
313 .collect()
314 })
315 .unwrap_or_default();
316 Some((impulse, level, spectrum))
317 }
318}
319
320impl SoundLevels for SoundSnapshot {
321 fn level(&self, id: &Uuid) -> f32 {
322 self.levels.get(id).copied().unwrap_or(0.0)
323 }
324
325 fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
326 let mut bytes = [0; FREQ_BINS * 4];
327 let len = self.spectrum.0.len().min(bytes.len());
328 bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
329 bytes
330 }
331}
332
333type InputRoute = Box<dyn Fn(ClientInput)>;
335
336thread_local! {
337 static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
340 const { std::cell::RefCell::new(None) };
341}
342
343pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
345 INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
346}
347
348impl ClientInput {
349 pub fn enqueue(self) {
351 INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
352 Some(route) => route(self),
353 None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
354 });
355 }
356}