fix: drop audio stream when idle to allow system suspend (#54)

* fix: drop audio stream when idle to allow system suspend

The daemon kept its ALSA playback stream open permanently, which
PipeWire reported as a running Stream/Output/Audio node even with
no tracks playing. This prevented desktop environments from detecting
idle state and entering suspend.

- Make the audio sink on-demand: created when playback starts,
  dropped when all tracks finish
- Reduce player loop polling from 100ms to 2s when idle
- Throttle PipeWire device enumeration to every ~5s while playing
- Log only first and last link retry attempt instead of all 60
This commit is contained in:
RiDDiX
2026-04-11 23:42:10 +02:00
committed by GitHub
parent 5a2418325d
commit cb56cb3a04
2 changed files with 65 additions and 29 deletions
+20 -10
View File
@@ -40,7 +40,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
println!("Successfully linked player to virtual mic.");
break;
}
Err(e) => println!("{e}\t{i}/{max_retries}"),
Err(e) => {
if i == 0 || i == max_retries {
eprintln!("{e} (attempt {i}/{max_retries})");
}
}
}
sleep(Duration::from_millis(1000)).await;
@@ -174,19 +178,25 @@ async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> {
}
async fn player_loop() {
let mut device_check_counter: u32 = 0;
loop {
match get_audio_player().await {
let is_idle = match get_audio_player().await {
Ok(player_mutex) => {
let mut audio_player = player_mutex.lock().await;
audio_player.update().await;
let check_devices = device_check_counter == 0;
audio_player.update(check_devices).await;
audio_player.tracks.is_empty()
}
Err(_err) => {
// To avoid spamming logs every 100ms when audio player fails to init
// we can just sleep, or you might prefer to print the error.
// Assuming it failed to initialize, no player update is possible.
}
}
Err(_err) => true,
};
sleep(Duration::from_millis(100)).await;
if is_idle {
device_check_counter = 0;
sleep(Duration::from_secs(2)).await;
} else {
// Check devices every ~5 seconds (50 * 100ms) while playing
device_check_counter = (device_check_counter + 1) % 50;
sleep(Duration::from_millis(100)).await;
}
}
}
+45 -19
View File
@@ -53,7 +53,7 @@ pub struct PlayingSound {
}
pub struct AudioPlayer {
pub stream_handle: MixerDeviceSink,
stream_handle: Option<MixerDeviceSink>,
pub tracks: HashMap<u32, PlayingSound>,
pub next_id: u32,
@@ -68,10 +68,8 @@ impl AudioPlayer {
let daemon_config = get_daemon_config();
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
let stream_handle = DeviceSinkBuilder::open_default_sink()?;
let mut audio_player = AudioPlayer {
stream_handle,
stream_handle: None,
tracks: HashMap::new(),
next_id: 1,
@@ -88,6 +86,21 @@ impl AudioPlayer {
Ok(audio_player)
}
fn ensure_stream(&mut self) -> Result<&MixerDeviceSink, Box<dyn Error>> {
if self.stream_handle.is_none() {
let mut sink = DeviceSinkBuilder::open_default_sink()?;
sink.log_on_drop(false);
self.stream_handle = Some(sink);
}
Ok(self.stream_handle.as_ref().unwrap())
}
fn drop_stream(&mut self) {
if self.stream_handle.is_some() {
self.stream_handle = None;
}
}
fn abort_link_thread(&mut self) {
if let Some(sender) = &self.input_link_sender {
match sender.send(Terminate {}) {
@@ -179,6 +192,9 @@ impl AudioPlayer {
} else {
self.tracks.clear();
}
if self.tracks.is_empty() {
self.drop_stream();
}
}
pub fn is_paused(&self) -> bool {
@@ -299,12 +315,15 @@ impl AudioPlayer {
self.tracks.clear();
}
self.ensure_stream()?;
let id = self.next_id;
self.next_id += 1;
let duration = source.total_duration().map(|d| d.as_secs_f32());
let sink = Player::connect_new(self.stream_handle.mixer());
let mixer = self.stream_handle.as_ref().unwrap().mixer();
let sink = Player::connect_new(mixer);
sink.set_volume(self.volume); // Default volume is 1.0 * master
sink.append(source);
sink.play();
@@ -358,20 +377,23 @@ impl AudioPlayer {
tracks
}
pub async fn update(&mut self) {
if let Some(input_device_name) = &self.input_device_name {
// Unlink devices if selected input device was removed
if self.input_link_sender.is_some() && get_device(input_device_name).await.is_err() {
// Selected input device was removed
eprintln!(
"Selected input device {} was removed, unlinking devices",
input_device_name
);
self.abort_link_thread();
}
// Link devices if not linked
else if self.input_link_sender.is_none() {
self.link_devices().await.ok();
pub async fn update(&mut self, check_devices: bool) {
if check_devices {
if let Some(input_device_name) = &self.input_device_name {
// Unlink devices if selected input device was removed
if self.input_link_sender.is_some()
&& get_device(input_device_name).await.is_err()
{
eprintln!(
"Selected input device {} was removed, unlinking devices",
input_device_name
);
self.abort_link_thread();
}
// Link devices if not linked
else if self.input_link_sender.is_none() {
self.link_devices().await.ok();
}
}
}
@@ -412,6 +434,10 @@ impl AudioPlayer {
self.tracks
.retain(|_, sound| !sound.sink.empty() || sound.looped);
if self.tracks.is_empty() {
self.drop_stream();
}
}
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> {