use std::cell::{OnceCell, RefCell};
use std::rc::Rc;
use std::sync::{Arc, Once};
use std::time::{Duration, Instant};
use calloop::channel::Sender;
use calloop::timer::TimeoutAction;
use crabshell_reactive::{ReadSignal, Signal};
use crabshell_ui::Cx;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::{Introspector, ServerInfo, SinkInfo};
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet};
use libpulse_binding::context::{Context, FlagSet, State};
use libpulse_binding::mainloop::threaded::Mainloop;
use libpulse_binding::volume::{ChannelVolumes, Volume};
use crate::Shared;
#[cfg(test)]
mod tests;
pub(crate) const RETRY: Duration = Duration::from_secs(2);
pub(crate) const RETRY_FOR: Duration = Duration::from_secs(60);
pub(crate) const SETTLE: Duration = Duration::from_millis(250);
pub(crate) struct Server {
pub(crate) address: Option<String>,
pub(crate) retry: Duration,
pub(crate) retry_for: Duration,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sink {
pub name: String,
pub volume_percent: u32,
pub muted: bool,
}
#[derive(Clone)]
pub struct Audio {
sink: ReadSignal<Option<Sink>>,
service: Option<Rc<AudioService>>,
}
impl Audio {
pub fn default_sink(&self) -> ReadSignal<Option<Sink>> {
self.sink
}
pub fn change_volume(&self, percent: i32) {
self.command(|pulse| pulse.change_volume(percent));
}
pub fn toggle_mute(&self) {
self.command(Pulse::toggle_mute);
}
fn command(&self, command: impl FnOnce(&mut Pulse)) {
let mut pulse = self
.service
.as_ref()
.map(|service| service.pulse.borrow_mut());
match pulse.as_deref_mut().and_then(Option::as_mut) {
Some(pulse) => command(pulse),
None => tracing::debug!("no audio service: nothing to change"),
}
}
}
pub(crate) fn audio(cx: &Cx) -> Audio {
if let Some(service) = cx.service::<AudioService>() {
return service.audio();
}
static LOGGED: Once = Once::new();
LOGGED.call_once(|| {
tracing::warn!("cx.audio() needs the services the shell provides; there is no sink");
});
Audio {
sink: cx.signal(None).read_only(),
service: None,
}
}
pub(crate) struct AudioService {
shared: Rc<Shared>,
sink: OnceCell<Signal<Option<Sink>>>,
pulse: RefCell<Option<Pulse>>,
}
pub(crate) type Message = (u32, Event);
pub(crate) enum Event {
State,
Server,
Sink(u32),
Default(Option<String>),
Info(Option<Info>),
}
impl AudioService {
pub(crate) fn new(shared: Rc<Shared>) -> AudioService {
AudioService {
shared,
sink: OnceCell::new(),
pulse: RefCell::new(None),
}
}
fn audio(self: &Rc<Self>) -> Audio {
let sink = *self.sink.get_or_init(|| {
self.after(Duration::ZERO, AudioService::start);
self.shared.root.signal(None)
});
Audio {
sink: sink.read_only(),
service: Some(Rc::clone(self)),
}
}
fn start(self: &Rc<Self>) {
let Some(server) = &self.shared.config.audio else {
return tracing::info!("no audio server is set, so there is no sink");
};
let sink = *self
.sink
.get()
.expect("the sink is made before libpulse starts");
let Some(event_loop) = self.shared.event_loop() else {
return;
};
let Some(mut mainloop) = Mainloop::new() else {
tracing::error!("libpulse couldn't make its mainloop: no audio");
return;
};
if let Err(error) = mainloop.start() {
tracing::error!(%error, "libpulse's thread didn't start: no audio");
return;
}
let (sender, channel) = calloop::channel::channel();
let pulse = Pulse {
intro: None,
context: None,
mainloop,
sender: Arc::new(sender),
generation: 0,
shown: None,
asked: None,
sink,
down: false,
retry_until: Instant::now() + server.retry_for,
};
let service = Rc::clone(self);
let on_message = Box::new(move |message| service.on_message(message));
if event_loop.insert_audio_channel(channel, on_message) {
*self.pulse.borrow_mut() = Some(pulse);
self.connect();
}
}
fn connect(self: &Rc<Self>) {
let mut pulse = self.pulse.borrow_mut();
let (Some(pulse), Some(server)) = (pulse.as_mut(), &self.shared.config.audio) else {
return;
};
if pulse.sink.try_with(|_| ()).is_none() {
return;
}
if let Err(why) = pulse.connect(server.address.as_deref()) {
self.lost(pulse, &why);
}
}
fn on_message(self: &Rc<Self>, (generation, event): Message) {
let mut pulse = self.pulse.borrow_mut();
let Some(pulse) = pulse
.as_mut()
.filter(|pulse| pulse.generation == generation)
else {
return;
};
match event {
Event::State => {
if let Err(why) = pulse.on_state() {
self.lost(pulse, &why);
}
}
Event::Server => pulse.read_default(),
Event::Sink(index) if pulse.shown.as_ref().is_some_and(|s| s.index == index) => {
pulse.read_default();
}
Event::Sink(_) => {}
Event::Default(name) => pulse.read_sink(name),
Event::Info(info) => pulse.show(info),
}
}
fn lost(self: &Rc<Self>, pulse: &mut Pulse, why: &str) {
let Some(Server {
retry, retry_for, ..
}) = self.shared.config.audio
else {
return;
};
let now = Instant::now();
if pulse.intro.is_some() {
pulse.retry_until = now + retry_for;
}
if pulse.disconnect() {
tracing::warn!(
reason = why,
"no audio server, trying again every {retry:?} for up to {retry_for:?}"
);
}
if now + retry > pulse.retry_until {
return tracing::info!(
"stopped trying to reach the audio server; it is tried again when the shell restarts"
);
}
self.after(retry, AudioService::connect);
}
fn after(self: &Rc<Self>, delay: Duration, f: fn(&Rc<AudioService>)) {
let Some(event_loop) = self.shared.event_loop() else {
return;
};
let service = Rc::clone(self);
let once = move || {
f(&service);
TimeoutAction::Drop
};
event_loop.insert_timer(Instant::now() + delay, Box::new(once));
}
}
struct Pulse {
intro: Option<Introspector>,
context: Option<Context>,
mainloop: Mainloop,
sender: Arc<Sender<Message>>,
generation: u32,
shown: Option<Info>,
asked: Option<(Info, Instant)>,
sink: Signal<Option<Sink>>,
down: bool,
retry_until: Instant,
}
#[derive(Clone, PartialEq)]
pub(crate) struct Info {
index: u32,
name: String,
volumes: ChannelVolumes,
muted: bool,
}
impl Pulse {
fn connect(&mut self, server: Option<&str>) -> Result<(), String> {
let lock = Locked::new(&mut self.mainloop);
let mut context = Context::new(&*lock.0, "crabshell").ok_or("libpulse made no context")?;
let (sender, generation) = (Arc::clone(&self.sender), self.generation);
context.set_state_callback(Some(Box::new(on_pa_thread(move || {
let _ = sender.send((generation, Event::State));
}))));
let connected = context.connect(server, FlagSet::NOAUTOSPAWN, None);
self.context = Some(context);
connected.map_err(|error| format!("{error}"))
}
fn on_state(&mut self) -> Result<(), String> {
let lock = Locked::new(&mut self.mainloop);
let Some(context) = self.context.as_mut() else {
return Ok(());
};
match context.get_state() {
State::Ready if self.intro.is_none() => {
let (sender, generation) = (Arc::clone(&self.sender), self.generation);
let on_event = move |facility, _, index| {
let event = match facility {
Some(Facility::Server) => Event::Server,
Some(Facility::Sink) => Event::Sink(index),
_ => return,
};
let _ = sender.send((generation, event));
};
context.set_subscribe_callback(Some(Box::new(on_pa_thread(on_event))));
let interest = InterestMaskSet::SINK | InterestMaskSet::SERVER;
context.subscribe(interest, on_pa_thread(|_| {}));
self.intro = Some(context.introspect());
drop(lock);
if std::mem::take(&mut self.down) {
tracing::info!("the audio server is back");
} else {
tracing::debug!("connected to the audio server");
}
self.read_default();
Ok(())
}
State::Failed | State::Terminated => Err(format!("{}", context.errno())),
_ => Ok(()),
}
}
fn when_ready(&mut self, call: impl FnOnce(&mut Introspector)) -> bool {
let _lock = Locked::new(&mut self.mainloop);
match (&self.context, &mut self.intro) {
(Some(context), Some(intro)) if context.get_state() == State::Ready => {
call(intro);
true
}
_ => false,
}
}
fn read_default(&mut self) {
let (sender, generation) = (Arc::clone(&self.sender), self.generation);
self.when_ready(|intro| {
intro.get_server_info(on_pa_thread(move |info: &ServerInfo| {
let name = info
.default_sink_name
.as_deref()
.filter(|name| !name.is_empty());
let _ = sender.send((generation, Event::Default(name.map(str::to_owned))));
}));
});
}
fn read_sink(&mut self, name: Option<String>) {
let Some(name) = name else {
return self.show(None);
};
let (sender, generation) = (Arc::clone(&self.sender), self.generation);
self.when_ready(|intro| {
let mut found = None;
let on_result = move |result: ListResult<&SinkInfo>| match result {
ListResult::Item(info) => found = Info::read(info),
ListResult::End | ListResult::Error => {
let _ = sender.send((generation, Event::Info(found.take())));
}
};
intro.get_sink_info_by_name(&name, on_pa_thread(on_result));
});
}
fn show(&mut self, info: Option<Info>) {
let stands = match (&self.asked, &info) {
(Some((asked, _)), Some(read)) => read != asked && read.index == asked.index,
_ => false,
};
if !stands {
self.asked = None;
}
let sink = info.as_ref().map(Info::sink);
self.shown = info;
if self.sink.try_with(|shown| *shown != sink) == Some(true) {
self.sink.set(sink);
}
}
fn base(&self) -> Option<Info> {
match &self.asked {
Some((asked, at)) if at.elapsed() < SETTLE => Some(asked.clone()),
_ => self.shown.clone(),
}
}
fn change_volume(&mut self, percent: i32) {
let Some(mut next) = self.base() else {
return tracing::debug!("no default sink: the volume stays");
};
next.volumes = changed(next.volumes, percent);
let sent = self.when_ready(|intro| {
intro.set_sink_volume_by_name(&next.name, &next.volumes, None);
});
if sent {
self.asked = Some((next, Instant::now()));
} else {
tracing::debug!("no audio server: the volume stays");
}
}
fn toggle_mute(&mut self) {
let Some(mut next) = self.base() else {
return tracing::debug!("no default sink: nothing to mute");
};
next.muted = !next.muted;
let sent = self.when_ready(|intro| {
intro.set_sink_mute_by_name(&next.name, next.muted, None);
});
if sent {
self.asked = Some((next, Instant::now()));
} else {
tracing::debug!("no audio server: nothing to mute");
}
}
fn disconnect(&mut self) -> bool {
let lock = Locked::new(&mut self.mainloop);
self.intro = None;
if let Some(mut context) = self.context.take() {
context.set_state_callback(None);
context.set_subscribe_callback(None);
context.disconnect();
}
drop(lock);
self.generation += 1;
self.show(None);
!std::mem::replace(&mut self.down, true)
}
}
impl Drop for Pulse {
fn drop(&mut self) {
self.mainloop.stop();
}
}
impl Info {
fn read(info: &SinkInfo) -> Option<Info> {
let (Some(name), true) = (info.name.as_deref(), info.volume.is_valid()) else {
tracing::warn!(
index = info.index,
"skipped a sink with no name or a bad volume"
);
return None;
};
Some(Info {
index: info.index,
name: name.to_owned(),
volumes: info.volume,
muted: info.mute,
})
}
fn sink(&self) -> Sink {
Sink {
name: self.name.clone(),
volume_percent: percent(self.volumes.avg()),
muted: self.muted,
}
}
}
struct Locked<'a>(&'a mut Mainloop);
impl<'a> Locked<'a> {
fn new(mainloop: &'a mut Mainloop) -> Locked<'a> {
mainloop.lock();
Locked(mainloop)
}
}
impl Drop for Locked<'_> {
fn drop(&mut self) {
self.0.unlock();
}
}
fn on_pa_thread<F: Send + 'static>(callback: F) -> F {
callback
}
fn percent(volume: Volume) -> u32 {
let norm = u64::from(Volume::NORMAL.0);
((u64::from(volume.0) * 100 + norm / 2) / norm) as u32
}
fn changed(mut volumes: ChannelVolumes, delta: i32) -> ChannelVolumes {
let norm = i64::from(Volume::NORMAL.0);
let average = volumes.avg();
let target = (i64::from(percent(average)) + i64::from(delta)).clamp(0, 100);
let shift = (target * norm + 50) / 100 - i64::from(average.0);
for volume in volumes.get_mut() {
volume.0 = (i64::from(volume.0) + shift).clamp(0, norm) as u32;
}
volumes
}