1use 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
34pub(crate) const RETRY: Duration = Duration::from_secs(2);
36
37pub(crate) const RETRY_FOR: Duration = Duration::from_secs(60);
40
41pub(crate) const SETTLE: Duration = Duration::from_millis(250);
43
44const MAX_NODES: usize = 128;
46
47pub(crate) struct Server {
49 pub(crate) address: Option<String>,
52 pub(crate) retry: Duration,
54 pub(crate) retry_for: Duration,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Sink {
61 pub name: String,
64 pub volume_percent: u32,
67 pub muted: bool,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct AudioNode {
75 pub name: String,
77 pub description: String,
79 pub volume_percent: u32,
81 pub muted: bool,
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct AudioStream {
88 pub index: u32,
90 pub app_name: String,
92 pub title: String,
94 pub icon_name: String,
96 pub sink: String,
98 pub volume_percent: u32,
100 pub muted: bool,
102}
103
104#[derive(Clone)]
106pub struct Audio {
107 sink: ReadSignal<Option<Sink>>,
108 lists: ReadLists,
109 service: Option<Rc<AudioService>>,
111}
112
113impl Audio {
114 pub fn default_sink(&self) -> ReadSignal<Option<Sink>> {
117 self.sink
118 }
119
120 pub fn change_volume(&self, percent: i32) {
126 self.command(|pulse| pulse.change_volume(percent));
127 }
128
129 pub fn toggle_mute(&self) {
132 self.command(Pulse::toggle_mute);
133 }
134
135 pub fn sinks(&self) -> ReadSignal<Vec<AudioNode>> {
137 self.lists.sinks
138 }
139
140 pub fn sources(&self) -> ReadSignal<Vec<AudioNode>> {
142 self.lists.sources
143 }
144
145 pub fn default_source(&self) -> ReadSignal<Option<AudioNode>> {
147 self.lists.default_source
148 }
149
150 pub fn streams(&self) -> ReadSignal<Vec<AudioStream>> {
152 self.lists.streams
153 }
154
155 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 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 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 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 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 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 pub fn set_stream_volume(&self, index: u32, percent: u32) {
197 self.command(move |pulse| pulse.set_stream_volume(index, percent));
198 }
199
200 pub fn set_stream_mute(&self, index: u32, muted: bool) {
202 self.command(move |pulse| pulse.set_stream_mute(index, muted));
203 }
204
205 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
223pub(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#[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#[derive(Clone, Copy, Debug)]
272enum Kind {
273 Sink,
274 Source,
275}
276
277pub(crate) struct AudioService {
279 shared: Rc<Shared>,
280 sink: OnceCell<Signal<Option<Sink>>>,
282 lists: OnceCell<ListSignals>,
284 pulse: RefCell<Option<Pulse>>,
287}
288
289pub(crate) type Message = (u32, Event);
292
293pub(crate) enum Event {
294 State,
296 Server,
298 Sink(u32),
300 Default(Option<String>),
302 DefaultSource(Option<String>),
304 Source,
306 Stream,
308 Sinks(Vec<NodeInfo>),
310 Sources(Vec<NodeInfo>),
312 Streams(Vec<StreamInfo>),
314 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 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 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 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 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 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 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 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 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 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 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
507struct Pulse {
511 intro: Option<Introspector>,
513 context: Option<Context>,
514 mainloop: Mainloop,
515 sender: Arc<Sender<Message>>,
519 generation: u32,
522 shown: Option<Info>,
524 asked: Option<(Info, Instant)>,
529 sink: Signal<Option<Sink>>,
530 lists: ListSignals,
531 nodes: Nodes,
533 down: bool,
535 retry_until: Instant,
537}
538
539#[derive(Default)]
541struct Nodes {
542 sinks: Vec<NodeInfo>,
543 sources: Vec<NodeInfo>,
544 streams: Vec<StreamInfo>,
545 default_source: Option<String>,
546}
547
548#[derive(Clone, PartialEq)]
550pub(crate) struct NodeInfo {
551 index: u32,
552 name: String,
553 description: String,
554 volumes: ChannelVolumes,
555 muted: bool,
556 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#[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#[derive(Clone, PartialEq)]
602pub(crate) struct Info {
603 index: u32,
604 name: String,
605 volumes: ChannelVolumes,
606 muted: bool,
607}
608
609impl Pulse {
610 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 let connected = context.connect(server, FlagSet::NOAUTOSPAWN, None);
621 self.context = Some(context);
622 connected.map_err(|error| format!("{error}"))
623 }
624
625 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 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 fn when_ready(&mut self, call: impl FnOnce(&mut Introspector)) -> bool {
673 self.when_ready_with(|_, intro| call(intro))
674 }
675
676 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 fn read_default(&mut self) {
690 let (sender, generation) = (Arc::clone(&self.sender), self.generation);
691 self.when_ready(|intro| {
692 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 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 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 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 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 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 fn show(&mut self, info: Option<Info>) {
898 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 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 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 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 self.mainloop.stop();
977 }
978}
979
980impl Info {
981 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
1008struct 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
1025fn on_pa_thread<F: Send + 'static>(callback: F) -> F {
1028 callback
1029}
1030
1031fn percent(volume: Volume) -> u32 {
1034 let norm = u64::from(Volume::NORMAL.0);
1035 ((u64::from(volume.0) * 100 + norm / 2) / norm) as u32
1037}
1038
1039fn 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 volume.0 = (i64::from(volume.0) + shift).clamp(0, norm) as u32;
1051 }
1052 volumes
1053}
1054
1055fn 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
1075fn at(volumes: ChannelVolumes, percent: u32) -> ChannelVolumes {
1078 let shown = self::percent(volumes.avg());
1079 changed(volumes, percent.min(100) as i32 - shown.min(10_000) as i32)
1081}