Compare commits

..

6 Commits

Author SHA1 Message Date
duffyduck 43c21d3ddc release: bump version to 0.0.9.3 2026-05-10 14:48:35 +02:00
duffyduck b73c6c346e fix(gps): Standort-Permission anfordern — sonst sendet App nie eine Position
Im Manifest fehlte ACCESS_COARSE/FINE_LOCATION komplett, und der
Settings-Toggle requestete keine Runtime-Permission. Geolocation
.getCurrentPosition() schlug darum lautlos fehl, App sendete nie ein
location-Feld → Diagnostic konnte nichts anzeigen, auch wenn der
Diagnostic-eigene "GPS einblenden"-Toggle aktiv war.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:47:35 +02:00
duffyduck b91ddc5bdf fix(audio): AudioTrack-Start-Threshold auf 100ms — kurze TTS startet jetzt
ENDLICH die Wurzel: AudioTrack hat seit API 31 setStartThresholdInFrames(),
default ist bufferSize/2. Bei 4s-Buffer = 2s Threshold — Track wartet bis
2s im Buffer sind, sonst startet play() nie wirklich (pos bleibt 0).

Bei 3 Worten (~1.4s) kommt's nie ueber die Schwelle. Threshold runter
auf 100ms (2400 Frames @ 24kHz) — Track laeuft sofort mit erstem Chunk an.

Erklaert auch warum genau ab 9 Worten (~3s+) der Pre-Roll-Pfad lief: dann
wurde die 2s-Schwelle ueberschritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:45:05 +02:00
duffyduck 7d08c06720 release: bump version to 0.0.9.2 2026-05-10 14:40:35 +02:00
duffyduck f066a2a555 fix(audio): Mute-Button stoppt jetzt auch laufenden PCM-Stream
pcmStreamActive wurde beim isFinal-Chunk schon auf false gesetzt, der
AudioTrack spielte aber noch aus seinem Buffer (kann sekundenlang sein).
stopPlayback() uebersprang darum PcmStreamPlayer.stop() — ARIA redete
weiter obwohl Spotify schon resumed war.

Fix: stop() immer rufen, der Flag-Check faellt weg (ist eh idempotent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:39:27 +02:00
duffyduck b55b0e7c42 fix(audio): play() beim 1. Chunk — kurze Texte stallen nicht mehr
Logs zeigten: Pre-Roll-Pfad (play() WAEHREND chunks reinkommen) lief
immer sauber, Kurz-Text-Pfad (play() NACHDEM Buffer komplett gefuellt
ist) stallte immer — egal mit wie viel Daten oder welchem USAGE-Tag.

Fix: play() beim allerersten data-chunk callen, kein Pre-Roll-Threshold
mehr. AudioTrack ist sofort im PLAYING-State, weitere chunks/trailing
fliessen parallel ab. Padding-Block nach mainLoop entfaellt komplett.

USAGE_MEDIA wieder auf USAGE_ASSISTANT zurueck — war nicht die Ursache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:38:02 +02:00
6 changed files with 68 additions and 52 deletions
+2 -2
View File
@@ -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 -1
View File
@@ -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",
+24 -1
View File
@@ -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(() => {});
}, []); }, []);
+9 -8
View File
@@ -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(() => {});