Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
761217cb5a | ||
|
|
ff6d31acbd | ||
|
|
2d7a5784c2 | ||
|
|
fa871219ae |
@@ -10,6 +10,27 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.2.1.5] — 2026-07-12 — Pro-Projekt-Queue mit Rückfrage-Loop
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
**Nachrichten-Queue pro Projekt (App + Diagnostic)**
|
||||||
|
- Eine zweite Nachricht, während ARIA am aktuellen Task arbeitet, wird jetzt **angestellt** statt den laufenden Task abzubrechen (vorher: Barge-In-Cancel). Sie läuft der Reihe nach, **pro Projekt unabhängig** (paralleles Arbeiten in mehreren Projekten bleibt). Wartende Nachrichten zeigen als ⏸-Bubble — tippen entfernt sie aus der Warteschlange.
|
||||||
|
- **Rückfrage-Loop:** Stellt ARIA eine echte, blockierende Rückfrage, **pausiert** die Queue und deine nächste Eingabe beantwortet sie — bis eine finale Antwort kommt, dann läuft der nächste Queue-Eintrag (gleiches Muster). Banner „❓ ARIA fragt nach — deine Eingabe beantwortet das". Der Stop-Button bricht den aktuellen Task ab und schaltet zum nächsten.
|
||||||
|
- ARIA signalisiert eine Rückfrage über einen **unsichtbaren `[[AWAIT]]`-Marker** — wie speak/converse deklariert das Modell den Zustand selbst (kein „endet-mit-?"-Raten). Brain strippt ihn, gibt `awaiting_reply` durch `chat()` → `ChatOut` → Bridge-Chat-Payload. Local (tool-los) und Fast-Path markieren nie.
|
||||||
|
|
||||||
|
**Pro-Projekt-Textfeld-Entwürfe (App + Diagnostic)**
|
||||||
|
- Der Feldinhalt bleibt beim Projektwechsel erhalten: in Projekt X tippen, zu Y wechseln (leeres Feld), zurück zu X → dein Entwurf steht wieder da. In Storage persistiert.
|
||||||
|
|
||||||
|
**TTS-Abspiel-Queue (App)**
|
||||||
|
- Zwei fast gleichzeitig fertige Antworten sprechen jetzt garantiert **nacheinander** statt sich gegenseitig abzuschneiden. Vorher war das Timing-Glück (`PcmStreamPlayer.start()` ruft `stopInternal()` = flush/release, hätte die laufende gecuttet). Jetzt: „spielt hörbar" gilt bis zum echten `PcmPlaybackFinished` (nicht nur bis Stream-Ende); eine neue hörbare Antwort, die währenddessen ankommt, wird gepuffert und danach nachgespielt (Kette für 3, 4, …). Harter Stop/Barge-In/Mund-Button verwirft die Queue.
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- **Voice bricht nicht mehr ab:** eine neue Sprachnachricht während ARIA arbeitet stoppt nur akustisch das TTS (sauberes Mikro) und wird über den Brain-Projekt-Lock serialisiert, statt den laufenden Task abzubrechen (passend zu „immer anstellen + Stop-Button"). Text-Senden erkennt Brain-busy als Fallback, damit auch nach einem voice-gestarteten Turn korrekt angestellt wird. Grenze: eine per Sprache gestartete Aufgabe erscheint nicht als löschbare ⏸-Bubble (Aufnahme wird live gestreamt, nicht app-seitig gepuffert).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.2.1.4] — 2026-07-12 — Lokales LLM: der ehrliche Rückbau
|
## [0.2.1.4] — 2026-07-12 — Lokales LLM: der ehrliche Rückbau
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|||||||
@@ -474,7 +474,9 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
|||||||
- **Gespraechsmodus** (Ohr-Button): Nach jeder ARIA-Antwort startet automatisch die Aufnahme — wie ein natuerliches Gespraech hin und her
|
- **Gespraechsmodus** (Ohr-Button): Nach jeder ARIA-Antwort startet automatisch die Aufnahme — wie ein natuerliches Gespraech hin und her
|
||||||
- **Wake-Word** (on-device, openWakeWord ONNX): "Hey Jarvis", "Alexa", "Hey Mycroft", "Hey Rhasspy" — Mikrofon hoert passiv mit, Konversation startet beim Schluesselwort. Komplett on-device via ONNX Runtime, kein API-Key, kein Cloud-Roundtrip, Audio verlaesst das Geraet nicht.
|
- **Wake-Word** (on-device, openWakeWord ONNX): "Hey Jarvis", "Alexa", "Hey Mycroft", "Hey Rhasspy" — Mikrofon hoert passiv mit, Konversation startet beim Schluesselwort. Komplett on-device via ONNX Runtime, kein API-Key, kein Cloud-Roundtrip, Audio verlaesst das Geraet nicht.
|
||||||
- **VAD (Voice Activity Detection)**: Adaptive Schwelle (Baseline aus ersten 500ms Mic-Pegel + 6dB Offset). Konfigurierbare Stille-Toleranz (1.0–8.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme einstellbar (1–30 min, Default 5 min)
|
- **VAD (Voice Activity Detection)**: Adaptive Schwelle (Baseline aus ersten 500ms Mic-Pegel + 6dB Offset). Konfigurierbare Stille-Toleranz (1.0–8.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme einstellbar (1–30 min, Default 5 min)
|
||||||
- **Barge-In**: Wenn du waehrend ARIAs Antwort eine neue Sprach-/Text-Nachricht reinschickst, wird sie unterbrochen + bekommt den Hint "das ist eine Korrektur"
|
- **Nachricht anstellen statt abbrechen** (Queue pro Projekt): Schickst du eine zweite Nachricht waehrend ARIA noch am aktuellen Task arbeitet, wird sie **angestellt** statt den laufenden abzubrechen — laeuft der Reihe nach, pro Projekt unabhaengig. Wartende zeigen als `⏸`-Bubble (tippen entfernt sie aus der Warteschlange). Explizites Abbrechen laeuft ueber den Stop-Button am „ARIA denkt". Eine neue Sprachnachricht stoppt nur akustisch das TTS (sauberes Mikro), bricht die laufende Arbeit aber nicht mehr ab
|
||||||
|
- **Rueckfrage-Loop**: Stellt ARIA eine echte, blockierende Rueckfrage, **pausiert** die Queue und deine naechste Eingabe beantwortet sie — bis eine finale Antwort kommt, dann laeuft der naechste Queue-Eintrag (gleiches Muster). Banner „❓ ARIA fragt nach — deine Eingabe beantwortet das". ARIA signalisiert das ueber einen unsichtbaren `[[AWAIT]]`-Marker im Antworttext (das Modell deklariert den Zustand selbst, kein „endet-mit-?"-Raten; Brain strippt ihn und gibt `awaiting_reply` an App + Diagnostic durch)
|
||||||
|
- **Pro-Projekt-Textfeld-Entwuerfe**: Der Feldinhalt bleibt beim Projektwechsel erhalten — in Projekt X tippen, zu Y wechseln (leeres Feld), zurueck zu X → dein Entwurf steht wieder da. Persistiert ueber Neustart. Gleiches Verhalten im Diagnostic
|
||||||
- **Wake-Word waehrend TTS**: Du kannst "Computer" sagen waehrend ARIA noch redet — AcousticEchoCanceler verhindert dass ARIAs eigene Stimme das Wake-Word triggert
|
- **Wake-Word waehrend TTS**: Du kannst "Computer" sagen waehrend ARIA noch redet — AcousticEchoCanceler verhindert dass ARIAs eigene Stimme das Wake-Word triggert
|
||||||
- **Anruf-Pause + Auto-Resume**: TTS verstummt bei klassischem Anruf oder VoIP-Call (WhatsApp/Signal/Discord). Nach dem Auflegen geht ARIA von der **genauen Stelle** weiter wo sie unterbrochen wurde — die App misst die Position vom Wiedergabe-Anfang und nutzt den WAV-Cache der Antwort
|
- **Anruf-Pause + Auto-Resume**: TTS verstummt bei klassischem Anruf oder VoIP-Call (WhatsApp/Signal/Discord). Nach dem Auflegen geht ARIA von der **genauen Stelle** weiter wo sie unterbrochen wurde — die App misst die Position vom Wiedergabe-Anfang und nutzt den WAV-Cache der Antwort
|
||||||
- **Speech Gate**: Aufnahme wird verworfen wenn keine Sprache erkannt
|
- **Speech Gate**: Aufnahme wird verworfen wenn keine Sprache erkannt
|
||||||
@@ -482,6 +484,7 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
|||||||
- **"ARIA denkt..." Indicator**: Zeigt live den Status vom Core (Denken, Tool, Schreiben) + Abbrechen-Button
|
- **"ARIA denkt..." Indicator**: Zeigt live den Status vom Core (Denken, Tool, Schreiben) + Abbrechen-Button
|
||||||
- **TTS-Wiedergabe**: F5-TTS PCM-Streaming direkt in AudioTrack mit konfigurierbarem Pre-Roll-Buffer (1.0–6.0s, Default 3.5s) gegen Gaps bei Render-Pausen
|
- **TTS-Wiedergabe**: F5-TTS PCM-Streaming direkt in AudioTrack mit konfigurierbarem Pre-Roll-Buffer (1.0–6.0s, Default 3.5s) gegen Gaps bei Render-Pausen
|
||||||
- **Audio-Pause**: Andere Apps (Spotify, YouTube etc.) pausieren komplett waehrend ARIA spricht und kommen erst wieder nach echtem Wiedergabe-Ende
|
- **Audio-Pause**: Andere Apps (Spotify, YouTube etc.) pausieren komplett waehrend ARIA spricht und kommen erst wieder nach echtem Wiedergabe-Ende
|
||||||
|
- **TTS-Abspiel-Queue**: Zwei fast gleichzeitig fertige Antworten sprechen garantiert **nacheinander** statt sich abzuschneiden — eine neue hoerbare Antwort, die waehrend der Wiedergabe einer anderen ankommt, wird gepuffert und erst nach deren echtem Wiedergabe-Ende (`PcmPlaybackFinished`, nicht nur Stream-Ende) nachgespielt. Harter Stop / Barge-In / Mund-Button verwirft die Queue
|
||||||
- **Lokale Voice-Wahl**: Pro Geraet eigene Stimme moeglich (in Settings). Diagnostic-Wechsel ueberschreibt alle App-Wahlen.
|
- **Lokale Voice-Wahl**: Pro Geraet eigene Stimme moeglich (in Settings). Diagnostic-Wechsel ueberschreibt alle App-Wahlen.
|
||||||
- **Voice-Ready Toast**: Beim Wechsel zeigt die App "Stimme X bereit (X.Ys)" sobald der Preload durch ist
|
- **Voice-Ready Toast**: Beim Wechsel zeigt die App "Stimme X bereit (X.Ys)" sobald der Preload durch ist
|
||||||
- **Play-Button**: Jede ARIA-Nachricht kann nochmal vorgelesen werden (aus Cache wenn vorhanden, sonst neu rendern)
|
- **Play-Button**: Jede ARIA-Nachricht kann nochmal vorgelesen werden (aus Cache wenn vorhanden, sonst neu rendern)
|
||||||
@@ -994,6 +997,9 @@ docker exec aria-brain curl localhost:8080/memory/stats
|
|||||||
- [x] Anruf-Pause + Auto-Resume: TTS verstummt bei Anruf, faehrt nach Auflegen ab der gemerkten Position fort (Date.now()-Tracking + WAV-Cache der Antwort)
|
- [x] Anruf-Pause + Auto-Resume: TTS verstummt bei Anruf, faehrt nach Auflegen ab der gemerkten Position fort (Date.now()-Tracking + WAV-Cache der Antwort)
|
||||||
- [x] PcmPlaybackFinished-Event: AudioFocus wird erst released wenn AudioTrack wirklich durch ist — kein Spotify-mid-TTS mehr
|
- [x] PcmPlaybackFinished-Event: AudioFocus wird erst released wenn AudioTrack wirklich durch ist — kein Spotify-mid-TTS mehr
|
||||||
- [x] Edge-Case: neue Frage waehrend Telefonat verwirft pending Auto-Resume, neueste Antwort gewinnt
|
- [x] Edge-Case: neue Frage waehrend Telefonat verwirft pending Auto-Resume, neueste Antwort gewinnt
|
||||||
|
- [x] **Pro-Projekt-Nachrichten-Queue** (loest das alte Barge-In-Cancel ab): zweite Nachricht wird **angestellt** statt den laufenden Task abzubrechen; **Rueckfrage-Loop** via unsichtbarem `[[AWAIT]]`-Marker (Queue pausiert, naechste Eingabe beantwortet die Rueckfrage); Stop-Button schaltet zum naechsten; sichtbare `⏸`-Bubbles (loeschbar). Pro Projekt unabhaengig, App + Diagnostic
|
||||||
|
- [x] Pro-Projekt-Textfeld-Entwuerfe (Feldinhalt bleibt beim Projektwechsel erhalten, persistiert; App + Diagnostic)
|
||||||
|
- [x] **TTS-Abspiel-Queue**: zwei fast gleichzeitig fertige Antworten sprechen garantiert nacheinander statt sich abzuschneiden (Puffern bis `PcmPlaybackFinished` der laufenden)
|
||||||
- [x] Settings-Sub-Screens: 8 Kategorien statt langer Liste
|
- [x] Settings-Sub-Screens: 8 Kategorien statt langer Liste
|
||||||
- [x] APK ABI-Split arm64-v8a: 35 MB statt 136 MB
|
- [x] APK ABI-Split arm64-v8a: 35 MB statt 136 MB
|
||||||
- [x] Sprachnachrichten-Bubble: audioRequestId statt Substring-Match — keine vertauschten Bubbles mehr bei parallelen Aufnahmen
|
- [x] Sprachnachrichten-Bubble: audioRequestId statt Substring-Match — keine vertauschten Bubbles mehr bei parallelen Aufnahmen
|
||||||
|
|||||||
@@ -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 20105
|
versionCode 20106
|
||||||
versionName "0.2.1.5"
|
versionName "0.2.1.6"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.2.1.5",
|
"version": "0.2.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -1731,6 +1731,13 @@ const ChatScreen: React.FC = () => {
|
|||||||
// das Mikro greifen kann.
|
// das Mikro greifen kann.
|
||||||
wakeWordService.stopBargeListening().catch(() => {});
|
wakeWordService.stopBargeListening().catch(() => {});
|
||||||
});
|
});
|
||||||
|
// Aus der TTS-Queue nachgespielte (zweite) Antwort: ihren WAV-Cache-Pfad an
|
||||||
|
// die Bubble haengen, damit der Mund-Button/Play sie auch abspielen kann.
|
||||||
|
const unsubPcmCached = audioService.onPcmCached((messageId, audioPath) => {
|
||||||
|
if (!messageId || !audioPath) return;
|
||||||
|
setMessages(prev => prev.map(m =>
|
||||||
|
m.messageId === messageId ? { ...m, audioPath } : m));
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubWake();
|
unsubWake();
|
||||||
@@ -1739,6 +1746,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
unsubPassive();
|
unsubPassive();
|
||||||
unsubTtsStart();
|
unsubTtsStart();
|
||||||
unsubTtsEnd();
|
unsubTtsEnd();
|
||||||
|
unsubPcmCached();
|
||||||
};
|
};
|
||||||
}, [wakeWordActive]);
|
}, [wakeWordActive]);
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,13 @@ class AudioService {
|
|||||||
private pcmSampleRate: number = 24000;
|
private pcmSampleRate: number = 24000;
|
||||||
private pcmChannels: number = 1;
|
private pcmChannels: number = 1;
|
||||||
private pcmBuffer: string[] = []; // base64-chunks zum spaeteren WAV-Build
|
private pcmBuffer: string[] = []; // base64-chunks zum spaeteren WAV-Build
|
||||||
|
// ── TTS-Abspiel-Queue: zwei back-to-back-Antworten sollen sich NICHT
|
||||||
|
// gegenseitig abschneiden. Eine neue hoerbare Antwort, die reinkommt waehrend
|
||||||
|
// eine andere noch HOERBAR spielt, wird gepuffert und nach PcmPlaybackFinished
|
||||||
|
// nachgespielt (statt via start()→stopInternal() die laufende zu cutten). ──
|
||||||
|
private pcmAudiblePlaying: boolean = false; // eine hoerbare Antwort spielt (bis PcmPlaybackFinished)
|
||||||
|
private pcmPlayingMsgId: string = ''; // deren messageId
|
||||||
|
private pcmPendingStreams: Array<{ messageId: string; sampleRate: number; channels: number; chunks: string[]; final: boolean }> = [];
|
||||||
private pcmBytesCollected: number = 0;
|
private pcmBytesCollected: number = 0;
|
||||||
private readonly PCM_MAX_CACHE_BYTES = 30 * 1024 * 1024; // 30MB
|
private readonly PCM_MAX_CACHE_BYTES = 30 * 1024 * 1024; // 30MB
|
||||||
|
|
||||||
@@ -374,6 +381,16 @@ class AudioService {
|
|||||||
const emitter = new NativeEventEmitter(NativeModules.PcmStreamPlayer as any);
|
const emitter = new NativeEventEmitter(NativeModules.PcmStreamPlayer as any);
|
||||||
emitter.addListener('PcmPlaybackFinished', () => {
|
emitter.addListener('PcmPlaybackFinished', () => {
|
||||||
console.log('[Audio] PcmPlaybackFinished — AudioTrack drained');
|
console.log('[Audio] PcmPlaybackFinished — AudioTrack drained');
|
||||||
|
this.pcmAudiblePlaying = false;
|
||||||
|
this.pcmPlayingMsgId = '';
|
||||||
|
// TTS-Abspiel-Queue: steht eine naechste Antwort bereit? Dann NICHT
|
||||||
|
// "fertig" melden (kein Wake-Word-Re-Arm / Conversation-Ende) — ARIA
|
||||||
|
// spricht gleich weiter. Die naechste gepufferte Antwort direkt spielen.
|
||||||
|
if (this.pcmPendingStreams.length > 0) {
|
||||||
|
this._promoteNextPendingStream().catch(err =>
|
||||||
|
console.warn('[Audio] promote next pending stream err:', err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._releaseFocusDeferred();
|
this._releaseFocusDeferred();
|
||||||
// Erst HIER playbackFinished-Listener feuern — nicht schon beim
|
// Erst HIER playbackFinished-Listener feuern — nicht schon beim
|
||||||
// Empfang des letzten PCM-Chunks (siehe handlePcmChunk). AudioTrack
|
// Empfang des letzten PCM-Chunks (siehe handlePcmChunk). AudioTrack
|
||||||
@@ -1401,6 +1418,23 @@ class AudioService {
|
|||||||
const base64 = payload.base64 || '';
|
const base64 = payload.base64 || '';
|
||||||
const isFinal = !!payload.final;
|
const isFinal = !!payload.final;
|
||||||
|
|
||||||
|
// ── TTS-Abspiel-Queue ──
|
||||||
|
// Kommt eine NEUE hoerbare Antwort rein, waehrend eine andere noch hoerbar
|
||||||
|
// spielt? Dann NICHT starten (start()→stopInternal() wuerde die laufende
|
||||||
|
// abschneiden) — puffern und nach deren PcmPlaybackFinished nachspielen.
|
||||||
|
if (!silent && this.pcmAudiblePlaying && messageId && messageId !== this.pcmPlayingMsgId) {
|
||||||
|
let entry = this.pcmPendingStreams.find(e => e.messageId === messageId);
|
||||||
|
if (!entry) {
|
||||||
|
entry = { messageId, sampleRate, channels, chunks: [], final: false };
|
||||||
|
this.pcmPendingStreams.push(entry);
|
||||||
|
console.log('[Audio] TTS-Queue: Antwort %s wird gepuffert (spielt gerade %s)',
|
||||||
|
messageId, this.pcmPlayingMsgId);
|
||||||
|
}
|
||||||
|
if (base64) entry.chunks.push(base64);
|
||||||
|
if (isFinal) entry.final = true;
|
||||||
|
return ''; // Live-Player nicht anfassen
|
||||||
|
}
|
||||||
|
|
||||||
// Neuer Stream? (messageId Wechsel oder nicht aktiv)
|
// Neuer Stream? (messageId Wechsel oder nicht aktiv)
|
||||||
if (!this.pcmStreamActive || this.pcmMessageId !== messageId) {
|
if (!this.pcmStreamActive || this.pcmMessageId !== messageId) {
|
||||||
if (this.pcmStreamActive && !silent) {
|
if (this.pcmStreamActive && !silent) {
|
||||||
@@ -1448,6 +1482,8 @@ class AudioService {
|
|||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
AudioFocus?.requestDuck().catch(() => {});
|
AudioFocus?.requestDuck().catch(() => {});
|
||||||
this._firePlaybackStarted();
|
this._firePlaybackStarted();
|
||||||
|
this.pcmAudiblePlaying = true;
|
||||||
|
this.pcmPlayingMsgId = messageId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,6 +1526,73 @@ class AudioService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Naechste gepufferte TTS-Antwort abspielen (TTS-Abspiel-Queue). Wird nach
|
||||||
|
* PcmPlaybackFinished der vorherigen aufgerufen — so sprechen zwei
|
||||||
|
* back-to-back-Antworten NACHEINANDER statt sich abzuschneiden. */
|
||||||
|
private async _promoteNextPendingStream(): Promise<void> {
|
||||||
|
const entry = this.pcmPendingStreams.shift();
|
||||||
|
if (!entry) return;
|
||||||
|
// Inzwischen global gemutet / im Anruf / vom User gestoppt? Dann NICHT
|
||||||
|
// hoerbar abspielen — nur cachen und die naechste promoten.
|
||||||
|
const mutedNow = this._muted || this._pausedForCall ||
|
||||||
|
(!!this._stoppedMessageId && this._stoppedMessageId === entry.messageId);
|
||||||
|
console.log('[Audio] TTS-Queue: spiele gepufferte Antwort %s (%d chunks, final=%s, muted=%s)',
|
||||||
|
entry.messageId, entry.chunks.length, entry.final, mutedNow);
|
||||||
|
// SOFORT als "spielt" markieren (vor jedem await) — sonst koennte ein
|
||||||
|
// gleichzeitig eintreffender Chunk einer DRITTEN Antwort in der await-Luecke
|
||||||
|
// einen konkurrierenden Stream starten statt zu puffern.
|
||||||
|
this.pcmPlayingMsgId = entry.messageId;
|
||||||
|
this.pcmAudiblePlaying = !mutedNow;
|
||||||
|
// Cache-State fuer den WAV-Build (Mund-Button-Replay) setzen.
|
||||||
|
this.pcmMessageId = entry.messageId;
|
||||||
|
this.pcmSampleRate = entry.sampleRate;
|
||||||
|
this.pcmChannels = entry.channels;
|
||||||
|
this.pcmBuffer = entry.chunks.slice();
|
||||||
|
this.pcmBytesCollected = entry.chunks.reduce((n, c) => n + Math.floor(c.length * 0.75), 0);
|
||||||
|
this.pcmStreamActive = true;
|
||||||
|
if (!mutedNow && PcmStreamPlayer) {
|
||||||
|
try {
|
||||||
|
const prerollSec = await loadPrerollSec();
|
||||||
|
await PcmStreamPlayer.start(entry.sampleRate, entry.channels, prerollSec);
|
||||||
|
this._cancelDeferredFocusRelease();
|
||||||
|
AudioFocus?.requestDuck().catch(() => {});
|
||||||
|
this._firePlaybackStarted();
|
||||||
|
this.pcmAudiblePlaying = true;
|
||||||
|
this.pcmPlayingMsgId = entry.messageId;
|
||||||
|
for (const c of entry.chunks) {
|
||||||
|
try { await PcmStreamPlayer.writeChunk(c); } catch (err) { console.warn('[Audio] promote writeChunk', err); }
|
||||||
|
}
|
||||||
|
if (entry.final) { try { await PcmStreamPlayer.end(); } catch {} }
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Audio] TTS-Queue promote start fehlgeschlagen:', err);
|
||||||
|
this.pcmAudiblePlaying = false;
|
||||||
|
this.pcmPlayingMsgId = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// War die Antwort schon komplett (final) da: WAV cachen + State wie im
|
||||||
|
// Normalpfad zuruecksetzen. Bei NICHT-final laeuft der Rest live ueber
|
||||||
|
// _handlePcmChunkImpl (messageId == pcmPlayingMsgId → Normalpfad).
|
||||||
|
if (entry.final) {
|
||||||
|
this.pcmStreamActive = false;
|
||||||
|
if (this.pcmBuffer.length > 0) {
|
||||||
|
const audioPath = await this._savePcmBufferAsWav(entry.messageId).catch(() => '');
|
||||||
|
if (audioPath) {
|
||||||
|
this.pcmCachedListeners.forEach(cb => {
|
||||||
|
try { cb(entry.messageId, audioPath); } catch (e) { console.warn('[Audio] pcmCached cb err:', e); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pcmBuffer = [];
|
||||||
|
this.pcmBytesCollected = 0;
|
||||||
|
this.pcmMessageId = '';
|
||||||
|
// Nicht hoerbar abgespielt (gemutet)? Dann feuert PcmPlaybackFinished nicht
|
||||||
|
// → die naechste gepufferte Antwort selbst nachziehen (Kette).
|
||||||
|
if (!this.pcmAudiblePlaying) {
|
||||||
|
await this._promoteNextPendingStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Gesammelte PCM-Chunks als WAV speichern. Gibt file:// Pfad zurueck. */
|
/** Gesammelte PCM-Chunks als WAV speichern. Gibt file:// Pfad zurueck. */
|
||||||
private async _savePcmBufferAsWav(messageId: string): Promise<string> {
|
private async _savePcmBufferAsWav(messageId: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
@@ -1578,6 +1681,19 @@ class AudioService {
|
|||||||
// Callback wenn alle Audio-Teile abgespielt sind
|
// Callback wenn alle Audio-Teile abgespielt sind
|
||||||
private playbackFinishedListeners: (() => void)[] = [];
|
private playbackFinishedListeners: (() => void)[] = [];
|
||||||
private playbackStartedListeners: (() => void)[] = [];
|
private playbackStartedListeners: (() => void)[] = [];
|
||||||
|
// Feuert wenn eine aus der TTS-Queue NACHgespielte Antwort ihren WAV-Cache
|
||||||
|
// geschrieben hat — der Normalpfad meldet den Pfad ueber den handlePcmChunk-
|
||||||
|
// Rueckgabewert, gepufferte (zweite) Antworten koennen das aber nicht (ihre
|
||||||
|
// Chunks returnen '' waehrend sie warten). Damit setzt die App auch fuer die
|
||||||
|
// nachgespielte Antwort m.audioPath (Mund-Button-Replay).
|
||||||
|
private pcmCachedListeners: Array<(messageId: string, audioPath: string) => void> = [];
|
||||||
|
|
||||||
|
onPcmCached(callback: (messageId: string, audioPath: string) => void): () => void {
|
||||||
|
this.pcmCachedListeners.push(callback);
|
||||||
|
return () => {
|
||||||
|
this.pcmCachedListeners = this.pcmCachedListeners.filter(cb => cb !== callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onPlaybackFinished(callback: () => void): () => void {
|
onPlaybackFinished(callback: () => void): () => void {
|
||||||
this.playbackFinishedListeners.push(callback);
|
this.playbackFinishedListeners.push(callback);
|
||||||
@@ -1759,6 +1875,10 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
// AudioTrack IMMER hart stoppen (idempotent) — auch im Drain-Fall.
|
// AudioTrack IMMER hart stoppen (idempotent) — auch im Drain-Fall.
|
||||||
PcmStreamPlayer?.stop().catch(() => {});
|
PcmStreamPlayer?.stop().catch(() => {});
|
||||||
|
// Wartende TTS-Antworten verwerfen (Mund-Button = still sein).
|
||||||
|
this.pcmPendingStreams = [];
|
||||||
|
this.pcmAudiblePlaying = false;
|
||||||
|
this.pcmPlayingMsgId = '';
|
||||||
stopBackgroundAudio().catch(() => {});
|
stopBackgroundAudio().catch(() => {});
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
AudioFocus?.release().catch(() => {});
|
AudioFocus?.release().catch(() => {});
|
||||||
@@ -1770,7 +1890,8 @@ class AudioService {
|
|||||||
// Kick-Cycle anstossen — Re-Renders triggern setMuted oft mehrfach hinter-
|
// Kick-Cycle anstossen — Re-Renders triggern setMuted oft mehrfach hinter-
|
||||||
// einander, und jeder weitere Kick lässt Spotify nochmal kurz pausieren.
|
// einander, und jeder weitere Kick lässt Spotify nochmal kurz pausieren.
|
||||||
const hasAnything = !!(this.currentSound || this.resumeSound || this.preloadedSound
|
const hasAnything = !!(this.currentSound || this.resumeSound || this.preloadedSound
|
||||||
|| this.pcmStreamActive || this.audioQueue.length || this.isPlaying);
|
|| this.pcmStreamActive || this.audioQueue.length || this.isPlaying
|
||||||
|
|| this.pcmPendingStreams.length);
|
||||||
if (!hasAnything) return;
|
if (!hasAnything) return;
|
||||||
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
|
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
|
||||||
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
|
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
|
||||||
@@ -1804,6 +1925,11 @@ class AudioService {
|
|||||||
this.pcmBuffer = [];
|
this.pcmBuffer = [];
|
||||||
this.pcmBytesCollected = 0;
|
this.pcmBytesCollected = 0;
|
||||||
this.pcmMessageId = '';
|
this.pcmMessageId = '';
|
||||||
|
// TTS-Abspiel-Queue verwerfen — harter Stop/Abbruch/Barge-In soll auch
|
||||||
|
// wartende Antworten fallenlassen (sonst sprechen sie nach dem Stop weiter).
|
||||||
|
this.pcmPendingStreams = [];
|
||||||
|
this.pcmAudiblePlaying = false;
|
||||||
|
this.pcmPlayingMsgId = '';
|
||||||
// Audio-Focus sofort freigeben — User hat explizit abgebrochen.
|
// Audio-Focus sofort freigeben — User hat explizit abgebrochen.
|
||||||
// Unser Focus war TRANSIENT, Spotify resumed darum automatisch beim
|
// Unser Focus war TRANSIENT, Spotify resumed darum automatisch beim
|
||||||
// Abandon. Den frueheren kickReleaseMedia haben wir entfernt: er
|
// Abandon. Den frueheren kickReleaseMedia haben wir entfernt: er
|
||||||
|
|||||||
Reference in New Issue
Block a user