Skip to main content

crabshell_services/
audio.rs

1//! Audio: the default sink's volume and mute, from the PulseAudio server,
2//! which pipewire-pulse serves on PipeWire systems.
3//!
4//! libpulse runs its mainloop on a thread of its own. Every libpulse call
5//! from this side runs on the main thread under the mainloop's lock.
6//! libpulse's callbacks run on its thread and only send a message over a
7//! calloop channel, so the signal is set on the main thread like any other.
8
9use 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
30/// How long the service waits before each reconnect.
31pub(crate) const RETRY: Duration = Duration::from_secs(2);
32
33/// How long the service keeps trying, from startup and from each drop of
34/// a working connection.
35pub(crate) const RETRY_FOR: Duration = Duration::from_secs(60);
36
37/// How long a command's result stands in for the server's reads.
38pub(crate) const SETTLE: Duration = Duration::from_millis(250);
39
40/// Where the audio server is.
41pub(crate) struct Server {
42  /// In libpulse's server syntax. `None` for libpulse's default, which
43  /// `PULSE_SERVER` and client.conf can set.
44  pub(crate) address: Option<String>,
45  /// How long to wait before each reconnect: [`RETRY`], less in tests.
46  pub(crate) retry: Duration,
47  /// How long to keep trying: [`RETRY_FOR`], less in tests.
48  pub(crate) retry_for: Duration,
49}
50
51/// The default sink: where sound goes unless an app picks another.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Sink {
54  /// Its name on the server, such as
55  /// `alsa_output.pci-0000_03_00.6.analog-stereo`.
56  pub name: String,
57  /// The average of its channels' volumes, rounded as `pactl` rounds
58  /// them. 100 is full volume, and other apps can set more.
59  pub volume_percent: u32,
60  /// Whether it is muted.
61  pub muted: bool,
62}
63
64/// The audio service, from `cx.audio()`. Cloning it is cheap.
65#[derive(Clone)]
66pub struct Audio {
67  sink: ReadSignal<Option<Sink>>,
68  /// `None` on a context the shell didn't set up.
69  service: Option<Rc<AudioService>>,
70}
71
72impl Audio {
73  /// The default sink. `None` while there is no audio server or the
74  /// server has no sink. Every caller gets the same signal.
75  pub fn default_sink(&self) -> ReadSignal<Option<Sink>> {
76    self.sink
77  }
78
79  /// Changes the default sink's volume by `percent`, louder when
80  /// positive, and keeps it within 0-100%, so a step from above 100%
81  /// lands on 100%. Every channel moves by the same amount, keeping their
82  /// balance, except that one reaching 0% or 100% stops there. With no
83  /// sink or no server it does nothing.
84  pub fn change_volume(&self, percent: i32) {
85    self.command(|pulse| pulse.change_volume(percent));
86  }
87
88  /// Mutes the default sink, or unmutes it. With no sink or no server it
89  /// does nothing.
90  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
106/// `cx.audio()`.
107pub(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
121/// The audio service of one shell. It connects on first use.
122pub(crate) struct AudioService {
123  shared: Rc<Shared>,
124  /// The default sink, under the root owner. Made on first use.
125  sink: OnceCell<Signal<Option<Sink>>>,
126  /// libpulse, from first use on. `None` before that, and if libpulse
127  /// couldn't start its thread.
128  pulse: RefCell<Option<Pulse>>,
129}
130
131/// What libpulse's thread sends: the generation of the context it came
132/// from, and what happened.
133pub(crate) type Message = (u32, Event);
134
135pub(crate) enum Event {
136  /// The context's state changed.
137  State,
138  /// The server changed, maybe its default sink.
139  Server,
140  /// The sink with this index changed.
141  Sink(u32),
142  /// The server's default sink name.
143  Default(Option<String>),
144  /// The sink asked for by name, or `None` for no such sink.
145  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      // On the loop's next turn, so `cx.audio()` never waits on libpulse.
160      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  /// Starts libpulse's thread and the first connection. Neither waits
170  /// for the server.
171  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    // From here on, dropping `pulse` stops libpulse's thread.
192    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  /// Connects to the server, or tries again later.
213  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    // The root is gone, and the shell with it.
219    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    // A message from a context that is gone is stale.
230    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  /// Drops the connection, shows no sink and tries again every `retry`
253  /// until `retry_for` has passed since startup, or since a working
254  /// connection dropped. Logs once per outage, not on every try, and
255  /// once on giving up.
256  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    // It was ready, so the server was there: give it a new while.
265    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      // No timer from here on, so audio never wakes the shell again.
276      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  /// Runs `f` from the event loop once, `delay` from now.
284  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
297/// One shell's libpulse objects and what they last told us. It stays on
298/// the main thread: `Mainloop` holds an `Rc`, so the compiler won't let it
299/// go to another.
300struct Pulse {
301  // Declared before `mainloop`, so they drop before it.
302  intro: Option<Introspector>,
303  context: Option<Context>,
304  mainloop: Mainloop,
305  /// Shared by every callback. Each `Sender` wakes the loop when it
306  /// drops, so a callback that holds its own would wake the shell when
307  /// libpulse drops it, with nothing to read.
308  sender: Arc<Sender<Message>>,
309  /// Bumped when a context goes, so the messages it left behind are
310  /// dropped.
311  generation: u32,
312  /// The default sink as last read.
313  shown: Option<Info>,
314  /// The default sink as the last command left it, and when that was
315  /// sent. pipewire-pulse answers a read sent right after a set with the
316  /// sink as it was before, so commands build on this instead until a
317  /// read echoes it or [`SETTLE`] passes.
318  asked: Option<(Info, Instant)>,
319  sink: Signal<Option<Sink>>,
320  /// Whether the server is gone and that was logged.
321  down: bool,
322  /// When the service stops trying to connect, if it can't.
323  retry_until: Instant,
324}
325
326/// The default sink as libpulse reported it.
327#[derive(Clone, PartialEq)]
328pub(crate) struct Info {
329  index: u32,
330  name: String,
331  volumes: ChannelVolumes,
332  muted: bool,
333}
334
335impl Pulse {
336  /// Makes a context and starts connecting it to `server`, or to
337  /// libpulse's default for `None`.
338  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    // NOAUTOSPAWN: a retry must never start a daemon next to pipewire-pulse.
346    let connected = context.connect(server, FlagSet::NOAUTOSPAWN, None);
347    self.context = Some(context);
348    connected.map_err(|error| format!("{error}"))
349  }
350
351  /// Acts on the context's new state. Once it is ready, subscribes to
352  /// sink and server events and reads the default sink. `Err` when the
353  /// connection failed or ended.
354  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      // Several queued messages can each find it ready.
361      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  /// Runs `call` under the lock if the context is ready, and says
390  /// whether it ran. libpulse returns no operation from a context that
391  /// isn't, and the binding panics on that.
392  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  /// Asks the server for its default sink's name.
404  fn read_default(&mut self) {
405    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
406    self.when_ready(|intro| {
407      // shortcut: on an error reply libpulse-binding panics on its thread instead of calling
408      // this, and the panic hook prints it raw; logging it needs the C API, so `unsafe`.
409      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  /// Asks for the sink called `name`, or shows none.
420  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  /// Shows `info`, or no sink. The signal is set only when that changes
438  /// what the bar shows, since every set repaints.
439  fn show(&mut self, info: Option<Info>) {
440    // An echo of the last command, or another sink, ends its stand-in.
441    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  /// What the next command builds on: the last one's result while it
456  /// stands in, or else the last read.
457  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  /// Drops the context and shows no sink. `true` when this starts an
495  /// outage.
496  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      // Cleared first: disconnecting calls the state callback.
501      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    // Unlocked, as libpulse asks. Once its thread is gone, the context
515    // and introspector drop with nothing running beside them.
516    self.mainloop.stop();
517  }
518}
519
520impl Info {
521  /// Copies what we need out of libpulse's `info`, which lives only for
522  /// the callback. A sink with no name or a bad volume is skipped.
523  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
548/// The mainloop's lock, held until this drops. libpulse's thread waits for
549/// it, so it must be let go even on an early return or a panic.
550struct 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
565/// libpulse runs its callbacks on its own thread but doesn't ask for
566/// `Send`. Wrapping each one in this makes the compiler check it.
567fn on_pa_thread<F: Send + 'static>(callback: F) -> F {
568  callback
569}
570
571/// The percent `pactl get-sink-volume` shows for `volume`: 100 is
572/// `PA_VOLUME_NORM`, and halves round up.
573fn percent(volume: Volume) -> u32 {
574  let norm = u64::from(Volume::NORMAL.0);
575  // At most u32::MAX * 100 / norm, which fits.
576  ((u64::from(volume.0) * 100 + norm / 2) / norm) as u32
577}
578
579/// `volumes` changed by `delta` percent. Every channel moves by the amount
580/// that puts their average on the shown percent plus `delta`, kept within
581/// 0-100%. A channel that would pass 0% or 100% stops there, and then the
582/// average falls short of that.
583fn 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    // Within 0..=norm, which fits.
590    volume.0 = (i64::from(volume.0) + shift).clamp(0, norm) as u32;
591  }
592  volumes
593}