1mod actor;
3mod cd;
4mod cd_authority;
5mod cd_reader;
6mod cd_stream;
7mod crossfade;
8mod crossfade_feedback;
9mod crossfade_mix;
10mod crossfade_source;
11mod output;
12mod protocol;
13mod queue;
14mod removal;
15mod socket;
16mod spectrum;
17mod stream;
18mod transport;
19use crate::network::Client;
20use output::Signals;
21use serde_json::{Value, json};
22use std::sync::{
23 Arc, Mutex,
24 atomic::{AtomicU64, Ordering},
25 mpsc::{self, SyncSender},
26};
27use std::thread::JoinHandle;
28#[derive(Clone)]
29enum Command {
30 Policy(Policy),
31 Crossfade(u64),
32 Jump(usize),
33 Remove(usize, SyncSender<Result<(), String>>),
34 Pause,
35 Resume,
36 Next,
37 Previous,
38 Seek(u64, u64),
39 Stop,
40 Output(String),
41}
42#[derive(Clone, Copy, Default, PartialEq, Eq)]
43pub(super) enum Policy {
44 #[default]
45 WithoutEffects,
46 Dsp,
47}
48impl Policy {
49 fn parse(value: &str) -> Result<Self, String> {
50 match value {
51 "without-effects" => Ok(Self::WithoutEffects),
52 "dsp" => Ok(Self::Dsp),
53 _ => Err("Le flux PCM ne prend pas en charge ce mode de lecture.".into()),
54 }
55 }
56 fn name(self) -> &'static str {
57 match self {
58 Self::WithoutEffects => "without-effects",
59 Self::Dsp => "dsp",
60 }
61 }
62}
63struct Worker {
64 commands: SyncSender<Command>,
65 handle: JoinHandle<()>,
66}
67#[derive(Default)]
68struct State {
69 local_cd: bool,
70 cd_disc: Option<String>,
71 status: &'static str,
72 reference: String,
73 index: usize,
74 total: usize,
75 position: u64,
76 duration: Option<u64>,
77 seek_id: u64,
78 queue_id: u64,
79 rate: u32,
80 error: String,
81 confirmed: bool,
82 device: String,
83 policy: Policy,
84 shuffled: bool,
85 repeat: &'static str,
86 crossfade_ms: u64,
87 queue: Vec<usize>,
88 tracks: Vec<String>,
89 protected: Vec<usize>,
90}
91impl State {
92 fn clear_selection(&mut self) {
93 *self = State {
94 status: "idle",
95 device: std::mem::take(&mut self.device),
96 policy: self.policy,
97 repeat: self.repeat,
98 crossfade_ms: self.crossfade_ms,
99 error: std::mem::take(&mut self.error),
100 ..State::default()
101 };
102 }
103}
104pub struct Player {
106 worker: Mutex<Option<Worker>>,
107 next_seek: AtomicU64,
108 next_queue: AtomicU64,
109 state: Arc<Mutex<State>>,
110 signals: Arc<Signals>,
111}
112impl Default for Player {
113 fn default() -> Self {
114 Self {
115 worker: Mutex::new(None),
116 next_seek: AtomicU64::new(1),
117 next_queue: AtomicU64::new(1),
118 state: Arc::new(Mutex::new(State {
119 status: "idle",
120 ..State::default()
121 })),
122 signals: Arc::new(Signals::default()),
123 }
124 }
125}
126impl Player {
127 pub fn devices(&self) -> Result<Value, String> {
129 output::devices()
130 }
131 pub fn snapshot(&self) -> Result<Value, String> {
133 let s = self.state.lock().map_err(|_| "État audio indisponible.")?;
134 let levels = if s.status == "playing" && !self.signals.paused.load(Ordering::Relaxed) {
135 self.signals
136 .levels
137 .each_ref()
138 .map(|v| f32::from_bits(v.load(Ordering::Relaxed)))
139 } else {
140 [0.; 4]
141 };
142 let level = if s.status == "playing" && !self.signals.paused.load(Ordering::Relaxed) {
143 f32::from_bits(self.signals.level.load(Ordering::Relaxed))
144 } else {
145 0.
146 };
147 let spectrum = if s.status == "playing" && !self.signals.paused.load(Ordering::Relaxed) {
148 self.signals
149 .spectrum
150 .each_ref()
151 .map(|v| f32::from_bits(v.load(Ordering::Relaxed)))
152 } else {
153 [0.; 24]
154 };
155 Ok(
156 json!({"source":if s.local_cd { "cd" } else { "aede" },"cd_disc":s.cd_disc,"underrun_frames":self.signals.underrun_frames.load(Ordering::Relaxed),"spectrum":spectrum,"spectrum_hz":spectrum::frequencies(),"crossfade_ms":s.crossfade_ms,"repeat":if s.repeat.is_empty() { "off" } else { s.repeat },"queue_id":s.queue_id,"shuffled":s.shuffled,"queue":s.queue,"protected_occurrences":s.protected,"seek_id":s.seek_id,"level":level,"levels":levels,"status":s.status,"reference":s.reference,"index":s.index,"total":s.total,"position_ms":s.position,"duration_ms":s.duration,"sample_rate":s.rate,"error":s.error,"history_confirmed":s.confirmed,"device":s.device,"policy":s.policy.name(),"volume":f32::from_bits(self.signals.volume.load(Ordering::Relaxed))}),
157 )
158 }
159 pub fn start(
161 &self,
162 client: Client,
163 tracks: Vec<String>,
164 device: String,
165 index: usize,
166 policy: &str,
167 ) -> Result<(), String> {
168 let policy = Policy::parse(policy)?;
169 if tracks.is_empty()
170 || tracks.len() > 4096
171 || index >= tracks.len()
172 || device.len() > 2048
173 || tracks
174 .iter()
175 .any(|r| protocol::start(r, 48000, policy).is_err())
176 {
177 return Err(
178 "Sélection audio invalide ou trop volumineuse (4096 pistes maximum).".into(),
179 );
180 }
181 if !matches!(
182 client.account()["account"]["role"].as_str(),
183 Some("user" | "admin")
184 ) {
185 return Err("Votre compte ne permet pas la lecture.".into());
186 }
187 self.launch(
188 actor::Source::Remote(client),
189 tracks,
190 device,
191 index,
192 policy,
193 None,
194 )
195 }
196 fn launch(
197 &self,
198 source: actor::Source,
199 tracks: Vec<String>,
200 device: String,
201 index: usize,
202 policy: Policy,
203 permission: Option<cd_authority::Check>,
204 ) -> Result<(), String> {
205 let local_cd = matches!(source, actor::Source::Disc(..));
206 let cd_disc = match &source {
207 actor::Source::Disc(disc, _) => Some(disc.token.clone()),
208 actor::Source::Remote(_) => None,
209 };
210 let mut slot = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
211 self.signals.paused.store(true, Ordering::Relaxed);
212 if let Some(old) = slot.take() {
213 let _ = old.commands.send(Command::Stop);
214 old.handle
215 .join()
216 .map_err(|_| "Arrêt du lecteur interrompu.")?;
217 }
218 let (tx, rx) = mpsc::sync_channel(32);
219 let state = self.state.clone();
220 let signals = self.signals.clone();
221 let crossfade_ms = self
222 .state
223 .lock()
224 .map_err(|_| "État audio indisponible.")?
225 .crossfade_ms;
226 let repeat = self
227 .state
228 .lock()
229 .map_err(|_| "État audio indisponible.")?
230 .repeat;
231 *self.state.lock().map_err(|_| "État audio indisponible.")? = State {
232 local_cd,
233 cd_disc,
234 repeat,
235 crossfade_ms,
236 status: "connecting",
237 total: tracks.len(),
238 index,
239 queue: (0..tracks.len()).collect(),
240 tracks,
241 queue_id: self.next_queue.fetch_add(1, Ordering::Relaxed),
242 device: device.clone(),
243 policy,
244 ..State::default()
245 };
246 let handle = std::thread::spawn(move || {
247 let _authority = cd_authority::Guard::new(permission, source.failure());
248 if let Err(error) = actor::run(source, index, (device, policy), rx, &state, &signals) {
249 signals.paused.store(true, Ordering::Relaxed);
250 if let Ok(mut s) = state.lock() {
251 s.status = "error";
252 s.error = error;
253 }
254 }
255 });
256 *slot = Some(Worker {
257 commands: tx,
258 handle,
259 });
260 Ok(())
261 }
262 pub fn append(
265 &self,
266 id: u64,
267 expected_total: usize,
268 tracks: Vec<String>,
269 ) -> Result<(), String> {
270 if tracks.is_empty()
271 || tracks.len() > 4096
272 || tracks
273 .iter()
274 .any(|r| protocol::start(r, 48000, Policy::WithoutEffects).is_err())
275 {
276 return Err(
277 "Sélection audio invalide ou trop volumineuse (4096 pistes maximum).".into(),
278 );
279 }
280 let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
281 if worker.as_ref().is_none_or(|w| w.handle.is_finished()) {
282 return Err("Aucune lecture active.".into());
283 }
284 let mut state = self.state.lock().map_err(|_| "État audio indisponible.")?;
285 if state.local_cd {
286 return Err("Une file CD ne peut pas recevoir de pistes du catalogue.".into());
287 }
288 if state.queue_id != id
289 || state.total != expected_total
290 || state.total == 0
291 || state.tracks.len() != state.total
292 {
293 return Err("La liste de lecture a changé.".into());
294 }
295 let total = state
296 .total
297 .checked_add(tracks.len())
298 .filter(|n| *n <= 4096)
299 .ok_or("Sélection limitée à 4096 pistes.")?;
300 let previous = state.total;
301 state.tracks.extend(tracks);
302 state.queue.extend(previous..total);
303 state.total = total;
304 Ok(())
305 }
306 pub fn halt(&self) -> Result<(), String> {
308 self.stop(false)
309 }
310 pub fn clear(&self) -> Result<(), String> {
312 self.stop(true)
313 }
314 fn stop(&self, clear: bool) -> Result<(), String> {
315 let mut slot = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
316 self.signals.paused.store(true, Ordering::Relaxed);
317 if let Some(worker) = slot.as_ref()
318 && !worker.handle.is_finished()
319 {
320 worker
321 .commands
322 .try_send(Command::Stop)
323 .map_err(|_| "Lecteur occupé, réessayez.")?;
324 }
325 if let Some(worker) = slot.take() {
326 worker
327 .handle
328 .join()
329 .map_err(|_| "Arrêt audio interrompu.")?;
330 }
331 update(&self.state, |s| {
332 if clear {
333 s.clear_selection();
334 } else {
335 s.status = "idle";
336 s.tracks.clear();
337 s.protected.clear();
338 }
339 })
340 }
341 pub fn seek(&self, position: u64) -> Result<u64, String> {
343 if position > 86_400_000 {
344 return Err("Position invalide.".into());
345 }
346 let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
347 let worker = worker
348 .as_ref()
349 .filter(|w| !w.handle.is_finished())
350 .ok_or("Aucune lecture active.")?;
351 let ticket = self.next_seek.fetch_add(1, Ordering::Relaxed);
352 worker
353 .commands
354 .try_send(Command::Seek(position, ticket))
355 .map_err(|_| "Lecteur occupé, réessayez.")?;
356 self.signals.paused.store(true, Ordering::Relaxed);
357 Ok(ticket)
358 }
359 pub fn control(&self, action: &str, value: Value) -> Result<(), String> {
361 if action == "crossfade" {
362 let ms = value
363 .as_u64()
364 .filter(|ms| *ms <= 10000)
365 .ok_or("Durée du fondu invalide.")?;
366 let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
367 if let Some(worker) = worker
368 .as_ref()
369 .filter(|worker| !worker.handle.is_finished())
370 {
371 worker
372 .commands
373 .try_send(Command::Crossfade(ms))
374 .map_err(|_| "Lecteur occupé, réessayez.".to_owned())?;
375 return Ok(());
377 }
378 return update(&self.state, |state| state.crossfade_ms = ms);
379 }
380 if action == "repeat" {
381 let repeat = match value.as_str() {
382 Some("off") => "off",
383 Some("one") => "one",
384 Some("all") => "all",
385 _ => return Err("Répétition invalide.".into()),
386 };
387 return update(&self.state, |s| s.repeat = repeat);
388 }
389 if action == "volume" {
390 let volume = value
391 .as_f64()
392 .filter(|v| v.is_finite() && (0.0..=1.0).contains(v))
393 .ok_or("Volume invalide.")?;
394 self.signals
395 .volume
396 .store((volume as f32).to_bits(), Ordering::Relaxed);
397 return Ok(());
398 }
399 if action == "seek" {
400 return self
401 .seek(value.as_u64().ok_or("Position invalide.")?)
402 .map(|_| ());
403 }
404 if action == "queue" {
405 let id = value["queue_id"]
406 .as_u64()
407 .ok_or("File de lecture invalide.")?;
408 let order = value["order"]
409 .as_array()
410 .filter(|order| !order.is_empty() && order.len() <= 4096)
411 .ok_or("File de lecture invalide.")?
412 .iter()
413 .map(|index| {
414 index
415 .as_u64()
416 .filter(|index| *index < 4096)
417 .map(|index| index as usize)
418 .ok_or("File de lecture invalide.")
419 })
420 .collect::<Result<Vec<_>, _>>()?;
421 let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
422 if worker
423 .as_ref()
424 .is_none_or(|worker| worker.handle.is_finished())
425 {
426 return Err("Aucune lecture active.".into());
427 }
428 let mut state = self.state.lock().map_err(|_| "État audio indisponible.")?;
429 if state.queue_id != id
430 || order.len() != state.queue.len()
431 || {
432 let mut requested = order.clone();
433 requested.sort_unstable();
434 let mut retained = state.queue.clone();
435 retained.sort_unstable();
436 requested != retained
437 }
438 || state.total == 0
439 || state.index >= state.total
440 {
441 return Err("La liste de lecture a changé.".into());
442 }
443 let mut queue = queue::Queue::new(state.total, state.index);
444 queue.synchronize(&order, false, state.total)?;
445 state.queue = queue.order().to_vec();
446 state.shuffled = false;
447 return Ok(());
448 }
449 if action == "shuffle" {
450 let enabled = value.as_bool().ok_or("Lecture aléatoire invalide.")?;
451 let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
452 if worker
453 .as_ref()
454 .is_none_or(|worker| worker.handle.is_finished())
455 {
456 return Err("Aucune lecture active.".into());
457 }
458 let mut state = self.state.lock().map_err(|_| "État audio indisponible.")?;
459 if state.queue.is_empty() {
460 return Err("Aucune lecture active.".into());
461 }
462 let mut queue = queue::Queue::new(state.total, state.index);
463 queue.synchronize(&state.queue, state.shuffled, state.total)?;
464 queue.shuffle(enabled)?;
465 state.queue = queue.order().to_vec();
466 state.shuffled = queue.shuffled();
467 return Ok(());
468 }
469 if action == "policy" && self.is_cd()? {
470 if value == "without-effects" {
471 return Ok(());
472 }
473 return Err("Le traitement DSP n’est pas disponible pour les CD.".into());
474 }
475 let command = match action {
476 "policy" => Command::Policy(Policy::parse(value.as_str().ok_or("Mode invalide.")?)?),
477 "pause" => Command::Pause,
478 "resume" => Command::Resume,
479 "jump" => Command::Jump(
480 value
481 .as_u64()
482 .filter(|index| {
483 self.state
484 .lock()
485 .is_ok_and(|state| state.queue.contains(&(*index as usize)))
486 })
487 .ok_or("Position de file invalide.")? as usize,
488 ),
489 "next" => Command::Next,
490 "previous" => Command::Previous,
491 "stop" => Command::Stop,
492 "output" => Command::Output(
493 value
494 .as_str()
495 .filter(|v| v.len() <= 2048)
496 .ok_or("Sortie invalide.")?
497 .into(),
498 ),
499 _ => return Err("Commande audio non prise en charge.".into()),
500 };
501 let worker = self.worker.lock().map_err(|_| "Lecteur indisponible.")?;
502 if matches!(command, Command::Stop) && worker.is_none() {
503 return Ok(());
504 }
505 let worker = worker
506 .as_ref()
507 .filter(|w| !w.handle.is_finished())
508 .ok_or("Aucune lecture active.")?;
509 worker
510 .commands
511 .try_send(command)
512 .map_err(|_| "Lecteur occupé, réessayez.")?;
513 if action != "resume" && action != "policy" {
514 self.signals.paused.store(true, Ordering::Relaxed);
515 }
516 Ok(())
517 }
518}
519impl Drop for Player {
520 fn drop(&mut self) {
521 self.signals.paused.store(true, Ordering::Relaxed);
522 if let Ok(slot) = self.worker.get_mut()
523 && let Some(worker) = slot.take()
524 {
525 let _ = worker.commands.try_send(Command::Stop);
526 }
527 }
528}
529fn update(state: &Mutex<State>, work: impl FnOnce(&mut State)) -> Result<(), String> {
530 let mut s = state.lock().map_err(|_| "État audio indisponible.")?;
531 work(&mut s);
532 Ok(())
533}
534
535#[cfg(test)]
536#[path = "player_tests.rs"]
537mod tests;
538
539#[cfg(test)]
540#[path = "append_tests.rs"]
541mod append_tests;
542
543#[cfg(test)]
544#[path = "clear_tests.rs"]
545mod clear_tests;
546
547#[cfg(test)]
548#[path = "cd_tests.rs"]
549mod cd_tests;