Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43c21d3ddc | |||
| b73c6c346e | |||
| b91ddc5bdf | |||
| 7d08c06720 | |||
| f066a2a555 | |||
| b55b0e7c42 |
@@ -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 901
|
versionCode 903
|
||||||
versionName "0.0.9.1"
|
versionName "0.0.9.3"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<!-- Anruf-State lesen damit TTS bei klingelndem Telefon pausiert -->
|
<!-- Anruf-State lesen damit TTS bei klingelndem Telefon pausiert -->
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
<!-- Optional: GPS-Position der Frage anhaengen (nur wenn User in Settings aktiviert) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<!-- Foreground-Service damit TTS auch bei minimierter App weiterlaeuft.
|
<!-- Foreground-Service damit TTS auch bei minimierter App weiterlaeuft.
|
||||||
FOREGROUND_SERVICE_MICROPHONE ist Pflicht ab Android 14 wenn der
|
FOREGROUND_SERVICE_MICROPHONE ist Pflicht ab Android 14 wenn der
|
||||||
Service waehrend des Backgrounds aufs Mikro zugreift (Wake-Word,
|
Service waehrend des Backgrounds aufs Mikro zugreift (Wake-Word,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.media.AudioAttributes
|
|||||||
import android.media.AudioFormat
|
import android.media.AudioFormat
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioTrack
|
import android.media.AudioTrack
|
||||||
|
import android.os.Build
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
@@ -92,12 +93,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
val newTrack = AudioTrack.Builder()
|
val newTrack = AudioTrack.Builder()
|
||||||
.setAudioAttributes(
|
.setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
// USAGE_MEDIA statt USAGE_ASSISTANT — auf OnePlus A12 stallt
|
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
||||||
// AudioTrack mit USAGE_ASSISTANT wenn play() nach komplettem
|
|
||||||
// Buffer-Fuellen called wird (pos bleibt 0). USAGE_MEDIA ist
|
|
||||||
// robust. AudioFocus wird eh separat ueber AudioFocusModule
|
|
||||||
// gehandhabt, nicht ueber dieses USAGE-Tag.
|
|
||||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
@@ -112,7 +108,20 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// AudioTrack erstellen — play() wird erst aufgerufen wenn Pre-Roll erreicht.
|
// Start-Threshold runterdrehen: Default ist bufferSize/2 (= 2s bei 4s
|
||||||
|
// Buffer). AudioTrack startet sonst nicht bevor 2s im Puffer sind —
|
||||||
|
// bei kurzen TTS-Antworten (3 Worte ~ 1.4s) bleibt pos auf 0 stehen.
|
||||||
|
// 0.1s reicht damit AudioTrack sofort mit dem ersten Chunk anlaeuft.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
try {
|
||||||
|
val startFrames = (sampleRate / 10).coerceAtLeast(1) // 100ms
|
||||||
|
newTrack.setStartThresholdInFrames(startFrames)
|
||||||
|
Log.i(TAG, "Start-Threshold gesetzt: ${startFrames} frames (~100ms)")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "setStartThresholdInFrames failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
track = newTrack
|
track = newTrack
|
||||||
queue.clear()
|
queue.clear()
|
||||||
writerShouldStop = false
|
writerShouldStop = false
|
||||||
@@ -164,11 +173,12 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS)
|
val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
if (endRequested) {
|
if (endRequested) {
|
||||||
// Bei kurzem Text NICHT hier play() callen — erst nach
|
// Falls play() noch gar nicht lief (Stream ohne data
|
||||||
// Trailing-Silence + Padding (siehe Block nach mainLoop),
|
// ueberhaupt — sehr seltene Edge-Case): jetzt anstossen
|
||||||
// damit AudioTrack mit komplett gefuelltem Buffer startet.
|
// damit das finally{}-Wait nicht endlos blockt.
|
||||||
// OnePlus A12: AudioTrack startet nicht zuverlaessig wenn
|
if (!playbackStarted) {
|
||||||
// play() bei dünnem Buffer gerufen wird.
|
try { t.play(); playbackStarted = true } catch (_: Exception) {}
|
||||||
|
}
|
||||||
break@mainLoop
|
break@mainLoop
|
||||||
}
|
}
|
||||||
// Underrun-Schutz: Stille reinfuettern wenn der AudioTrack-
|
// Underrun-Schutz: Stille reinfuettern wenn der AudioTrack-
|
||||||
@@ -199,12 +209,16 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
}
|
}
|
||||||
idleMs = 0L
|
idleMs = 0L
|
||||||
|
|
||||||
// Pre-Roll Check: play() erst wenn genug gepuffert
|
// play() beim ALLERERSTEN data-chunk aufrufen — egal wie wenig
|
||||||
if (!playbackStarted && bytesBuffered + data.size >= prerollBytes) {
|
// Daten da sind. Sonst stallt AudioTrack auf OnePlus A12 wenn
|
||||||
|
// play() erst gerufen wird nachdem der Buffer komplett gefuellt
|
||||||
|
// ist. Pre-Roll als "Vorrat aufbauen" passiert dann waehrend
|
||||||
|
// der Track schon spielt — Underrun-Schutz fuettert ggf. Stille.
|
||||||
|
if (!playbackStarted) {
|
||||||
try {
|
try {
|
||||||
t.play()
|
t.play()
|
||||||
playbackStarted = true
|
playbackStarted = true
|
||||||
Log.i(TAG, "Playback gestartet nach Pre-Roll ${bytesBuffered + data.size} Bytes")
|
Log.i(TAG, "Playback gestartet beim 1. Chunk (${bytesBuffered}B leading + ${data.size}B data)")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "play() failed: ${e.message}")
|
Log.w(TAG, "play() failed: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -231,31 +245,6 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
}
|
}
|
||||||
bytesBuffered += silence.size
|
bytesBuffered += silence.size
|
||||||
}
|
}
|
||||||
// Bei kurzem Text (play() noch nicht gestartet): Buffer auf min.
|
|
||||||
// 3s padden + DANN play(). Auf OnePlus A12 startet AudioTrack
|
|
||||||
// bei < 3s Buffer-Inhalt nicht — pos bleibt auf 0 stehen.
|
|
||||||
// (2s war zu wenig, 8 Worte ~2.5s gingen, 3 Worte ~1s nicht.)
|
|
||||||
if (!playbackStarted && !writerShouldStop) {
|
|
||||||
val minStartBytes = bytesPerSecond * 3
|
|
||||||
if (bytesBuffered < minStartBytes) {
|
|
||||||
val padBytes = (minStartBytes - bytesBuffered.toInt()) and 0x7FFFFFFE
|
|
||||||
val pad = ByteArray(padBytes)
|
|
||||||
var padOff = 0
|
|
||||||
while (padOff < pad.size && !writerShouldStop) {
|
|
||||||
val w = t.write(pad, padOff, pad.size - padOff)
|
|
||||||
if (w <= 0) break
|
|
||||||
padOff += w
|
|
||||||
}
|
|
||||||
bytesBuffered += pad.size
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
t.play()
|
|
||||||
playbackStarted = true
|
|
||||||
Log.i(TAG, "Playback gestartet (kurzer Text, ${bytesBuffered}B komplett gepuffert)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "play() short-text failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Writer-Thread Fehler: ${e.message}")
|
Log.w(TAG, "Writer-Thread Fehler: ${e.message}")
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.0.9.1",
|
"version": "0.0.9.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Modal,
|
Modal,
|
||||||
|
PermissionsAndroid,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
@@ -457,7 +458,29 @@ const SettingsScreen: React.FC = () => {
|
|||||||
|
|
||||||
// --- GPS Toggle ---
|
// --- GPS Toggle ---
|
||||||
|
|
||||||
const handleGPSToggle = useCallback((value: boolean) => {
|
const handleGPSToggle = useCallback(async (value: boolean) => {
|
||||||
|
if (value && Platform.OS === 'android') {
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
|
||||||
|
{
|
||||||
|
title: 'ARIA — Standort an Anfragen anhaengen',
|
||||||
|
message: 'Damit ARIA bei Anfragen wie "Wo ist der naechste...?" den '
|
||||||
|
+ 'Standort kennt, darf die App den ungefaehren Standort lesen. '
|
||||||
|
+ 'Wird nur bei jeder Anfrage einmal abgerufen, nicht im Hintergrund.',
|
||||||
|
buttonPositive: 'Erlauben',
|
||||||
|
buttonNegative: 'Abbrechen',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
||||||
|
ToastAndroid.show('Standort-Berechtigung abgelehnt', ToastAndroid.SHORT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Settings] GPS-Permission Request gescheitert:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setGpsEnabled(value);
|
setGpsEnabled(value);
|
||||||
AsyncStorage.setItem('aria_gps_enabled', String(value)).catch(() => {});
|
AsyncStorage.setItem('aria_gps_enabled', String(value)).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1098,14 +1098,15 @@ class AudioService {
|
|||||||
if (this.preloadedPath) RNFS.unlink(this.preloadedPath).catch(() => {});
|
if (this.preloadedPath) RNFS.unlink(this.preloadedPath).catch(() => {});
|
||||||
this.preloadedPath = '';
|
this.preloadedPath = '';
|
||||||
}
|
}
|
||||||
// PCM-Stream ebenfalls hart stoppen (Cancel/Abbruch)
|
// PCM-Stream ebenfalls hart stoppen (Cancel/Abbruch).
|
||||||
if (this.pcmStreamActive) {
|
// pcmStreamActive wird beim isFinal-Chunk schon false gesetzt — der
|
||||||
PcmStreamPlayer?.stop().catch(() => {});
|
// AudioTrack spielt aber noch sekundenlang aus seinem Buffer ab. Daher
|
||||||
this.pcmStreamActive = false;
|
// IMMER stop() aufrufen, ohne den Flag zu pruefen (ist idempotent).
|
||||||
this.pcmBuffer = [];
|
PcmStreamPlayer?.stop().catch(() => {});
|
||||||
this.pcmBytesCollected = 0;
|
this.pcmStreamActive = false;
|
||||||
this.pcmMessageId = '';
|
this.pcmBuffer = [];
|
||||||
}
|
this.pcmBytesCollected = 0;
|
||||||
|
this.pcmMessageId = '';
|
||||||
// Audio-Focus sofort freigeben — User hat explizit abgebrochen
|
// Audio-Focus sofort freigeben — User hat explizit abgebrochen
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
AudioFocus?.release().catch(() => {});
|
AudioFocus?.release().catch(() => {});
|
||||||
|
|||||||
Reference in New Issue
Block a user