Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2750b867a3 | |||
| f6424add6c | |||
| 2dfd21d1d0 | |||
| 9d9ddc730b | |||
| 77ccee8331 | |||
| 175dcdf225 | |||
| 1549e9cd4f | |||
| 910e74b497 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 904
|
||||
versionName "0.0.9.4"
|
||||
versionCode 906
|
||||
versionName "0.0.9.6"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.0.9.4",
|
||||
"version": "0.0.9.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -890,6 +890,7 @@ const ChatScreen: React.FC = () => {
|
||||
// Alle Pending Anhaenge + Text senden
|
||||
const sendPendingAttachments = useCallback(async (messageText: string) => {
|
||||
if (pendingAttachments.length === 0) return;
|
||||
console.log('[Chat] sendPendingAttachments: %d Anhang/Anhaenge', pendingAttachments.length);
|
||||
const location = await getCurrentLocation();
|
||||
const msgId = nextId();
|
||||
|
||||
@@ -939,6 +940,8 @@ const ChatScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
// An RVS senden
|
||||
console.log('[Chat] sende file: name=%s mime=%s size=%s b64Bytes=%s',
|
||||
name, mimeType, file.size, base64.length);
|
||||
rvs.send('file', {
|
||||
name,
|
||||
type: mimeType,
|
||||
|
||||
@@ -347,19 +347,65 @@ class AudioService {
|
||||
this._releaseFocusDeferred();
|
||||
}
|
||||
|
||||
/** TTS-Wiedergabe haart stoppen — z.B. wenn ein Anruf reinkommt.
|
||||
* Released auch sofort den AudioFocus damit der Anruf-Klingelton hoerbar ist. */
|
||||
/** TTS-Wiedergabe haart stoppen — z.B. fuer Barge-In. Buffer wird geleert,
|
||||
* kein Auto-Resume. Released auch sofort den AudioFocus. */
|
||||
haltAllPlayback(reason: string = ''): void {
|
||||
console.log('[Audio] haltAllPlayback: %s', reason || '(no reason)');
|
||||
this._conversationFocusActive = false;
|
||||
this.stopPlayback();
|
||||
}
|
||||
|
||||
/** Speziell fuer Anrufe: AudioTrack stoppen + Focus releasen, ABER pcm-
|
||||
* Buffer + messageId behalten damit weitere Chunks der unterbrochenen
|
||||
* Antwort weiter gesammelt werden. isFinal schreibt dann die WAV trotz
|
||||
* Anruf — und resumeFromInterruption findet sie. */
|
||||
pauseForCall(reason: string = ''): void {
|
||||
console.log('[Audio] pauseForCall: %s', reason || '(no reason)');
|
||||
this._conversationFocusActive = false;
|
||||
this._pausedForCall = true;
|
||||
// Foreground-Service stoppen — Notification waere sonst irrefuehrend
|
||||
stopBackgroundAudio().catch(() => {});
|
||||
// SoundPool/RNSound (Resume-Sound, Play-Button) stoppen — nicht relevant fuer Auto-Resume
|
||||
if (this.currentSound) {
|
||||
try { this.currentSound.stop(); this.currentSound.release(); } catch {}
|
||||
this.currentSound = null;
|
||||
}
|
||||
if (this.resumeSound) {
|
||||
try { this.resumeSound.stop(); this.resumeSound.release(); } catch {}
|
||||
this.resumeSound = null;
|
||||
}
|
||||
// AudioTrack hart stoppen damit nichts mehr aus dem Lautsprecher kommt.
|
||||
// pcmStreamActive bleibt true, pcmBuffer/pcmMessageId BLEIBEN — damit
|
||||
// weitere Chunks gesammelt werden und isFinal die WAV schreiben kann.
|
||||
PcmStreamPlayer?.stop().catch(() => {});
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.release().catch(() => {});
|
||||
}
|
||||
|
||||
/** Anruf vorbei → weitere Chunks duerfen wieder abgespielt werden.
|
||||
* resumeFromInterruption uebernimmt die Wiedergabe ab gemerkter Position. */
|
||||
endCallPause(): void {
|
||||
if (!this._pausedForCall) return;
|
||||
this._pausedForCall = false;
|
||||
console.log('[Audio] endCallPause');
|
||||
}
|
||||
|
||||
/** Bei Anruf: aktuelle Wiedergabe-Position merken damit wir nach dem
|
||||
* Auflegen von dort weitermachen koennen. Returnt Position in Sekunden
|
||||
* oder 0 wenn nichts spielte. */
|
||||
* oder 0 wenn nichts spielte.
|
||||
*
|
||||
* Idempotent: bei mehrfachem Aufruf (ringing → offhook) wird die Position
|
||||
* vom ersten Mal NICHT ueberschrieben. playbackStartTime laeuft stumpf
|
||||
* weiter obwohl das Audio gestoppt ist — der erste Halt ist der echte. */
|
||||
captureInterruption(): number {
|
||||
if (this.pausedMessageId) {
|
||||
console.log('[Audio] captureInterruption: bereits erfasst (msgId=%s pos=%ss) — skip',
|
||||
this.pausedMessageId, this.pausedPosition.toFixed(2));
|
||||
return this.pausedPosition;
|
||||
}
|
||||
if (!this.playbackStartTime || !this.currentPlaybackMsgId) {
|
||||
console.log('[Audio] captureInterruption: nichts spielte (startTime=%s, msgId=%s)',
|
||||
this.playbackStartTime, this.currentPlaybackMsgId || '(leer)');
|
||||
this.pausedPosition = 0;
|
||||
this.pausedMessageId = '';
|
||||
return 0;
|
||||
@@ -379,7 +425,12 @@ class AudioService {
|
||||
async resumeFromInterruption(maxWaitMs: number = 30000): Promise<boolean> {
|
||||
const msgId = this.pausedMessageId;
|
||||
const position = this.pausedPosition;
|
||||
if (!msgId) return false;
|
||||
if (!msgId) {
|
||||
console.log('[Audio] resumeFromInterruption: kein gemerkter Stand — skip');
|
||||
return false;
|
||||
}
|
||||
console.log('[Audio] resumeFromInterruption: starte fuer msgId=%s pos=%ss',
|
||||
msgId, position.toFixed(2));
|
||||
this.pausedMessageId = ''; // konsumieren
|
||||
const cachePath = `${RNFS.DocumentDirectoryPath}/tts_cache/${msgId}.wav`;
|
||||
const startTime = Date.now();
|
||||
@@ -413,6 +464,14 @@ class AudioService {
|
||||
this._firePlaybackStarted();
|
||||
this.isPlaying = true;
|
||||
this.resumeSound = sound;
|
||||
// Tracking auch fuer den Resume-Sound aktualisieren — sonst kann
|
||||
// captureInterruption bei einem zweiten Anruf die Position nicht
|
||||
// mehr ermitteln (playbackStartTime waere von der ersten Wiedergabe).
|
||||
const msgIdMatch = path.match(/([0-9a-f-]+)\.wav$/i);
|
||||
if (msgIdMatch) this.currentPlaybackMsgId = msgIdMatch[1];
|
||||
// Virtuelle Start-Zeit so setzen, dass captureInterruption (das den
|
||||
// Leading-Silence-Offset wieder abzieht) die korrekte Position liefert.
|
||||
this.playbackStartTime = Date.now() - (positionSec + this.LEADING_SILENCE_SEC) * 1000;
|
||||
console.log('[Audio] Resume von Position %ss aus %s',
|
||||
positionSec.toFixed(2), path);
|
||||
sound.setCurrentTime(Math.max(0, positionSec));
|
||||
@@ -788,7 +847,9 @@ class AudioService {
|
||||
}): Promise<string> {
|
||||
// Globaler Mute-Flag uebersteuert das per-Call silent — verhindert
|
||||
// Race-Conditions wenn der User zwischen Chunks den Mute-Knopf drueckt.
|
||||
const silent = !!payload.silent || this._muted;
|
||||
// _pausedForCall: AudioTrack ist gestoppt waehrend Anruf — Chunks weiter
|
||||
// sammeln (fuer WAV-Cache), aber NICHT in den Player schicken.
|
||||
const silent = !!payload.silent || this._muted || this._pausedForCall;
|
||||
if (!silent && !PcmStreamPlayer) {
|
||||
console.warn('[Audio] PcmStreamPlayer Native Module nicht verfuegbar');
|
||||
return '';
|
||||
@@ -946,7 +1007,10 @@ class AudioService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Audio aus lokaler Datei (file:// Pfad) in die Queue und abspielen. */
|
||||
/** Audio aus lokaler Datei (file:// Pfad) in die Queue und abspielen.
|
||||
* Setzt zusaetzlich playbackStartTime + currentPlaybackMsgId damit ein
|
||||
* Anruf waehrend dieses Playbacks korrekt erfasst wird (ohne dieses
|
||||
* Tracking liefert captureInterruption nichts → kein Auto-Resume). */
|
||||
async playFromPath(filePath: string): Promise<void> {
|
||||
if (!filePath) return;
|
||||
try {
|
||||
@@ -955,6 +1019,12 @@ class AudioService {
|
||||
console.warn('[Audio] Cache-Datei existiert nicht mehr:', cleanPath);
|
||||
return;
|
||||
}
|
||||
const msgIdMatch = cleanPath.match(/([0-9a-f-]+)\.wav$/i);
|
||||
if (msgIdMatch) {
|
||||
this.currentPlaybackMsgId = msgIdMatch[1];
|
||||
// Start-Zeit so setzen dass elapsed = LEADING (≈ 0 nach captureInterruption-Berechnung)
|
||||
this.playbackStartTime = Date.now() - this.LEADING_SILENCE_SEC * 1000;
|
||||
}
|
||||
const b64 = await RNFS.readFile(cleanPath, 'base64');
|
||||
this.playAudio(b64);
|
||||
} catch (err) {
|
||||
@@ -1074,6 +1144,10 @@ class AudioService {
|
||||
* — die Bridge kann einen Chunk im selben JS-Tick liefern in dem der
|
||||
* User Mute geklickt hat. */
|
||||
private _muted: boolean = false;
|
||||
/** Anruf laeuft → Chunks werden nur in den Cache-Buffer gepusht, nicht
|
||||
* abgespielt. Wird in pauseForCall gesetzt, in endCallPause/resumeFrom-
|
||||
* Interruption zurueckgenommen. */
|
||||
private _pausedForCall: boolean = false;
|
||||
setMuted(muted: boolean): void {
|
||||
this._muted = muted;
|
||||
if (muted) this.stopPlayback();
|
||||
|
||||
@@ -189,12 +189,17 @@ class PhoneCallService {
|
||||
private _haltForCall(toast: string): void {
|
||||
// Position merken bevor wir den Stream killen — fuer Auto-Resume.
|
||||
audioService.captureInterruption();
|
||||
audioService.haltAllPlayback(toast);
|
||||
// pauseForCall (statt haltAllPlayback): pcmBuffer + messageId bleiben,
|
||||
// weitere Chunks werden weiter gesammelt damit isFinal die WAV schreibt.
|
||||
audioService.pauseForCall(toast);
|
||||
wakeWordService.pauseForCall().catch(() => {});
|
||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||
}
|
||||
|
||||
private _resumeAfterCall(toast: string): void {
|
||||
// Anruf-Pause aufheben — neue Chunks duerfen wieder direkt abgespielt
|
||||
// werden (falls die Bridge mid-Anruf isFinal noch nicht geschickt hat).
|
||||
audioService.endCallPause();
|
||||
wakeWordService.resumeFromCall().catch(() => {});
|
||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||
// Auto-Resume: ab gemerkter Position weiterspielen wenn ARIA vor dem
|
||||
|
||||
@@ -1634,6 +1634,11 @@ class ARIABridge:
|
||||
}
|
||||
if audio_request_id:
|
||||
stt_payload["audioRequestId"] = audio_request_id
|
||||
# GPS aus dem Original-Audio-Payload mitgeben — Diagnostic
|
||||
# zeigt sie sonst nicht an (App sendet location nur einmal,
|
||||
# die im audio-Payload). Reine Anzeige-Information.
|
||||
if location:
|
||||
stt_payload["location"] = location
|
||||
ok = await self._send_to_rvs({
|
||||
"type": "chat",
|
||||
"payload": stt_payload,
|
||||
|
||||
@@ -85,6 +85,10 @@ Wichtige Mechanismen:
|
||||
- [x] **App-Resume-Cooldown**: Wechsel von Background → Foreground triggert keinen falschen Wake-Word-Trigger mehr. AppState-Listener setzt 1.5s Cooldown in dem onWakeDetected-Events ignoriert werden (Audio-Pegel-Spike beim AudioFocus-Switch sonst als Wake-Word interpretiert)
|
||||
- [x] Background-Mikro robust: acquireBackgroundAudio('rec'/'wake') wird jetzt VOR AudioRecord.startRecording gerufen — Foreground-Service mit foregroundServiceType=microphone muss aktiv sein bevor das Mikro greift, sonst blockiert Android ab 11+ den Background-Zugriff
|
||||
- [x] **Stille-Pegel manuell setzbar** (Settings → Spracheingabe): Override-Wert in dB von -55 bis -15, default "automatisch". Info-Button mit Modal erklaert die Skala (niedriger = sensibler, hoeher = robuster gegen Hintergrundlaerm). Bei manuell gesetztem Wert wird die adaptive Baseline ignoriert
|
||||
- [x] **Kurze TTS-Texte (1-3 Worte) spielen jetzt ab** — auf OnePlus A12 stallte AudioTrack mit `pos=0` weil der Default-Start-Threshold `bufferSize/2` (= 2s) bei kurzen Streams nie ueberschritten wurde. Fix: `setStartThresholdInFrames(100ms)` direkt nach dem Track-Build (API 31+). Buffer auf 4s entkoppelt von Pre-Roll, `play()` wird beim allerersten data-chunk gerufen
|
||||
- [x] **Mute-Button stoppt jetzt auch laufenden PCM-Stream** — `pcmStreamActive` wurde beim isFinal-Chunk schon false gesetzt, der AudioTrack spielte aber noch sekundenlang aus seinem Buffer. `stopPlayback()` uebersprang darum `PcmStreamPlayer.stop()`. Fix: stop() immer rufen (ist idempotent), kein Flag-Check mehr
|
||||
- [x] **GPS-Permission im Manifest + Runtime-Request** beim Settings-Toggle — vorher fehlten ACCESS_COARSE_LOCATION / ACCESS_FINE_LOCATION komplett. `Geolocation.getCurrentPosition` schlug lautlos fehl, App sendete nie ein location-Feld
|
||||
- [x] **GPS-Position auch im STT-Payload an Diagnostic** — die App sendet location einmal im audio-Payload. Die Bridge nutzte sie zwar (ging in aria-core's Kontext rein), reichte sie aber nicht im STT-broadcast an Diagnostic durch. Diagnostic zeigte darum bei Spracheingaben nie den GPS-Block, obwohl der "GPS einblenden"-Toggle aktiv war
|
||||
|
||||
### App Features
|
||||
|
||||
|
||||
Reference in New Issue
Block a user