Просмотр исходного кода

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 2 недель назад
Родитель
Сommit
b10532e335
4 измененных файлов с 105 добавлено и 9 удалено
  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"
 version = "0.22.2"
 source = "git+https://github.com/RustAudio/rodio#880aaba54b00aead53651ed1ccbdb672d1375a95"
 source = "git+https://github.com/RustAudio/rodio#880aaba54b00aead53651ed1ccbdb672d1375a95"
 dependencies = [
 dependencies = [
+ "atomic_float",
  "cpal",
  "cpal",
  "dasp_sample",
  "dasp_sample",
  "rubato",
  "rubato",

+ 3 - 1
bin/app/Cargo.toml

@@ -67,7 +67,9 @@ indoc = "2.0.7"
 # Sound playback for UI events.
 # Sound playback for UI events.
 # Git master: uses cpal 0.18 which has native pulseaudio/pipewire backends
 # 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).
 # (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.
 # This makes a HUGE difference to decoding speed.
 # Over 160s to like 2s.
 # Over 160s to like 2s.

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

@@ -24,7 +24,7 @@ use zeromq::{Socket, SocketRecv, SocketSend};
 use crate::{
 use crate::{
     app::node::{create_layer, create_vector_art},
     app::node::{create_layer, create_vector_art},
     error::{Error, Result},
     error::{Error, Result},
-    expr::{decompile, MachineGlobals, Compiler, SExprCode, SExprMachine, SExprVal},
+    expr::{decompile, Compiler, MachineGlobals, SExprCode, SExprMachine, SExprVal},
     gfx::{gfxtag, Renderer},
     gfx::{gfxtag, Renderer},
     prop::{PropertyType, Role},
     prop::{PropertyType, Role},
     scene::{Pimpl, SceneNodeId, SceneNodePtr, SceneNodeType, ScenePath, Slot},
     scene::{Pimpl, SceneNodeId, SceneNodePtr, SceneNodeType, ScenePath, Slot},
@@ -373,9 +373,9 @@ impl ZeroMQAdapter {
                         let mut names: Vec<String> =
                         let mut names: Vec<String> =
                             prop.get_depends().into_iter().map(|d| d.local_name).collect();
                             prop.get_depends().into_iter().map(|d| d.local_name).collect();
                         names.extend(
                         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)?;
                         check_expr(&code, &names)?;
                         prop.set_expr(atom, Role::User, prop_i, code)?;
                         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.
 //! device cannot be opened, sounds are silently disabled for the session.
 //! On Android the JavaVM/Activity context must be registered with
 //! On Android the JavaVM/Activity context must be registered with
 //! `ndk_context` before cpal initializes its AAudio backend.
 //! `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 rodio::Source;
 use std::{io::Cursor, sync::LazyLock};
 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)*); } }
 macro_rules! e { ($($arg:tt)*) => { error!(target: "app::sfx", $($arg)*); } }
 
 
 static CLICK_OGA: &[u8] = include_bytes!("../data/sfx/click.oga");
 static CLICK_OGA: &[u8] = include_bytes!("../data/sfx/click.oga");
 static COMMUP_OGA: &[u8] = include_bytes!("../data/sfx/commup.oga");
 static COMMUP_OGA: &[u8] = include_bytes!("../data/sfx/commup.oga");
 static CLOAK_OGA: &[u8] = include_bytes!("../data/sfx/cloak.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 {
 struct Sfx {
     /// Keeps the output stream alive for the process lifetime
     /// Keeps the output stream alive for the process lifetime
     _output: rodio::MixerDeviceSink,
     _output: rodio::MixerDeviceSink,
@@ -43,6 +62,9 @@ struct Sfx {
     commup: rodio::buffer::SamplesBuffer,
     commup: rodio::buffer::SamplesBuffer,
     /// Used for showing p2p overlay
     /// Used for showing p2p overlay
     cloak: rodio::buffer::SamplesBuffer,
     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() {
 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 click = rodio::Decoder::try_from(Cursor::new(CLICK_OGA))?.record();
     let commup = rodio::Decoder::try_from(Cursor::new(COMMUP_OGA))?.record();
     let commup = rodio::Decoder::try_from(Cursor::new(COMMUP_OGA))?.record();
     let cloak = rodio::Decoder::try_from(Cursor::new(CLOAK_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
 /// 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() {
 pub fn play_click() {
     if let Some(sfx) = &*SFX {
     if let Some(sfx) = &*SFX {
-        sfx.mixer.add(sfx.click.clone());
+        sfx.play(&sfx.click);
     }
     }
 }
 }
 
 
 pub fn play_commup() {
 pub fn play_commup() {
     if let Some(sfx) = &*SFX {
     if let Some(sfx) = &*SFX {
-        sfx.mixer.add(sfx.commup.clone());
+        sfx.play(&sfx.commup);
     }
     }
 }
 }
 
 
 pub fn play_cloak() {
 pub fn play_cloak() {
     if let Some(sfx) = &*SFX {
     if let Some(sfx) = &*SFX {
-        sfx.mixer.add(sfx.cloak.clone());
+        sfx.play(&sfx.cloak);
     }
     }
 }
 }