1use std::cell::{OnceCell, RefCell};
10use std::rc::Rc;
11use std::sync::{Arc, Once};
12use std::time::{Duration, Instant};
13
14use calloop::channel::Sender;
15use calloop::timer::TimeoutAction;
16use crabshell_reactive::{ReadSignal, Signal};
17use crabshell_ui::Cx;
18use libpulse_binding::callbacks::ListResult;
19use libpulse_binding::context::introspect::{Introspector, ServerInfo, SinkInfo};
20use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
21use libpulse_binding::context::{Context, FlagSet, State};
22use libpulse_binding::mainloop::threaded::Mainloop;
23use libpulse_binding::volume::{ChannelVolumes, Volume};
24
25use crate::Shared;
26
27#[cfg(test)]
28mod tests;
29
30pub(crate) const RETRY: Duration = Duration::from_secs(2);
32
33pub(crate) const RETRY_FOR: Duration = Duration::from_secs(60);
36
37pub(crate) const SETTLE: Duration = Duration::from_millis(250);
39
40pub(crate) struct Server {
42 pub(crate) address: Option<String>,
45 pub(crate) retry: Duration,
47 pub(crate) retry_for: Duration,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Sink {
54 pub name: String,
57 pub volume_percent: u32,
60 pub muted: bool,
62}
63
64#[derive(Clone)]
66pub struct Audio {
67 sink: ReadSignal<Option<Sink>>,
68 service: Option<Rc<AudioService>>,
70}
71
72impl Audio {
73 pub fn default_sink(&self) -> ReadSignal<Option<Sink>> {
76 self.sink
77 }
78
79 pub fn change_volume(&self, percent: i32) {
85 self.command(|pulse| pulse.change_volume(percent));
86 }
87
88 pub fn toggle_mute(&self) {
91 self.command(Pulse::toggle_mute);
92 }
93
94 fn command(&self, command: impl FnOnce(&mut Pulse)) {
95 let mut pulse = self
96 .service
97 .as_ref()
98 .map(|service| service.pulse.borrow_mut());
99 match pulse.as_deref_mut().and_then(Option::as_mut) {
100 Some(pulse) => command(pulse),
101 None => tracing::debug!("no audio service: nothing to change"),
102 }
103 }
104}
105
106pub(crate) fn audio(cx: &Cx) -> Audio {
108 if let Some(service) = cx.service::<AudioService>() {
109 return service.audio();
110 }
111 static LOGGED: Once = Once::new();
112 LOGGED.call_once(|| {
113 tracing::warn!("cx.audio() needs the services the shell provides; there is no sink");
114 });
115 Audio {
116 sink: cx.signal(None).read_only(),
117 service: None,
118 }
119}
120
121pub(crate) struct AudioService {
123 shared: Rc<Shared>,
124 sink: OnceCell<Signal<Option<Sink>>>,
126 pulse: RefCell<Option<Pulse>>,
129}
130
131pub(crate) type Message = (u32, Event);
134
135pub(crate) enum Event {
136 State,
138 Server,
140 Sink(u32),
142 Default(Option<String>),
144 Info(Option<Info>),
146}
147
148impl AudioService {
149 pub(crate) fn new(shared: Rc<Shared>) -> AudioService {
150 AudioService {
151 shared,
152 sink: OnceCell::new(),
153 pulse: RefCell::new(None),
154 }
155 }
156
157 fn audio(self: &Rc<Self>) -> Audio {
158 let sink = *self.sink.get_or_init(|| {
159 self.after(Duration::ZERO, AudioService::start);
161 self.shared.root.signal(None)
162 });
163 Audio {
164 sink: sink.read_only(),
165 service: Some(Rc::clone(self)),
166 }
167 }
168
169 fn start(self: &Rc<Self>) {
172 let Some(server) = &self.shared.config.audio else {
173 return tracing::info!("no audio server is set, so there is no sink");
174 };
175 let sink = *self
176 .sink
177 .get()
178 .expect("the sink is made before libpulse starts");
179 let Some(event_loop) = self.shared.event_loop() else {
180 return;
181 };
182 let Some(mut mainloop) = Mainloop::new() else {
183 tracing::error!("libpulse couldn't make its mainloop: no audio");
184 return;
185 };
186 if let Err(error) = mainloop.start() {
187 tracing::error!(%error, "libpulse's thread didn't start: no audio");
188 return;
189 }
190 let (sender, channel) = calloop::channel::channel();
191 let pulse = Pulse {
193 intro: None,
194 context: None,
195 mainloop,
196 sender: Arc::new(sender),
197 generation: 0,
198 shown: None,
199 asked: None,
200 sink,
201 down: false,
202 retry_until: Instant::now() + server.retry_for,
203 };
204 let service = Rc::clone(self);
205 let on_message = Box::new(move |message| service.on_message(message));
206 if event_loop.insert_audio_channel(channel, on_message) {
207 *self.pulse.borrow_mut() = Some(pulse);
208 self.connect();
209 }
210 }
211
212 fn connect(self: &Rc<Self>) {
214 let mut pulse = self.pulse.borrow_mut();
215 let (Some(pulse), Some(server)) = (pulse.as_mut(), &self.shared.config.audio) else {
216 return;
217 };
218 if pulse.sink.try_with(|_| ()).is_none() {
220 return;
221 }
222 if let Err(why) = pulse.connect(server.address.as_deref()) {
223 self.lost(pulse, &why);
224 }
225 }
226
227 fn on_message(self: &Rc<Self>, (generation, event): Message) {
228 let mut pulse = self.pulse.borrow_mut();
229 let Some(pulse) = pulse
231 .as_mut()
232 .filter(|pulse| pulse.generation == generation)
233 else {
234 return;
235 };
236 match event {
237 Event::State => {
238 if let Err(why) = pulse.on_state() {
239 self.lost(pulse, &why);
240 }
241 }
242 Event::Server => pulse.read_default(),
243 Event::Sink(index) if pulse.shown.as_ref().is_some_and(|s| s.index == index) => {
244 pulse.read_default();
245 }
246 Event::Sink(_) => {}
247 Event::Default(name) => pulse.read_sink(name),
248 Event::Info(info) => pulse.show(info),
249 }
250 }
251
252 fn lost(self: &Rc<Self>, pulse: &mut Pulse, why: &str) {
257 let Some(Server {
258 retry, retry_for, ..
259 }) = self.shared.config.audio
260 else {
261 return;
262 };
263 let now = Instant::now();
264 if pulse.intro.is_some() {
266 pulse.retry_until = now + retry_for;
267 }
268 if pulse.disconnect() {
269 tracing::warn!(
270 reason = why,
271 "no audio server, trying again every {retry:?} for up to {retry_for:?}"
272 );
273 }
274 if now + retry > pulse.retry_until {
275 return tracing::info!(
277 "stopped trying to reach the audio server; it is tried again when the shell restarts"
278 );
279 }
280 self.after(retry, AudioService::connect);
281 }
282
283 fn after(self: &Rc<Self>, delay: Duration, f: fn(&Rc<AudioService>)) {
285 let Some(event_loop) = self.shared.event_loop() else {
286 return;
287 };
288 let service = Rc::clone(self);
289 let once = move || {
290 f(&service);
291 TimeoutAction::Drop
292 };
293 event_loop.insert_timer(Instant::now() + delay, Box::new(once));
294 }
295}
296
297struct Pulse {
301 intro: Option<Introspector>,
303 context: Option<Context>,
304 mainloop: Mainloop,
305 sender: Arc<Sender<Message>>,
309 generation: u32,
312 shown: Option<Info>,
314 asked: Option<(Info, Instant)>,
319 sink: Signal<Option<Sink>>,
320 down: bool,
322 retry_until: Instant,
324}
325
326#[derive(Clone, PartialEq)]
328pub(crate) struct Info {
329 index: u32,
330 name: String,
331 volumes: ChannelVolumes,
332 muted: bool,
333}
334
335impl Pulse {
336 fn connect(&mut self, server: Option<&str>) -> Result<(), String> {
339 let lock = Locked::new(&mut self.mainloop);
340 let mut context = Context::new(&*lock.0, "crabshell").ok_or("libpulse made no context")?;
341 let (sender, generation) = (Arc::clone(&self.sender), self.generation);
342 context.set_state_callback(Some(Box::new(on_pa_thread(move || {
343 let _ = sender.send((generation, Event::State));
344 }))));
345 let connected = context.connect(server, FlagSet::NOAUTOSPAWN, None);
347 self.context = Some(context);
348 connected.map_err(|error| format!("{error}"))
349 }
350
351 fn on_state(&mut self) -> Result<(), String> {
355 let lock = Locked::new(&mut self.mainloop);
356 let Some(context) = self.context.as_mut() else {
357 return Ok(());
358 };
359 match context.get_state() {
360 State::Ready if self.intro.is_none() => {
362 let (sender, generation) = (Arc::clone(&self.sender), self.generation);
363 let on_event = move |facility, _, index| {
364 let event = match facility {
365 Some(Facility::Server) => Event::Server,
366 Some(Facility::Sink) => Event::Sink(index),
367 _ => return,
368 };
369 let _ = sender.send((generation, event));
370 };
371 context.set_subscribe_callback(Some(Box::new(on_pa_thread(on_event))));
372 let interest = InterestMaskSet::SINK | InterestMaskSet::SERVER;
373 context.subscribe(interest, on_pa_thread(|_| {}));
374 self.intro = Some(context.introspect());
375 drop(lock);
376 if std::mem::take(&mut self.down) {
377 tracing::info!("the audio server is back");
378 } else {
379 tracing::debug!("connected to the audio server");
380 }
381 self.read_default();
382 Ok(())
383 }
384 State::Failed | State::Terminated => Err(format!("{}", context.errno())),
385 _ => Ok(()),
386 }
387 }
388
389 fn when_ready(&mut self, call: impl FnOnce(&mut Introspector)) -> bool {
393 let _lock = Locked::new(&mut self.mainloop);
394 match (&self.context, &mut self.intro) {
395 (Some(context), Some(intro)) if context.get_state() == State::Ready => {
396 call(intro);
397 true
398 }
399 _ => false,
400 }
401 }
402
403 fn read_default(&mut self) {
405 let (sender, generation) = (Arc::clone(&self.sender), self.generation);
406 self.when_ready(|intro| {
407 intro.get_server_info(on_pa_thread(move |info: &ServerInfo| {
410 let name = info
411 .default_sink_name
412 .as_deref()
413 .filter(|name| !name.is_empty());
414 let _ = sender.send((generation, Event::Default(name.map(str::to_owned))));
415 }));
416 });
417 }
418
419 fn read_sink(&mut self, name: Option<String>) {
421 let Some(name) = name else {
422 return self.show(None);
423 };
424 let (sender, generation) = (Arc::clone(&self.sender), self.generation);
425 self.when_ready(|intro| {
426 let mut found = None;
427 let on_result = move |result: ListResult<&SinkInfo>| match result {
428 ListResult::Item(info) => found = Info::read(info),
429 ListResult::End | ListResult::Error => {
430 let _ = sender.send((generation, Event::Info(found.take())));
431 }
432 };
433 intro.get_sink_info_by_name(&name, on_pa_thread(on_result));
434 });
435 }
436
437 fn show(&mut self, info: Option<Info>) {
440 let stands = match (&self.asked, &info) {
442 (Some((asked, _)), Some(read)) => read != asked && read.index == asked.index,
443 _ => false,
444 };
445 if !stands {
446 self.asked = None;
447 }
448 let sink = info.as_ref().map(Info::sink);
449 self.shown = info;
450 if self.sink.try_with(|shown| *shown != sink) == Some(true) {
451 self.sink.set(sink);
452 }
453 }
454
455 fn base(&self) -> Option<Info> {
458 match &self.asked {
459 Some((asked, at)) if at.elapsed() < SETTLE => Some(asked.clone()),
460 _ => self.shown.clone(),
461 }
462 }
463
464 fn change_volume(&mut self, percent: i32) {
465 let Some(mut next) = self.base() else {
466 return tracing::debug!("no default sink: the volume stays");
467 };
468 next.volumes = changed(next.volumes, percent);
469 let sent = self.when_ready(|intro| {
470 intro.set_sink_volume_by_name(&next.name, &next.volumes, None);
471 });
472 if sent {
473 self.asked = Some((next, Instant::now()));
474 } else {
475 tracing::debug!("no audio server: the volume stays");
476 }
477 }
478
479 fn toggle_mute(&mut self) {
480 let Some(mut next) = self.base() else {
481 return tracing::debug!("no default sink: nothing to mute");
482 };
483 next.muted = !next.muted;
484 let sent = self.when_ready(|intro| {
485 intro.set_sink_mute_by_name(&next.name, next.muted, None);
486 });
487 if sent {
488 self.asked = Some((next, Instant::now()));
489 } else {
490 tracing::debug!("no audio server: nothing to mute");
491 }
492 }
493
494 fn disconnect(&mut self) -> bool {
497 let lock = Locked::new(&mut self.mainloop);
498 self.intro = None;
499 if let Some(mut context) = self.context.take() {
500 context.set_state_callback(None);
502 context.set_subscribe_callback(None);
503 context.disconnect();
504 }
505 drop(lock);
506 self.generation += 1;
507 self.show(None);
508 !std::mem::replace(&mut self.down, true)
509 }
510}
511
512impl Drop for Pulse {
513 fn drop(&mut self) {
514 self.mainloop.stop();
517 }
518}
519
520impl Info {
521 fn read(info: &SinkInfo) -> Option<Info> {
524 let (Some(name), true) = (info.name.as_deref(), info.volume.is_valid()) else {
525 tracing::warn!(
526 index = info.index,
527 "skipped a sink with no name or a bad volume"
528 );
529 return None;
530 };
531 Some(Info {
532 index: info.index,
533 name: name.to_owned(),
534 volumes: info.volume,
535 muted: info.mute,
536 })
537 }
538
539 fn sink(&self) -> Sink {
540 Sink {
541 name: self.name.clone(),
542 volume_percent: percent(self.volumes.avg()),
543 muted: self.muted,
544 }
545 }
546}
547
548struct Locked<'a>(&'a mut Mainloop);
551
552impl<'a> Locked<'a> {
553 fn new(mainloop: &'a mut Mainloop) -> Locked<'a> {
554 mainloop.lock();
555 Locked(mainloop)
556 }
557}
558
559impl Drop for Locked<'_> {
560 fn drop(&mut self) {
561 self.0.unlock();
562 }
563}
564
565fn on_pa_thread<F: Send + 'static>(callback: F) -> F {
568 callback
569}
570
571fn percent(volume: Volume) -> u32 {
574 let norm = u64::from(Volume::NORMAL.0);
575 ((u64::from(volume.0) * 100 + norm / 2) / norm) as u32
577}
578
579fn changed(mut volumes: ChannelVolumes, delta: i32) -> ChannelVolumes {
584 let norm = i64::from(Volume::NORMAL.0);
585 let average = volumes.avg();
586 let target = (i64::from(percent(average)) + i64::from(delta)).clamp(0, 100);
587 let shift = (target * norm + 50) / 100 - i64::from(average.0);
588 for volume in volumes.get_mut() {
589 volume.0 = (i64::from(volume.0) + shift).clamp(0, norm) as u32;
591 }
592 volumes
593}