Skip to main content

crabshell_services/
audio.rs

1//! Audio: the default sink's volume and mute, and every sink, source and
2//! app stream with theirs, from the PulseAudio server, which
3//! pipewire-pulse serves on PipeWire systems.
4//!
5//! libpulse runs its mainloop on a thread of its own. Every libpulse call
6//! from this side runs on the main thread under the mainloop's lock.
7//! libpulse's callbacks run on its thread and only send a message over a
8//! calloop channel, so the signal is set on the main thread like any other.
9
10use std::cell::{OnceCell, RefCell};
11use std::rc::Rc;
12use std::sync::{Arc, Once};
13use std::time::{Duration, Instant};
14
15use calloop::channel::Sender;
16use calloop::timer::TimeoutAction;
17use crabshell_reactive::{ReadSignal, Signal};
18use crabshell_ui::Cx;
19use libpulse_binding::callbacks::ListResult;
20use libpulse_binding::context::introspect::{
21  Introspector, ServerInfo, SinkInfo, SinkInputInfo, SourceInfo,
22};
23use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
24use libpulse_binding::context::{Context, FlagSet, State};
25use libpulse_binding::mainloop::threaded::Mainloop;
26use libpulse_binding::volume::{ChannelVolumes, Volume};
27
28use crate::Shared;
29use crate::dbus::set_changed;
30
31#[cfg(test)]
32mod tests;
33
34/// How long the service waits before each reconnect.
35pub(crate) const RETRY: Duration = Duration::from_secs(2);
36
37/// How long the service keeps trying, from startup and from each drop of
38/// a working connection.
39pub(crate) const RETRY_FOR: Duration = Duration::from_secs(60);
40
41/// How long a command's result stands in for the server's reads.
42pub(crate) const SETTLE: Duration = Duration::from_millis(250);
43
44/// The most sinks, sources or streams kept, each.
45const MAX_NODES: usize = 128;
46
47/// Where the audio server is.
48pub(crate) struct Server {
49  /// In libpulse's server syntax. `None` for libpulse's default, which
50  /// `PULSE_SERVER` and client.conf can set.
51  pub(crate) address: Option<String>,
52  /// How long to wait before each reconnect: [`RETRY`], less in tests.
53  pub(crate) retry: Duration,
54  /// How long to keep trying: [`RETRY_FOR`], less in tests.
55  pub(crate) retry_for: Duration,
56}
57
58/// The default sink: where sound goes unless an app picks another.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Sink {
61  /// Its name on the server, such as
62  /// `alsa_output.pci-0000_03_00.6.analog-stereo`.
63  pub name: String,
64  /// The average of its channels' volumes, rounded as `pactl` rounds
65  /// them. 100 is full volume, and other apps can set more.
66  pub volume_percent: u32,
67  /// Whether it is muted.
68  pub muted: bool,
69}
70
71/// A sink or a source: where sound goes out, such as speakers, or comes
72/// in, such as a microphone.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct AudioNode {
75  /// Its name on the server, which the setters take.
76  pub name: String,
77  /// What a person calls it, such as `Built-in Audio Analog Stereo`.
78  pub description: String,
79  /// The average of its channels' volumes, as [`Sink`]'s.
80  pub volume_percent: u32,
81  /// Whether it is muted.
82  pub muted: bool,
83}
84
85/// An app playing sound: one of the server's sink inputs.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct AudioStream {
88  /// The server's number for it, which the setters take.
89  pub index: u32,
90  /// The app's name, such as `Firefox`.
91  pub app_name: String,
92  /// What it plays, such as a tab's title.
93  pub title: String,
94  /// The app's icon name, or `""`.
95  pub icon_name: String,
96  /// The name of the sink it plays to.
97  pub sink: String,
98  /// The average of its channels' volumes, as [`Sink`]'s.
99  pub volume_percent: u32,
100  /// Whether it is muted.
101  pub muted: bool,
102}
103
104/// The audio service, from `cx.audio()`. Cloning it is cheap.
105#[derive(Clone)]
106pub struct Audio {
107  sink: ReadSignal<Option<Sink>>,
108  lists: ReadLists,
109  /// `None` on a context the shell didn't set up.
110  service: Option<Rc<AudioService>>,
111}
112
113impl Audio {
114  /// The default sink. `None` while there is no audio server or the
115  /// server has no sink. Every caller gets the same signal.
116  pub fn default_sink(&self) -> ReadSignal<Option<Sink>> {
117    self.sink
118  }
119
120  /// Changes the default sink's volume by `percent`, louder when
121  /// positive, and keeps it within 0-100%, so a step from above 100%
122  /// lands on 100%. Every channel moves by the same amount, keeping their
123  /// balance, except that one reaching 0% or 100% stops there. With no
124  /// sink or no server it does nothing.
125  pub fn change_volume(&self, percent: i32) {
126    self.command(|pulse| pulse.change_volume(percent));
127  }
128
129  /// Mutes the default sink, or unmutes it. With no sink or no server it
130  /// does nothing.
131  pub fn toggle_mute(&self) {
132    self.command(Pulse::toggle_mute);
133  }
134
135  /// Every sink, in the server's order. Empty while there is no server.
136  pub fn sinks(&self) -> ReadSignal<Vec<AudioNode>> {
137    self.lists.sinks
138  }
139
140  /// Every source except sinks' monitors, in the server's order.
141  pub fn sources(&self) -> ReadSignal<Vec<AudioNode>> {
142    self.lists.sources
143  }
144
145  /// The default source, where a call's microphone sound comes from.
146  pub fn default_source(&self) -> ReadSignal<Option<AudioNode>> {
147    self.lists.default_source
148  }
149
150  /// Every app playing sound, in the server's order.
151  pub fn streams(&self) -> ReadSignal<Vec<AudioStream>> {
152    self.lists.streams
153  }
154
155  /// Makes the sink called `name` the default.
156  pub fn set_default_sink(&self, name: &str) {
157    let name = name.to_owned();
158    self.command(move |pulse| pulse.set_default(Kind::Sink, &name));
159  }
160
161  /// Makes the source called `name` the default.
162  pub fn set_default_source(&self, name: &str) {
163    let name = name.to_owned();
164    self.command(move |pulse| pulse.set_default(Kind::Source, &name));
165  }
166
167  /// Sets every channel of the sink called `name` to `percent`, at most
168  /// 100.
169  pub fn set_sink_volume(&self, name: &str, percent: u32) {
170    let name = name.to_owned();
171    self.command(move |pulse| pulse.set_node_volume(Kind::Sink, &name, percent));
172  }
173
174  /// Mutes the sink called `name`, or unmutes it.
175  pub fn set_sink_mute(&self, name: &str, muted: bool) {
176    let name = name.to_owned();
177    self.command(move |pulse| pulse.set_node_mute(Kind::Sink, &name, muted));
178  }
179
180  /// Sets every channel of the source called `name` to `percent`, at most
181  /// 100.
182  pub fn set_source_volume(&self, name: &str, percent: u32) {
183    let name = name.to_owned();
184    self.command(move |pulse| pulse.set_node_volume(Kind::Source, &name, percent));
185  }
186
187  /// Mutes the source called `name`, or unmutes it, as a microphone
188  /// button does.
189  pub fn set_source_mute(&self, name: &str, muted: bool) {
190    let name = name.to_owned();
191    self.command(move |pulse| pulse.set_node_mute(Kind::Source, &name, muted));
192  }
193
194  /// Sets every channel of the stream numbered `index` to `percent`, at
195  /// most 100.
196  pub fn set_stream_volume(&self, index: u32, percent: u32) {
197    self.command(move |pulse| pulse.set_stream_volume(index, percent));
198  }
199
200  /// Mutes the stream numbered `index`, or unmutes it.
201  pub fn set_stream_mute(&self, index: u32, muted: bool) {
202    self.command(move |pulse| pulse.set_stream_mute(index, muted));
203  }
204
205  /// Moves the stream numbered `index` to play to the sink called `sink`.
206  pub fn move_stream(&self, index: u32, sink: &str) {
207    let sink = sink.to_owned();
208    self.command(move |pulse| pulse.move_stream(index, &sink));
209  }
210
211  fn command(&self, command: impl FnOnce(&mut Pulse)) {
212    let mut pulse = self
213      .service
214      .as_ref()
215      .map(|service| service.pulse.borrow_mut());
216    match pulse.as_deref_mut().and_then(Option::as_mut) {
217      Some(pulse) => command(pulse),
218      None => tracing::debug!("no audio service: nothing to change"),
219    }
220  }
221}
222
223/// `cx.audio()`.
224pub(crate) fn audio(cx: &Cx) -> Audio {
225  if let Some(service) = cx.service::<AudioService>() {
226    return service.audio();
227  }
228  static LOGGED: Once = Once::new();
229  LOGGED.call_once(|| {
230    tracing::warn!("cx.audio() needs the services the shell provides; there is no sink");
231  });
232  Audio {
233    sink: cx.signal(None).read_only(),
234    lists: Lists {
235      sinks: cx.signal(Vec::new()).read_only(),
236      sources: cx.signal(Vec::new()).read_only(),
237      streams: cx.signal(Vec::new()).read_only(),
238      default_source: cx.signal(None).read_only(),
239    },
240    service: None,
241  }
242}
243
244/// The lists the service shows, as signals to set or read.
245#[derive(Clone, Copy)]
246struct Lists<N, S, D> {
247  sinks: N,
248  sources: N,
249  streams: S,
250  default_source: D,
251}
252
253type ReadLists =
254  Lists<ReadSignal<Vec<AudioNode>>, ReadSignal<Vec<AudioStream>>, ReadSignal<Option<AudioNode>>>;
255
256type ListSignals =
257  Lists<Signal<Vec<AudioNode>>, Signal<Vec<AudioStream>>, Signal<Option<AudioNode>>>;
258
259impl ListSignals {
260  fn read_only(&self) -> ReadLists {
261    Lists {
262      sinks: self.sinks.read_only(),
263      sources: self.sources.read_only(),
264      streams: self.streams.read_only(),
265      default_source: self.default_source.read_only(),
266    }
267  }
268}
269
270/// A sink or a source, for the setters both have.
271#[derive(Clone, Copy, Debug)]
272enum Kind {
273  Sink,
274  Source,
275}
276
277/// The audio service of one shell. It connects on first use.
278pub(crate) struct AudioService {
279  shared: Rc<Shared>,
280  /// The default sink, under the root owner. Made on first use.
281  sink: OnceCell<Signal<Option<Sink>>>,
282  /// The sinks, sources and streams, made with `sink`.
283  lists: OnceCell<ListSignals>,
284  /// libpulse, from first use on. `None` before that, and if libpulse
285  /// couldn't start its thread.
286  pulse: RefCell<Option<Pulse>>,
287}
288
289/// What libpulse's thread sends: the generation of the context it came
290/// from, and what happened.
291pub(crate) type Message = (u32, Event);
292
293pub(crate) enum Event {
294  /// The context's state changed.
295  State,
296  /// The server changed, maybe its default sink.
297  Server,
298  /// The sink with this index changed.
299  Sink(u32),
300  /// The server's default sink name.
301  Default(Option<String>),
302  /// The server's default source name.
303  DefaultSource(Option<String>),
304  /// A source changed.
305  Source,
306  /// A stream changed.
307  Stream,
308  /// Every sink, read.
309  Sinks(Vec<NodeInfo>),
310  /// Every source but monitors, read.
311  Sources(Vec<NodeInfo>),
312  /// Every stream, read.
313  Streams(Vec<StreamInfo>),
314  /// The sink asked for by name, or `None` for no such sink.
315  Info(Option<Info>),
316}
317
318impl AudioService {
319  pub(crate) fn new(shared: Rc<Shared>) -> AudioService {
320    AudioService {
321      shared,
322      sink: OnceCell::new(),
323      lists: OnceCell::new(),
324      pulse: RefCell::new(None),
325    }
326  }
327
328  fn audio(self: &Rc<Self>) -> Audio {
329    let sink = *self.sink.get_or_init(|| {
330      // On the loop's next turn, so `cx.audio()` never waits on libpulse.
331      self.after(Duration::ZERO, AudioService::start);
332      self.shared.root.signal(None)
333    });
334    let lists = *self.lists.get_or_init(|| {
335      let root = &self.shared.root;
336      Lists {
337        sinks: root.signal(Vec::new()),
338        sources: root.signal(Vec::new()),
339        streams: root.signal(Vec::new()),
340        default_source: root.signal(None),
341      }
342    });
343    Audio {
344      sink: sink.read_only(),
345      lists: lists.read_only(),
346      service: Some(Rc::clone(self)),
347    }
348  }
349
350  /// Starts libpulse's thread and the first connection. Neither waits
351  /// for the server.
352  fn start(self: &Rc<Self>) {
353    let Some(server) = &self.shared.config.audio else {
354      return tracing::info!("no audio server is set, so there is no sink");
355    };
356    let sink = *self
357      .sink
358      .get()
359      .expect("the sink is made before libpulse starts");
360    let lists = *self
361      .lists
362      .get()
363      .expect("the lists are made before libpulse starts");
364    let Some(event_loop) = self.shared.event_loop() else {
365      return;
366    };
367    let Some(mut mainloop) = Mainloop::new() else {
368      tracing::error!("libpulse couldn't make its mainloop: no audio");
369      return;
370    };
371    if let Err(error) = mainloop.start() {
372      tracing::error!(%error, "libpulse's thread didn't start: no audio");
373      return;
374    }
375    let (sender, channel) = calloop::channel::channel();
376    // From here on, dropping `pulse` stops libpulse's thread.
377    let pulse = Pulse {
378      intro: None,
379      context: None,
380      mainloop,
381      sender: Arc::new(sender),
382      generation: 0,
383      shown: None,
384      asked: None,
385      sink,
386      lists,
387      nodes: Nodes::default(),
388      down: false,
389      retry_until: Instant::now() + server.retry_for,
390    };
391    let service = Rc::clone(self);
392    let on_message = Box::new(move |message| service.on_message(message));
393    if event_loop.insert_audio_channel(channel, on_message) {
394      *self.pulse.borrow_mut() = Some(pulse);
395      self.connect();
396    }
397  }
398
399  /// Connects to the server, or tries again later.
400  fn connect(self: &Rc<Self>) {
401    let mut pulse = self.pulse.borrow_mut();
402    let (Some(pulse), Some(server)) = (pulse.as_mut(), &self.shared.config.audio) else {
403      return;
404    };
405    // The root is gone, and the shell with it.
406    if pulse.sink.try_with(|_| ()).is_none() {
407      return;
408    }
409    if let Err(why) = pulse.connect(server.address.as_deref()) {
410      self.lost(pulse, &why);
411    }
412  }
413
414  fn on_message(self: &Rc<Self>, (generation, event): Message) {
415    let mut pulse = self.pulse.borrow_mut();
416    // A message from a context that is gone is stale.
417    let Some(pulse) = pulse
418      .as_mut()
419      .filter(|pulse| pulse.generation == generation)
420    else {
421      return;
422    };
423    match event {
424      Event::State => {
425        if let Err(why) = pulse.on_state() {
426          self.lost(pulse, &why);
427        }
428      }
429      Event::Server => {
430        pulse.read_default();
431        pulse.read_lists();
432      }
433      Event::Sink(index) => {
434        if pulse.shown.as_ref().is_some_and(|s| s.index == index) {
435          pulse.read_default();
436        }
437        pulse.read_sinks();
438      }
439      Event::Default(name) => pulse.read_sink(name),
440      Event::Info(info) => pulse.show(info),
441      Event::DefaultSource(name) => {
442        pulse.nodes.default_source = name;
443        pulse.show_lists();
444      }
445      Event::Source => pulse.read_sources(),
446      Event::Stream => pulse.read_streams(),
447      Event::Sinks(sinks) => {
448        pulse.nodes.sinks = sinks;
449        pulse.show_lists();
450      }
451      Event::Sources(sources) => {
452        pulse.nodes.sources = sources;
453        pulse.show_lists();
454      }
455      Event::Streams(streams) => {
456        pulse.nodes.streams = streams;
457        pulse.show_lists();
458      }
459    }
460  }
461
462  /// Drops the connection, shows no sink and tries again every `retry`
463  /// until `retry_for` has passed since startup, or since a working
464  /// connection dropped. Logs once per outage, not on every try, and
465  /// once on giving up.
466  fn lost(self: &Rc<Self>, pulse: &mut Pulse, why: &str) {
467    let Some(Server {
468      retry, retry_for, ..
469    }) = self.shared.config.audio
470    else {
471      return;
472    };
473    let now = Instant::now();
474    // It was ready, so the server was there: give it a new while.
475    if pulse.intro.is_some() {
476      pulse.retry_until = now + retry_for;
477    }
478    if pulse.disconnect() {
479      tracing::warn!(
480        reason = why,
481        "no audio server, trying again every {retry:?} for up to {retry_for:?}"
482      );
483    }
484    if now + retry > pulse.retry_until {
485      // No timer from here on, so audio never wakes the shell again.
486      return tracing::info!(
487        "stopped trying to reach the audio server; it is tried again when the shell restarts"
488      );
489    }
490    self.after(retry, AudioService::connect);
491  }
492
493  /// Runs `f` from the event loop once, `delay` from now.
494  fn after(self: &Rc<Self>, delay: Duration, f: fn(&Rc<AudioService>)) {
495    let Some(event_loop) = self.shared.event_loop() else {
496      return;
497    };
498    let service = Rc::clone(self);
499    let once = move || {
500      f(&service);
501      TimeoutAction::Drop
502    };
503    event_loop.insert_timer(Instant::now() + delay, Box::new(once));
504  }
505}
506
507/// One shell's libpulse objects and what they last told us. It stays on
508/// the main thread: `Mainloop` holds an `Rc`, so the compiler won't let it
509/// go to another.
510struct Pulse {
511  // Declared before `mainloop`, so they drop before it.
512  intro: Option<Introspector>,
513  context: Option<Context>,
514  mainloop: Mainloop,
515  /// Shared by every callback. Each `Sender` wakes the loop when it
516  /// drops, so a callback that holds its own would wake the shell when
517  /// libpulse drops it, with nothing to read.
518  sender: Arc<Sender<Message>>,
519  /// Bumped when a context goes, so the messages it left behind are
520  /// dropped.
521  generation: u32,
522  /// The default sink as last read.
523  shown: Option<Info>,
524  /// The default sink as the last command left it, and when that was
525  /// sent. pipewire-pulse answers a read sent right after a set with the
526  /// sink as it was before, so commands build on this instead until a
527  /// read echoes it or [`SETTLE`] passes.
528  asked: Option<(Info, Instant)>,
529  sink: Signal<Option<Sink>>,
530  lists: ListSignals,
531  /// Every sink, source and stream as last read.
532  nodes: Nodes,
533  /// Whether the server is gone and that was logged.
534  down: bool,
535  /// When the service stops trying to connect, if it can't.
536  retry_until: Instant,
537}
538
539/// Every sink, source and stream as libpulse last reported them.
540#[derive(Default)]
541struct Nodes {
542  sinks: Vec<NodeInfo>,
543  sources: Vec<NodeInfo>,
544  streams: Vec<StreamInfo>,
545  default_source: Option<String>,
546}
547
548/// A sink or source as libpulse reported it.
549#[derive(Clone, PartialEq)]
550pub(crate) struct NodeInfo {
551  index: u32,
552  name: String,
553  description: String,
554  volumes: ChannelVolumes,
555  muted: bool,
556  /// A sink's monitor: a source, but not one listed.
557  monitor: bool,
558}
559
560impl NodeInfo {
561  fn node(&self) -> AudioNode {
562    AudioNode {
563      name: self.name.clone(),
564      description: self.description.clone(),
565      volume_percent: percent(self.volumes.avg()),
566      muted: self.muted,
567    }
568  }
569}
570
571/// A sink input as libpulse reported it.
572#[derive(Clone, PartialEq)]
573pub(crate) struct StreamInfo {
574  index: u32,
575  app_name: String,
576  title: String,
577  icon_name: String,
578  sink: u32,
579  volumes: ChannelVolumes,
580  muted: bool,
581}
582
583impl StreamInfo {
584  fn read(info: &SinkInputInfo) -> StreamInfo {
585    use libpulse_binding::proplist::properties::{APPLICATION_ICON_NAME, APPLICATION_NAME};
586
587    let property = |key| info.proplist.get_str(key).unwrap_or_default();
588    StreamInfo {
589      index: info.index,
590      app_name: property(APPLICATION_NAME),
591      title: info.name.as_deref().unwrap_or_default().to_owned(),
592      icon_name: property(APPLICATION_ICON_NAME),
593      sink: info.sink,
594      volumes: info.volume,
595      muted: info.mute,
596    }
597  }
598}
599
600/// The default sink as libpulse reported it.
601#[derive(Clone, PartialEq)]
602pub(crate) struct Info {
603  index: u32,
604  name: String,
605  volumes: ChannelVolumes,
606  muted: bool,
607}
608
609impl Pulse {
610  /// Makes a context and starts connecting it to `server`, or to
611  /// libpulse's default for `None`.
612  fn connect(&mut self, server: Option<&str>) -> Result<(), String> {
613    let lock = Locked::new(&mut self.mainloop);
614    let mut context = Context::new(&*lock.0, "crabshell").ok_or("libpulse made no context")?;
615    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
616    context.set_state_callback(Some(Box::new(on_pa_thread(move || {
617      let _ = sender.send((generation, Event::State));
618    }))));
619    // NOAUTOSPAWN: a retry must never start a daemon next to pipewire-pulse.
620    let connected = context.connect(server, FlagSet::NOAUTOSPAWN, None);
621    self.context = Some(context);
622    connected.map_err(|error| format!("{error}"))
623  }
624
625  /// Acts on the context's new state. Once it is ready, subscribes to
626  /// sink and server events and reads the default sink. `Err` when the
627  /// connection failed or ended.
628  fn on_state(&mut self) -> Result<(), String> {
629    let lock = Locked::new(&mut self.mainloop);
630    let Some(context) = self.context.as_mut() else {
631      return Ok(());
632    };
633    match context.get_state() {
634      // Several queued messages can each find it ready.
635      State::Ready if self.intro.is_none() => {
636        let (sender, generation) = (Arc::clone(&self.sender), self.generation);
637        let on_event = move |facility, _, index| {
638          let event = match facility {
639            Some(Facility::Server) => Event::Server,
640            Some(Facility::Sink) => Event::Sink(index),
641            Some(Facility::Source) => Event::Source,
642            Some(Facility::SinkInput) => Event::Stream,
643            _ => return,
644          };
645          let _ = sender.send((generation, event));
646        };
647        context.set_subscribe_callback(Some(Box::new(on_pa_thread(on_event))));
648        let interest = InterestMaskSet::SINK
649          | InterestMaskSet::SOURCE
650          | InterestMaskSet::SINK_INPUT
651          | InterestMaskSet::SERVER;
652        context.subscribe(interest, on_pa_thread(|_| {}));
653        self.intro = Some(context.introspect());
654        drop(lock);
655        if std::mem::take(&mut self.down) {
656          tracing::info!("the audio server is back");
657        } else {
658          tracing::debug!("connected to the audio server");
659        }
660        self.read_default();
661        self.read_lists();
662        Ok(())
663      }
664      State::Failed | State::Terminated => Err(format!("{}", context.errno())),
665      _ => Ok(()),
666    }
667  }
668
669  /// Runs `call` under the lock if the context is ready, and says
670  /// whether it ran. libpulse returns no operation from a context that
671  /// isn't, and the binding panics on that.
672  fn when_ready(&mut self, call: impl FnOnce(&mut Introspector)) -> bool {
673    self.when_ready_with(|_, intro| call(intro))
674  }
675
676  /// [`when_ready`](Self::when_ready), with the context too.
677  fn when_ready_with(&mut self, call: impl FnOnce(&mut Context, &mut Introspector)) -> bool {
678    let _lock = Locked::new(&mut self.mainloop);
679    match (&mut self.context, &mut self.intro) {
680      (Some(context), Some(intro)) if context.get_state() == State::Ready => {
681        call(context, intro);
682        true
683      }
684      _ => false,
685    }
686  }
687
688  /// Asks the server for its default sink's name.
689  fn read_default(&mut self) {
690    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
691    self.when_ready(|intro| {
692      // shortcut: on an error reply libpulse-binding panics on its thread instead of calling
693      // this, and the panic hook prints it raw; logging it needs the C API, so `unsafe`.
694      intro.get_server_info(on_pa_thread(move |info: &ServerInfo| {
695        let name = |name: &Option<std::borrow::Cow<'_, str>>| {
696          let name = name.as_deref().filter(|name| !name.is_empty());
697          name.map(str::to_owned)
698        };
699        let _ = sender.send((generation, Event::Default(name(&info.default_sink_name))));
700        let source = Event::DefaultSource(name(&info.default_source_name));
701        let _ = sender.send((generation, source));
702      }));
703    });
704  }
705
706  /// Asks for the sink called `name`, or shows none.
707  fn read_sink(&mut self, name: Option<String>) {
708    let Some(name) = name else {
709      return self.show(None);
710    };
711    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
712    self.when_ready(|intro| {
713      let mut found = None;
714      let on_result = move |result: ListResult<&SinkInfo>| match result {
715        ListResult::Item(info) => found = Info::read(info),
716        ListResult::End | ListResult::Error => {
717          let _ = sender.send((generation, Event::Info(found.take())));
718        }
719      };
720      intro.get_sink_info_by_name(&name, on_pa_thread(on_result));
721    });
722  }
723
724  /// Asks for every sink, source and stream.
725  fn read_lists(&mut self) {
726    self.read_sinks();
727    self.read_sources();
728    self.read_streams();
729  }
730
731  fn read_sinks(&mut self) {
732    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
733    self.when_ready(|intro| {
734      let mut found = Vec::new();
735      let on_result = move |result: ListResult<&SinkInfo>| match result {
736        ListResult::Item(info) => {
737          let read = node(
738            info.index,
739            &info.name,
740            &info.description,
741            info.volume,
742            info.mute,
743          );
744          found.extend(read);
745        }
746        ListResult::End => {
747          let _ = sender.send((generation, Event::Sinks(std::mem::take(&mut found))));
748        }
749        // The last list stays.
750        ListResult::Error => tracing::warn!("the audio server didn't list its sinks"),
751      };
752      intro.get_sink_info_list(on_pa_thread(on_result));
753    });
754  }
755
756  fn read_sources(&mut self) {
757    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
758    self.when_ready(|intro| {
759      let mut found = Vec::new();
760      let on_result = move |result: ListResult<&SourceInfo>| match result {
761        ListResult::Item(info) => {
762          let read = node(
763            info.index,
764            &info.name,
765            &info.description,
766            info.volume,
767            info.mute,
768          );
769          let monitor = info.monitor_of_sink.is_some();
770          found.extend(read.map(|node| NodeInfo { monitor, ..node }));
771        }
772        ListResult::End => {
773          let _ = sender.send((generation, Event::Sources(std::mem::take(&mut found))));
774        }
775        ListResult::Error => tracing::warn!("the audio server didn't list its sources"),
776      };
777      intro.get_source_info_list(on_pa_thread(on_result));
778    });
779  }
780
781  fn read_streams(&mut self) {
782    let (sender, generation) = (Arc::clone(&self.sender), self.generation);
783    self.when_ready(|intro| {
784      let mut found = Vec::new();
785      let on_result = move |result: ListResult<&SinkInputInfo>| match result {
786        ListResult::Item(info) if info.volume.is_valid() => found.push(StreamInfo::read(info)),
787        ListResult::Item(_) => {}
788        ListResult::End => {
789          let _ = sender.send((generation, Event::Streams(std::mem::take(&mut found))));
790        }
791        ListResult::Error => tracing::warn!("the audio server didn't list its streams"),
792      };
793      intro.get_sink_input_info_list(on_pa_thread(on_result));
794    });
795  }
796
797  /// Shows the lists as last read, capped. Each signal is set only when
798  /// what it shows changed.
799  fn show_lists(&mut self) {
800    let Nodes {
801      sinks,
802      sources,
803      streams,
804      default_source,
805    } = &self.nodes;
806    let capped = |nodes: &[NodeInfo]| -> Vec<AudioNode> {
807      nodes.iter().take(MAX_NODES).map(NodeInfo::node).collect()
808    };
809    let sink_name = |index| {
810      let sink = sinks.iter().find(|sink| sink.index == index);
811      sink.map_or(String::new(), |sink| sink.name.clone())
812    };
813    let streams = streams
814      .iter()
815      .take(MAX_NODES)
816      .map(|stream| AudioStream {
817        index: stream.index,
818        app_name: stream.app_name.clone(),
819        title: stream.title.clone(),
820        icon_name: stream.icon_name.clone(),
821        sink: sink_name(stream.sink),
822        volume_percent: percent(stream.volumes.avg()),
823        muted: stream.muted,
824      })
825      .collect();
826    let default = default_source
827      .as_ref()
828      .and_then(|name| sources.iter().find(|source| source.name == *name))
829      .map(NodeInfo::node);
830    set_changed(self.lists.sinks, capped(sinks));
831    // A sink's monitor isn't a source a person picks, though it can be
832    // the default, as while recording what plays.
833    let listed: Vec<NodeInfo> = sources.iter().filter(|s| !s.monitor).cloned().collect();
834    set_changed(self.lists.sources, capped(&listed));
835    set_changed(self.lists.streams, streams);
836    set_changed(self.lists.default_source, default);
837  }
838
839  fn set_default(&mut self, kind: Kind, name: &str) {
840    let sent = self.when_ready_with(|context, _| {
841      let done = on_pa_thread(|_| {});
842      match kind {
843        Kind::Sink => drop(context.set_default_sink(name, done)),
844        Kind::Source => drop(context.set_default_source(name, done)),
845      }
846    });
847    if !sent {
848      tracing::debug!(name, "no audio server: the default stays");
849    }
850  }
851
852  fn set_node_volume(&mut self, kind: Kind, name: &str, percent: u32) {
853    let nodes = match kind {
854      Kind::Sink => &self.nodes.sinks,
855      Kind::Source => &self.nodes.sources,
856    };
857    let Some(node) = nodes.iter().find(|node| node.name == name) else {
858      return tracing::debug!(name, ?kind, "no such node: its volume stays");
859    };
860    let volumes = at(node.volumes, percent);
861    self.when_ready(|intro| match kind {
862      Kind::Sink => drop(intro.set_sink_volume_by_name(name, &volumes, None)),
863      Kind::Source => drop(intro.set_source_volume_by_name(name, &volumes, None)),
864    });
865  }
866
867  fn set_node_mute(&mut self, kind: Kind, name: &str, muted: bool) {
868    self.when_ready(|intro| match kind {
869      Kind::Sink => drop(intro.set_sink_mute_by_name(name, muted, None)),
870      Kind::Source => drop(intro.set_source_mute_by_name(name, muted, None)),
871    });
872  }
873
874  fn set_stream_volume(&mut self, index: u32, percent: u32) {
875    let Some(stream) = self
876      .nodes
877      .streams
878      .iter()
879      .find(|stream| stream.index == index)
880    else {
881      return tracing::debug!(index, "no such stream: its volume stays");
882    };
883    let volumes = at(stream.volumes, percent);
884    self.when_ready(|intro| drop(intro.set_sink_input_volume(index, &volumes, None)));
885  }
886
887  fn set_stream_mute(&mut self, index: u32, muted: bool) {
888    self.when_ready(|intro| drop(intro.set_sink_input_mute(index, muted, None)));
889  }
890
891  fn move_stream(&mut self, index: u32, sink: &str) {
892    self.when_ready(|intro| drop(intro.move_sink_input_by_name(index, sink, None)));
893  }
894
895  /// Shows `info`, or no sink. The signal is set only when that changes
896  /// what the bar shows, since every set repaints.
897  fn show(&mut self, info: Option<Info>) {
898    // An echo of the last command, or another sink, ends its stand-in.
899    let stands = match (&self.asked, &info) {
900      (Some((asked, _)), Some(read)) => read != asked && read.index == asked.index,
901      _ => false,
902    };
903    if !stands {
904      self.asked = None;
905    }
906    let sink = info.as_ref().map(Info::sink);
907    self.shown = info;
908    if self.sink.try_with(|shown| *shown != sink) == Some(true) {
909      self.sink.set(sink);
910    }
911  }
912
913  /// What the next command builds on: the last one's result while it
914  /// stands in, or else the last read.
915  fn base(&self) -> Option<Info> {
916    match &self.asked {
917      Some((asked, at)) if at.elapsed() < SETTLE => Some(asked.clone()),
918      _ => self.shown.clone(),
919    }
920  }
921
922  fn change_volume(&mut self, percent: i32) {
923    let Some(mut next) = self.base() else {
924      return tracing::debug!("no default sink: the volume stays");
925    };
926    next.volumes = changed(next.volumes, percent);
927    let sent = self.when_ready(|intro| {
928      intro.set_sink_volume_by_name(&next.name, &next.volumes, None);
929    });
930    if sent {
931      self.asked = Some((next, Instant::now()));
932    } else {
933      tracing::debug!("no audio server: the volume stays");
934    }
935  }
936
937  fn toggle_mute(&mut self) {
938    let Some(mut next) = self.base() else {
939      return tracing::debug!("no default sink: nothing to mute");
940    };
941    next.muted = !next.muted;
942    let sent = self.when_ready(|intro| {
943      intro.set_sink_mute_by_name(&next.name, next.muted, None);
944    });
945    if sent {
946      self.asked = Some((next, Instant::now()));
947    } else {
948      tracing::debug!("no audio server: nothing to mute");
949    }
950  }
951
952  /// Drops the context and shows no sink. `true` when this starts an
953  /// outage.
954  fn disconnect(&mut self) -> bool {
955    let lock = Locked::new(&mut self.mainloop);
956    self.intro = None;
957    if let Some(mut context) = self.context.take() {
958      // Cleared first: disconnecting calls the state callback.
959      context.set_state_callback(None);
960      context.set_subscribe_callback(None);
961      context.disconnect();
962    }
963    drop(lock);
964    self.generation += 1;
965    self.show(None);
966    self.nodes = Nodes::default();
967    self.show_lists();
968    !std::mem::replace(&mut self.down, true)
969  }
970}
971
972impl Drop for Pulse {
973  fn drop(&mut self) {
974    // Unlocked, as libpulse asks. Once its thread is gone, the context
975    // and introspector drop with nothing running beside them.
976    self.mainloop.stop();
977  }
978}
979
980impl Info {
981  /// Copies what we need out of libpulse's `info`, which lives only for
982  /// the callback. A sink with no name or a bad volume is skipped.
983  fn read(info: &SinkInfo) -> Option<Info> {
984    let (Some(name), true) = (info.name.as_deref(), info.volume.is_valid()) else {
985      tracing::warn!(
986        index = info.index,
987        "skipped a sink with no name or a bad volume"
988      );
989      return None;
990    };
991    Some(Info {
992      index: info.index,
993      name: name.to_owned(),
994      volumes: info.volume,
995      muted: info.mute,
996    })
997  }
998
999  fn sink(&self) -> Sink {
1000    Sink {
1001      name: self.name.clone(),
1002      volume_percent: percent(self.volumes.avg()),
1003      muted: self.muted,
1004    }
1005  }
1006}
1007
1008/// The mainloop's lock, held until this drops. libpulse's thread waits for
1009/// it, so it must be let go even on an early return or a panic.
1010struct Locked<'a>(&'a mut Mainloop);
1011
1012impl<'a> Locked<'a> {
1013  fn new(mainloop: &'a mut Mainloop) -> Locked<'a> {
1014    mainloop.lock();
1015    Locked(mainloop)
1016  }
1017}
1018
1019impl Drop for Locked<'_> {
1020  fn drop(&mut self) {
1021    self.0.unlock();
1022  }
1023}
1024
1025/// libpulse runs its callbacks on its own thread but doesn't ask for
1026/// `Send`. Wrapping each one in this makes the compiler check it.
1027fn on_pa_thread<F: Send + 'static>(callback: F) -> F {
1028  callback
1029}
1030
1031/// The percent `pactl get-sink-volume` shows for `volume`: 100 is
1032/// `PA_VOLUME_NORM`, and halves round up.
1033fn percent(volume: Volume) -> u32 {
1034  let norm = u64::from(Volume::NORMAL.0);
1035  // At most u32::MAX * 100 / norm, which fits.
1036  ((u64::from(volume.0) * 100 + norm / 2) / norm) as u32
1037}
1038
1039/// `volumes` changed by `delta` percent. Every channel moves by the amount
1040/// that puts their average on the shown percent plus `delta`, kept within
1041/// 0-100%. A channel that would pass 0% or 100% stops there, and then the
1042/// average falls short of that.
1043fn changed(mut volumes: ChannelVolumes, delta: i32) -> ChannelVolumes {
1044  let norm = i64::from(Volume::NORMAL.0);
1045  let average = volumes.avg();
1046  let target = (i64::from(percent(average)) + i64::from(delta)).clamp(0, 100);
1047  let shift = (target * norm + 50) / 100 - i64::from(average.0);
1048  for volume in volumes.get_mut() {
1049    // Within 0..=norm, which fits.
1050    volume.0 = (i64::from(volume.0) + shift).clamp(0, norm) as u32;
1051  }
1052  volumes
1053}
1054
1055/// A sink or source as libpulse reported it, unless it has no name or a
1056/// bad volume.
1057fn node(
1058  index: u32,
1059  name: &Option<std::borrow::Cow<'_, str>>,
1060  description: &Option<std::borrow::Cow<'_, str>>,
1061  volumes: ChannelVolumes,
1062  muted: bool,
1063) -> Option<NodeInfo> {
1064  let name = name.as_deref().filter(|_| volumes.is_valid())?;
1065  Some(NodeInfo {
1066    index,
1067    name: name.to_owned(),
1068    description: description.as_deref().unwrap_or(name).to_owned(),
1069    volumes,
1070    muted,
1071    monitor: false,
1072  })
1073}
1074
1075/// `volumes` moved so their average is `percent`, at most 100, keeping
1076/// the channels' balance as [`changed`] does.
1077fn at(volumes: ChannelVolumes, percent: u32) -> ChannelVolumes {
1078  let shown = self::percent(volumes.avg());
1079  // Both at most a few thousand, which fit.
1080  changed(volumes, percent.min(100) as i32 - shown.min(10_000) as i32)
1081}