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 sound::{AudioDeviceId, trigger_params::FREQ_BINS, trigger_ref::SoundLevels},
20 timing::BeatState,
21};
22use serde::{Deserialize, Serialize};
23use uuid::Uuid;
24
25use crate::{
26 action::UiAction,
27 assets::{AnyAsset, AnyAssetId},
28 bytes::SharedBytes,
29 midi::TestCommand,
30};
31
32#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
34pub struct OutputConfig {
35 pub universes: Arc<BTreeSet<u16>>,
36 pub routings: Arc<OutputRoutings>,
37}
38
39#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
41pub struct RenderStats {
42 pub fps: f32,
43 pub max_frame_ms: f32,
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
49#[serde(default)]
50pub struct ServerStatus {
51 pub storage_task: Option<String>,
53 pub storage_error: Option<String>,
54 pub storage_working: bool,
56 pub staged_files: usize,
58 pub branches: Vec<String>,
59 pub current_branch: Option<String>,
60 pub storage_dir: String,
62 pub network_in: f32,
64 pub network_out: f32,
65 pub temperature: Option<f32>,
68 pub host_name: String,
71 pub link_peers: u64,
73 pub settings: ServerSettings,
74 pub artnet: ArtnetConfig,
77 pub devices: Devices,
79 pub svg_templates: Vec<String>,
82}
83
84#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
86#[serde(default)]
87pub struct Devices {
88 pub audio_inputs: Vec<(AudioDeviceId, String)>,
90 pub midi_outputs: Vec<String>,
91 pub enttec: Vec<String>,
93}
94
95pub const FPS_LIMIT_MAX: f32 = 333.0;
97
98#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
100#[serde(default)]
101pub struct ServerSettings {
102 pub fps_limit: f32,
105 pub ableton_link_read_only: bool,
107 pub git_url: String,
109 pub git_private_key: Option<String>,
111 pub git_use_passphrase: bool,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub enum StorageCommand {
117 Commit {
118 message: String,
119 },
120 Pull,
121 Push,
122 SwitchBranch(String),
123 Reclone,
125 SetGitPassphrase(String),
127}
128
129#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
131pub struct EngineState {
132 pub project: Option<Project>,
133 pub project_id: Option<AssetId<Project>>,
134 pub selected_scene_instance: GridLocation,
135 pub blackout: bool,
136 pub blackout_hold: bool,
137 pub midi_output_active: bool,
138 pub output_config: OutputConfig,
139 pub beat: BeatState,
140 pub tap_count: usize,
142 pub layout_generation: u64,
144 pub render: RenderStats,
145 pub status: ServerStatus,
146}
147
148impl EngineState {
149 pub fn without_project(&self) -> Self {
151 let Self {
152 project: _,
153 project_id,
154 selected_scene_instance,
155 blackout,
156 blackout_hold,
157 midi_output_active,
158 output_config,
159 beat,
160 tap_count,
161 layout_generation,
162 render,
163 status,
164 } = self;
165 Self {
166 project: None,
167 project_id: *project_id,
168 selected_scene_instance: *selected_scene_instance,
169 blackout: *blackout,
170 blackout_hold: *blackout_hold,
171 midi_output_active: *midi_output_active,
172 output_config: output_config.clone(),
173 beat: *beat,
174 tap_count: *tap_count,
175 layout_generation: *layout_generation,
176 render: *render,
177 status: status.clone(),
178 }
179 }
180
181 pub fn same_but_timing(&self, other: &Self) -> bool {
185 let Self {
186 project,
187 project_id,
188 selected_scene_instance,
189 blackout,
190 blackout_hold,
191 midi_output_active,
192 output_config,
193 beat: _,
194 tap_count,
195 layout_generation,
196 render: _,
197 status,
198 } = self;
199 *project_id == other.project_id
200 && *selected_scene_instance == other.selected_scene_instance
201 && *blackout == other.blackout
202 && *blackout_hold == other.blackout_hold
203 && *midi_output_active == other.midi_output_active
204 && *tap_count == other.tap_count
205 && *layout_generation == other.layout_generation
206 && *output_config == other.output_config
207 && *status == other.status
208 && *project == other.project
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub enum ClientInput {
215 Action {
217 sequence: u64,
218 action: UiAction,
219 },
220 Keys(BTreeSet<KeyCode>),
222 SetBeatsPerMinute(f32),
224 SaveAsset(AnyAsset),
226 DeleteAsset(AnyAssetId),
228 Storage(StorageCommand),
229 SetServerSettings(ServerSettings),
230 HoverOutputRouting(Option<OutputRouting>),
233 WatchDevices(bool),
236 WatchMidi(bool),
239 MidiLearning(bool),
241 MidiTest(TestCommand),
242 UseSvgTemplate(String),
245 WatchSoundTriggers(BTreeSet<Uuid>),
247}
248
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251pub enum EngineEvent {
252 ShaderReloaded(Option<AssetId<Animation>>),
254 Error(String),
256}
257
258#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
261#[serde(default)]
262pub struct SoundSnapshot {
263 pub levels: BTreeMap<Uuid, f32>,
264 pub impulses: BTreeMap<Uuid, f32>,
266 pub spectrum: SharedBytes,
268 pub trigger_spectra: BTreeMap<Uuid, SharedBytes>,
271}
272
273impl SoundSnapshot {
274 pub fn reading(&self, id: &Uuid) -> Option<(f32, f32, Vec<f32>)> {
277 let level = *self.levels.get(id)?;
278 let impulse = self.impulses.get(id).copied().unwrap_or_default();
279 let spectrum = self
280 .trigger_spectra
281 .get(id)
282 .map(|bytes| {
283 bytes
284 .0
285 .chunks_exact(4)
286 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
287 .collect()
288 })
289 .unwrap_or_default();
290 Some((impulse, level, spectrum))
291 }
292}
293
294impl SoundLevels for SoundSnapshot {
295 fn level(&self, id: &Uuid) -> f32 {
296 self.levels.get(id).copied().unwrap_or(0.0)
297 }
298
299 fn spectrum_bytes(&self) -> [u8; FREQ_BINS * 4] {
300 let mut bytes = [0; FREQ_BINS * 4];
301 let len = self.spectrum.0.len().min(bytes.len());
302 bytes[..len].copy_from_slice(&self.spectrum.0[..len]);
303 bytes
304 }
305}
306
307type InputRoute = Box<dyn Fn(ClientInput)>;
309
310thread_local! {
311 static INPUT_ROUTE: std::cell::RefCell<Option<InputRoute>> =
314 const { std::cell::RefCell::new(None) };
315}
316
317pub fn route_inputs_on_this_thread(route: impl Fn(ClientInput) + 'static) {
319 INPUT_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
320}
321
322impl ClientInput {
323 pub fn enqueue(self) {
325 INPUT_ROUTE.with(|cell| match cell.borrow().as_ref() {
326 Some(route) => route(self),
327 None => tracing::warn!("Dropped {self:?}: this thread has no connection to the engine"),
328 });
329 }
330}