Explorar el Código

app: on android keeping a handle to the audiomixer open will constantly play a 'silent' side which keeps a wakelock that stops suspend and drains the battery. here to solve the issue we use an experimental api from rodio which enables us to pause the mixer on android. we do this with a special reaper thread that activates 6s after starting a sound to pause the mixer. fucking android

darkfi hace 2 semanas
padre
commit
b10532e335
Se han modificado 4 ficheros con 105 adiciones y 9 borrados
  1. 1 0
      bin/app/Cargo.lock
  2. 3 1
      bin/app/Cargo.toml
  3. 4 4
      bin/app/src/net.rs
  4. 97 4
      bin/app/src/sfx.rs

+ 1 - 0
bin/app/Cargo.lock

@@ -6559,6 +6559,7 @@ name = "rodio"
 version = "0.22.2"
 source = "git+https://github.com/RustAudio/rodio#880aaba54b00aead53651ed1ccbdb672d1375a95"
 dependencies = [
+ "atomic_float",
  "cpal",
  "dasp_sample",
  "rubato",

+ 3 - 1
bin/app/Cargo.toml

@@ -67,7 +67,9 @@ indoc = "2.0.7"
 # Sound playback for UI events.
 # Git master: uses cpal 0.18 which has native pulseaudio/pipewire backends
 # (cpal 0.17 is ALSA-only and fails when pipewire owns the default device).
-rodio = { git = "https://github.com/RustAudio/rodio", default-features = false, features = ["playback", "vorbis", "pulseaudio"] }
+# "experimental" gates MixerDeviceSink::pause()/play(), used on Android to
+# pause the stream while idle so it holds no system wakelock.
+rodio = { git = "https://github.com/RustAudio/rodio", default-features = false, features = ["playback", "vorbis", "pulseaudio", "experimental"] }
 
 # This makes a HUGE difference to decoding speed.
 # Over 160s to like 2s.

+ 4 - 4
bin/app/src/net.rs

@@ -24,7 +24,7 @@ use zeromq::{Socket, SocketRecv, SocketSend};
 use crate::{
     app::node::{create_layer, create_vector_art},
     error::{Error, Result},
-    expr::{decompile, MachineGlobals, Compiler, SExprCode, SExprMachine, SExprVal},
+    expr::{decompile, Compiler, MachineGlobals, SExprCode, SExprMachine, SExprVal},
     gfx::{gfxtag, Renderer},
     prop::{PropertyType, Role},
     scene::{Pimpl, SceneNodeId, SceneNodePtr, SceneNodeType, ScenePath, Slot},
@@ -373,9 +373,9 @@ impl ZeroMQAdapter {
                         let mut names: Vec<String> =
                             prop.get_depends().into_iter().map(|d| d.local_name).collect();
                         names.extend(
-                            ["w", "h", "parent_w", "parent_h", "rect_w", "rect_h"].iter().map(
-                                |s| s.to_string(),
-                            ),
+                            ["w", "h", "parent_w", "parent_h", "rect_w", "rect_h"]
+                                .iter()
+                                .map(|s| s.to_string()),
                         );
                         check_expr(&code, &names)?;
                         prop.set_expr(atom, Role::User, prop_i, code)?;

+ 97 - 4
bin/app/src/sfx.rs

@@ -22,16 +22,35 @@
 //! device cannot be opened, sounds are silently disabled for the session.
 //! On Android the JavaVM/Activity context must be registered with
 //! `ndk_context` before cpal initializes its AAudio backend.
+//!
+//! On Android a started but idle stream keeps the audio pipeline and its
+//! system wakelock active, so the stream is paused once the sounds have
+//! finished and resumed when the next one plays.
 
 use rodio::Source;
 use std::{io::Cursor, sync::LazyLock};
 
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+use std::{
+    sync::mpsc::{self, RecvTimeoutError},
+    time::Duration,
+};
+
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+use crate::util::spawn_thread;
+
 macro_rules! e { ($($arg:tt)*) => { error!(target: "app::sfx", $($arg)*); } }
 
 static CLICK_OGA: &[u8] = include_bytes!("../data/sfx/click.oga");
 static COMMUP_OGA: &[u8] = include_bytes!("../data/sfx/commup.oga");
 static CLOAK_OGA: &[u8] = include_bytes!("../data/sfx/cloak.oga");
 
+/// Idle window after the last sound before the stream is paused.
+/// Pausing flushes buffered audio, so it must exceed the duration of
+/// the longest sound (cloak, ~3.4s).
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+const IDLE_MARGIN: Duration = Duration::from_secs(6);
+
 struct Sfx {
     /// Keeps the output stream alive for the process lifetime
     _output: rodio::MixerDeviceSink,
@@ -43,6 +62,9 @@ struct Sfx {
     commup: rodio::buffer::SamplesBuffer,
     /// Used for showing p2p overlay
     cloak: rodio::buffer::SamplesBuffer,
+    /// Signals playback activity to the idle reaper thread
+    #[cfg(any(target_os = "android", feature = "emulate-android"))]
+    activity: mpsc::Sender<()>,
 }
 
 static SFX: LazyLock<Option<Sfx>> = LazyLock::new(|| match init() {
@@ -62,7 +84,69 @@ fn init() -> Result<Sfx, Box<dyn std::error::Error>> {
     let click = rodio::Decoder::try_from(Cursor::new(CLICK_OGA))?.record();
     let commup = rodio::Decoder::try_from(Cursor::new(COMMUP_OGA))?.record();
     let cloak = rodio::Decoder::try_from(Cursor::new(CLOAK_OGA))?.record();
-    Ok(Sfx { _output: output, mixer, click, commup, cloak })
+
+    // Leaving the sound mixer on in Android will just hold a wakelock
+    // and drain battery. It is playing a silent sound so we must pause
+    // the stream.
+    #[cfg(any(target_os = "android", feature = "emulate-android"))]
+    let activity = spawn_idle_reaper();
+
+    Ok(Sfx {
+        _output: output,
+        mixer,
+        click,
+        commup,
+        cloak,
+        #[cfg(any(target_os = "android", feature = "emulate-android"))]
+        activity,
+    })
+}
+
+/// Pauses the output stream once the idle window passes without playback,
+/// and resumes it when the next sound is played. A started but idle
+/// AAudio stream keeps the audio pipeline and its system wakelock open,
+/// so it must not stay running while nothing plays. Pausing flushes
+/// buffered audio, which is why the idle window always exceeds the
+/// longest sound. cpal's pause and play block on a state change with a
+/// timeout, so they run on this dedicated thread instead of the UI
+/// thread.
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+fn spawn_idle_reaper() -> mpsc::Sender<()> {
+    let (tx, rx) = mpsc::channel();
+    spawn_thread("sfx-idle", move || {
+        // The stream runs from creation, then alternates between running
+        // while sounds keep arriving and paused once the idle window
+        // elapses, so every play() lands on a paused stream and every
+        // pause() on a running one.
+        loop {
+            loop {
+                match rx.recv_timeout(IDLE_MARGIN) {
+                    // Another sound has been played so continue loop
+                    Ok(()) => {}
+                    // Idle timeout so pause the stream
+                    Err(RecvTimeoutError::Timeout) => {
+                        if let Some(sfx) = &*SFX {
+                            sfx._output.pause();
+                        }
+                        break;
+                    }
+                    // Receiver dropped which means this thread can exit
+                    Err(RecvTimeoutError::Disconnected) => return,
+                }
+            }
+            match rx.recv() {
+                // Sound is received for playing
+                Ok(()) => {
+                    if let Some(sfx) = &*SFX {
+                        sfx._output.play();
+                    }
+                }
+                // Receiver dropped which means this thread can exit
+                Err(_) => return,
+            }
+        }
+    });
+    tx
 }
 
 /// cpal's AAudio backend fetches the JavaVM and Activity from the
@@ -82,20 +166,29 @@ fn init_android_context() {
     }
 }
 
+impl Sfx {
+    fn play(&self, sound: &rodio::buffer::SamplesBuffer) {
+        // Notify the reaper before appending, so the stream is resumed
+        #[cfg(any(target_os = "android", feature = "emulate-android"))]
+        let _ = self.activity.send(());
+        self.mixer.add(sound.clone());
+    }
+}
+
 pub fn play_click() {
     if let Some(sfx) = &*SFX {
-        sfx.mixer.add(sfx.click.clone());
+        sfx.play(&sfx.click);
     }
 }
 
 pub fn play_commup() {
     if let Some(sfx) = &*SFX {
-        sfx.mixer.add(sfx.commup.clone());
+        sfx.play(&sfx.commup);
     }
 }
 
 pub fn play_cloak() {
     if let Some(sfx) = &*SFX {
-        sfx.mixer.add(sfx.cloak.clone());
+        sfx.play(&sfx.cloak);
     }
 }