fix(app): PARTIAL_WAKE_LOCK fuer Wake-Word — JS-Bridge bleibt im Hintergrund aktiv
Stefan-Bug-Report: Wake-Word wird im Hintergrund erkannt (Spotify
pausiert sofort), aber der Gong + Aufnahme-Start kommen erst wenn die
App in den Vordergrund geholt wird. Akku-Optimierung war bereits
deaktiviert ("Hintergrund aktiv").
Ursache: Foreground-Service haelt den App-Prozess am Leben + erlaubt
mic-Zugriff via foregroundServiceType=microphone. Aber: ohne expliziten
WakeLock kann die CPU im Doze-Mode (Display aus / Telefon idle) die
Auslieferung von DeviceEvents an die React-Native-JS-Bridge pausieren.
Folge: Native erkennt Wake-Word, ruft emit("WakeWordDetected"), aber
das Event queued sich nur — der JS-Listener (onWakeDetected → start-
Recording + playWakeReadySound) feuert erst beim naechsten JS-Tick,
und der kommt erst beim App-Resume.
Fix:
- AndroidManifest: WAKE_LOCK Permission hinzu (kein User-Prompt noetig,
ist eine "normal" Permission).
- OpenWakeWordModule.kt: PowerManager.PARTIAL_WAKE_LOCK in start()
acquired (8h Cap als Sicherheit), in stop() + dispose() released.
Lock-Tag "AriaCockpit:WakeWordRecord" damit der in adb shell dumpsys
power sichtbar ist.
Wirkung: solange Wake-Word "armed" ist, bleibt die CPU wach und die
JS-Bridge verarbeitet die Detection-Events live — Gong, Mic-Start,
ARIA-Antwort kommen ohne Foreground-Resume durch.
APK muss neu gebaut werden (native Kotlin-Aenderung).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,11 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<!-- WAKE_LOCK damit Wake-Word + JS-Bridge auch bei aus-Display und Doze
|
||||||
|
arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord
|
||||||
|
laeuft weiter aber JS-Bridge frisst die DeviceEvents nicht mehr ->
|
||||||
|
Wake-Word wird erkannt aber callbacks feuern erst beim App-Resume. -->
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ai.onnxruntime.OnnxTensor
|
|||||||
import ai.onnxruntime.OrtEnvironment
|
import ai.onnxruntime.OrtEnvironment
|
||||||
import ai.onnxruntime.OrtSession
|
import ai.onnxruntime.OrtSession
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.media.AudioFormat
|
import android.media.AudioFormat
|
||||||
import android.media.AudioRecord
|
import android.media.AudioRecord
|
||||||
@@ -11,6 +12,7 @@ import android.media.MediaRecorder
|
|||||||
import android.media.audiofx.AcousticEchoCanceler
|
import android.media.audiofx.AcousticEchoCanceler
|
||||||
import android.media.audiofx.AutomaticGainControl
|
import android.media.audiofx.AutomaticGainControl
|
||||||
import android.media.audiofx.NoiseSuppressor
|
import android.media.audiofx.NoiseSuppressor
|
||||||
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.facebook.react.bridge.Promise
|
import com.facebook.react.bridge.Promise
|
||||||
@@ -80,6 +82,13 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|||||||
private var ns: NoiseSuppressor? = null
|
private var ns: NoiseSuppressor? = null
|
||||||
private var agc: AutomaticGainControl? = null
|
private var agc: AutomaticGainControl? = null
|
||||||
|
|
||||||
|
// PARTIAL_WAKE_LOCK damit die CPU bei aus-Display nicht in Doze geht und
|
||||||
|
// die JS-Bridge die WakeWordDetected-Events live verarbeitet (sonst
|
||||||
|
// queuen sich die Events nur und werden erst beim App-Foreground
|
||||||
|
// delivered — Stefan-Beobachtung: "Spotify pausiert, aber Gong/Aufnahme
|
||||||
|
// kommen erst wenn ich die App nach vorne hole").
|
||||||
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
// Inferenz-State
|
// Inferenz-State
|
||||||
private val melBuffer: ArrayList<FloatArray> = ArrayList(256) // Liste von 32-dim Frames
|
private val melBuffer: ArrayList<FloatArray> = ArrayList(256) // Liste von 32-dim Frames
|
||||||
private var melProcessedIdx: Int = 0
|
private var melProcessedIdx: Int = 0
|
||||||
@@ -198,6 +207,21 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|||||||
running.set(true)
|
running.set(true)
|
||||||
record.startRecording()
|
record.startRecording()
|
||||||
|
|
||||||
|
// PARTIAL_WAKE_LOCK greifen damit die CPU nicht in Doze geht und
|
||||||
|
// die JS-Bridge die emit("WakeWordDetected")-Events live verarbeitet.
|
||||||
|
// 8h Cap als Sicherheit gegen forgotten-release.
|
||||||
|
try {
|
||||||
|
val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
"AriaCockpit:WakeWordRecord").apply {
|
||||||
|
setReferenceCounted(false)
|
||||||
|
acquire(8 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "WakeLock acquired")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
captureThread = Thread({ captureLoop() }, "OpenWakeWordCapture").apply {
|
captureThread = Thread({ captureLoop() }, "OpenWakeWordCapture").apply {
|
||||||
isDaemon = true
|
isDaemon = true
|
||||||
start()
|
start()
|
||||||
@@ -232,6 +256,7 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|||||||
try { audioRecord?.release() } catch (_: Exception) {}
|
try { audioRecord?.release() } catch (_: Exception) {}
|
||||||
audioRecord = null
|
audioRecord = null
|
||||||
releaseAudioEffects()
|
releaseAudioEffects()
|
||||||
|
releaseWakeLock()
|
||||||
Log.i(TAG, "Lauschen gestoppt")
|
Log.i(TAG, "Lauschen gestoppt")
|
||||||
promise.resolve(true)
|
promise.resolve(true)
|
||||||
}
|
}
|
||||||
@@ -245,10 +270,21 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|||||||
try { audioRecord?.release() } catch (_: Exception) {}
|
try { audioRecord?.release() } catch (_: Exception) {}
|
||||||
audioRecord = null
|
audioRecord = null
|
||||||
releaseAudioEffects()
|
releaseAudioEffects()
|
||||||
|
releaseWakeLock()
|
||||||
disposeSessions()
|
disposeSessions()
|
||||||
promise.resolve(true)
|
promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun releaseWakeLock() {
|
||||||
|
try {
|
||||||
|
wakeLock?.takeIf { it.isHeld }?.release()
|
||||||
|
if (wakeLock != null) Log.i(TAG, "WakeLock released")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "WakeLock release fehlgeschlagen: ${e.message}")
|
||||||
|
}
|
||||||
|
wakeLock = null
|
||||||
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun isAvailable(promise: Promise) {
|
fun isAvailable(promise: Promise) {
|
||||||
// Wake-Word ist immer verfuegbar (kein API-Key, alles on-device)
|
// Wake-Word ist immer verfuegbar (kein API-Key, alles on-device)
|
||||||
|
|||||||
Reference in New Issue
Block a user