Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b182ef5ed5 | |||
| 9818dc1867 | |||
| 543ad3c46d | |||
| 408d20a087 |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10604
|
versionCode 10605
|
||||||
versionName "0.1.6.4"
|
versionName "0.1.6.5"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
||||||
@@ -32,15 +34,26 @@ class AriaPlaybackService : Service() {
|
|||||||
|
|
||||||
private var currentReason: String = ""
|
private var currentReason: String = ""
|
||||||
|
|
||||||
|
// PARTIAL_WAKE_LOCK haelt die CPU wach solange der Foreground-Service
|
||||||
|
// aktiv ist. Damit bleibt die JS-Bridge im Doze ansprechbar und die
|
||||||
|
// gesamte Sprach-Pipeline (Wake → Aufnahme → POST → ARIA → TTS → wieder
|
||||||
|
// Wake) laeuft durchgehend im Hintergrund. Ein einziger Lock fuer den
|
||||||
|
// ganzen Foreground-Cycle, nicht pro Sub-Modul.
|
||||||
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ensureNotificationChannel()
|
ensureNotificationChannel()
|
||||||
|
acquireWakeLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val reason = intent?.getStringExtra(EXTRA_REASON) ?: ""
|
val reason = intent?.getStringExtra(EXTRA_REASON) ?: ""
|
||||||
currentReason = reason
|
currentReason = reason
|
||||||
Log.i(TAG, "Foreground-Service start/update (reason=$reason)")
|
Log.i(TAG, "Foreground-Service start/update (reason=$reason)")
|
||||||
|
// Falls der Lock zwischendurch released wurde (z.B. nach onCreate-
|
||||||
|
// race oder OS-quirk), hier sicherheits-halber erneut anfordern.
|
||||||
|
acquireWakeLock()
|
||||||
try {
|
try {
|
||||||
startForeground(NOTIFICATION_ID, buildNotification(reason))
|
startForeground(NOTIFICATION_ID, buildNotification(reason))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -53,10 +66,36 @@ class AriaPlaybackService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
releaseWakeLock()
|
||||||
Log.i(TAG, "Foreground-Service gestoppt")
|
Log.i(TAG, "Foreground-Service gestoppt")
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun acquireWakeLock() {
|
||||||
|
if (wakeLock?.isHeld == true) return
|
||||||
|
try {
|
||||||
|
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
"AriaCockpit:Pipeline").apply {
|
||||||
|
setReferenceCounted(false)
|
||||||
|
acquire(8 * 60 * 60 * 1000L) // 8h Sicherheits-Cap
|
||||||
|
}
|
||||||
|
Log.i(TAG, "WakeLock acquired (CPU bleibt wach im Hintergrund)")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
private fun ensureNotificationChannel() {
|
private fun ensureNotificationChannel() {
|
||||||
|
|||||||
@@ -131,6 +131,58 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
|||||||
promise.resolve(true)
|
promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sanfter Spotify-Resume-Nudge: kurz USAGE_MEDIA mit TRANSIENT
|
||||||
|
* requesten und sofort abandonen. Spotify bekommt das als
|
||||||
|
* Focus-Frei-Signal und resumed automatisch — aber weil TRANSIENT
|
||||||
|
* (nicht GAIN permanent), interpretiert Spotify das NICHT als
|
||||||
|
* "user stopped" was Auto-Resume verhindert haette.
|
||||||
|
*
|
||||||
|
* Hintergrund: ARIA spricht TTS via USAGE_ASSISTANT GAIN_TRANSIENT,
|
||||||
|
* Spotify pausiert. ARIA released. Spotify SOLLTE nach
|
||||||
|
* TRANSIENT-Loss + Abandon automatisch resumen, tut es aber bei
|
||||||
|
* manchen Versionen / Geraeten nicht zuverlaessig. Dieser Nudge
|
||||||
|
* triggert den Focus-Stack-Refresh ohne den Spotify-Auto-Stop-Bug
|
||||||
|
* der alten kickReleaseMedia mit GAIN permanent.
|
||||||
|
*/
|
||||||
|
@ReactMethod
|
||||||
|
fun nudgeMediaResume(promise: Promise) {
|
||||||
|
val am = audioManager()
|
||||||
|
if (am == null) {
|
||||||
|
promise.resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val attrs = AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
|
.build()
|
||||||
|
val nudgeListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||||
|
val nudgeReq = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||||
|
.setAudioAttributes(attrs)
|
||||||
|
.setOnAudioFocusChangeListener(nudgeListener)
|
||||||
|
.build()
|
||||||
|
am.requestAudioFocus(nudgeReq)
|
||||||
|
Thread.sleep(100)
|
||||||
|
am.abandonAudioFocusRequest(nudgeReq)
|
||||||
|
} else {
|
||||||
|
val nudgeListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
am.requestAudioFocus(nudgeListener, AudioManager.STREAM_MUSIC,
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||||
|
Thread.sleep(100)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
am.abandonAudioFocus(nudgeListener)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "nudgeMediaResume: USAGE_MEDIA TRANSIENT request+abandon (Spotify-Resume-Trigger)")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "nudgeMediaResume failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
/** Den USAGE_MEDIA-Focus-Stack im System aufmischen, damit Spotify/YouTube
|
/** Den USAGE_MEDIA-Focus-Stack im System aufmischen, damit Spotify/YouTube
|
||||||
* resumen wenn ein anderer Player (z.B. react-native-sound) seinen Focus
|
* resumen wenn ein anderer Player (z.B. react-native-sound) seinen Focus
|
||||||
* nicht ordnungsgemaess released hat. Strategie: kurz selbst USAGE_MEDIA
|
* nicht ordnungsgemaess released hat. Strategie: kurz selbst USAGE_MEDIA
|
||||||
@@ -140,6 +192,10 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
|||||||
*
|
*
|
||||||
* Workaround fuer das react-native-sound-Bug: Sound.stop()/release()
|
* Workaround fuer das react-native-sound-Bug: Sound.stop()/release()
|
||||||
* laesst den AudioFocusRequest haengen.
|
* laesst den AudioFocusRequest haengen.
|
||||||
|
*
|
||||||
|
* ⚠️ ACHTUNG: nutzt AUDIOFOCUS_GAIN (permanent), Spotify kann das als
|
||||||
|
* "user-action stopp" interpretieren und Auto-Resume verhindern.
|
||||||
|
* Fuer Spotify-Resume nach TTS lieber nudgeMediaResume() nehmen (sanfter).
|
||||||
*/
|
*/
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun kickReleaseMedia(promise: Promise) {
|
fun kickReleaseMedia(promise: Promise) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.6.4",
|
"version": "0.1.6.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const { AudioFocus, PcmStreamPlayer } = NativeModules as {
|
|||||||
AudioFocus?: {
|
AudioFocus?: {
|
||||||
requestDuck: () => Promise<boolean>;
|
requestDuck: () => Promise<boolean>;
|
||||||
requestExclusive: () => Promise<boolean>;
|
requestExclusive: () => Promise<boolean>;
|
||||||
|
nudgeMediaResume: () => Promise<boolean>;
|
||||||
release: () => Promise<boolean>;
|
release: () => Promise<boolean>;
|
||||||
kickReleaseMedia: () => Promise<boolean>;
|
kickReleaseMedia: () => Promise<boolean>;
|
||||||
getMode?: () => Promise<number>;
|
getMode?: () => Promise<number>;
|
||||||
@@ -332,6 +333,13 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
console.log('[Audio] AudioFocus jetzt released');
|
console.log('[Audio] AudioFocus jetzt released');
|
||||||
AudioFocus?.release().catch(() => {});
|
AudioFocus?.release().catch(() => {});
|
||||||
|
// Spotify-Resume-Trigger: nach Abandon den USAGE_MEDIA-Focus-Stack
|
||||||
|
// mit kurzem TRANSIENT-Nudge aufmischen. Spotify resumed sonst bei
|
||||||
|
// manchen Versionen / Geraeten nicht zuverlaessig nach Auto-Loss.
|
||||||
|
// 50ms Delay damit das Abandon erst durch ist.
|
||||||
|
setTimeout(() => {
|
||||||
|
AudioFocus?.nudgeMediaResume().catch(() => {});
|
||||||
|
}, 50);
|
||||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user