feat(audio): Auto-Resume nach Anruf ab der gemerkten Position
Stefans Idee: Position beim Halt merken (Date.now() - playbackStart - leadingSilence), nach dem Auflegen ab da weitermachen. Wenn der Cache noch nicht komplett ist (final-Marker kam waehrend Anruf), warten wir bis zu 30s auf das WAV — meistens ist's schon da weil das Telefonat laenger als die Antwort dauerte. audio.ts: - captureInterruption(): merkt position + messageId, returnt Sekunden - resumeFromInterruption(maxWaitMs): wartet auf WAV-Cache, lädt mit Sound, setCurrentTime(position), play - Tracking-Felder: playbackStartTime, currentPlaybackMsgId, pausedX phoneCall.ts: - _haltForCall ruft captureInterruption() VOR haltAllPlayback - _resumeAfterCall triggert resumeFromInterruption(30s) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,20 @@ class AudioService {
|
||||
private vadAdaptiveSilenceDb: number = VAD_SILENCE_FALLBACK_DB;
|
||||
private vadAdaptiveSpeechDb: number = VAD_SPEECH_FALLBACK_DB;
|
||||
|
||||
// Interruption-Tracking fuer Auto-Resume nach Anruf:
|
||||
// - playbackStartTime: ms-Timestamp wenn AudioTrack tatsaechlich anfing
|
||||
// abzuspielen (= _firePlaybackStarted)
|
||||
// - currentPlaybackMsgId: welche Antwort lief gerade
|
||||
// - pausedPosition / pausedMessageId: bei captureInterruption gemerkt
|
||||
private playbackStartTime: number = 0;
|
||||
private currentPlaybackMsgId: string = '';
|
||||
private pausedPosition: number = 0; // Sekunden in der Audio-Datei
|
||||
private pausedMessageId: string = '';
|
||||
private resumeSound: Sound | null = null; // halten damit GC nicht zuschlaegt
|
||||
// Leading-Silence wird im Native vor den Chunks geschrieben — beim
|
||||
// Position-Berechnen vom playbackStarted abziehen
|
||||
private readonly LEADING_SILENCE_SEC = 0.3;
|
||||
|
||||
constructor() {
|
||||
this.recorder = new AudioRecorderPlayer();
|
||||
this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates
|
||||
@@ -341,6 +355,84 @@ class AudioService {
|
||||
this.stopPlayback();
|
||||
}
|
||||
|
||||
/** Bei Anruf: aktuelle Wiedergabe-Position merken damit wir nach dem
|
||||
* Auflegen von dort weitermachen koennen. Returnt Position in Sekunden
|
||||
* oder 0 wenn nichts spielte. */
|
||||
captureInterruption(): number {
|
||||
if (!this.playbackStartTime || !this.currentPlaybackMsgId) {
|
||||
this.pausedPosition = 0;
|
||||
this.pausedMessageId = '';
|
||||
return 0;
|
||||
}
|
||||
const elapsedMs = Date.now() - this.playbackStartTime;
|
||||
const positionSec = Math.max(0, elapsedMs / 1000 - this.LEADING_SILENCE_SEC);
|
||||
this.pausedPosition = positionSec;
|
||||
this.pausedMessageId = this.currentPlaybackMsgId;
|
||||
console.log('[Audio] captureInterruption: msgId=%s pos=%ss',
|
||||
this.pausedMessageId, positionSec.toFixed(2));
|
||||
return positionSec;
|
||||
}
|
||||
|
||||
/** Nach Anruf-Ende: ab gemerkter Position weiterspielen. Wenn Cache noch
|
||||
* nicht geschrieben (final kam waehrend Anruf vielleicht doch nicht),
|
||||
* warten bis maxWaitMs und dann probieren. Returnt true wenn gestartet. */
|
||||
async resumeFromInterruption(maxWaitMs: number = 30000): Promise<boolean> {
|
||||
const msgId = this.pausedMessageId;
|
||||
const position = this.pausedPosition;
|
||||
if (!msgId) return false;
|
||||
this.pausedMessageId = ''; // konsumieren
|
||||
const cachePath = `${RNFS.DocumentDirectoryPath}/tts_cache/${msgId}.wav`;
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
try {
|
||||
if (await RNFS.exists(cachePath)) {
|
||||
return await this._playFromPathAtPosition(cachePath, position);
|
||||
}
|
||||
} catch {}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
console.warn('[Audio] resumeFromInterruption: WAV %s nicht binnen %dms verfuegbar',
|
||||
msgId, maxWaitMs);
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _playFromPathAtPosition(path: string, positionSec: number): Promise<boolean> {
|
||||
try {
|
||||
// Bestehende laufende Wiedergabe abbrechen damit wir sauber starten
|
||||
if (this.resumeSound) {
|
||||
try { this.resumeSound.stop(); this.resumeSound.release(); } catch {}
|
||||
this.resumeSound = null;
|
||||
}
|
||||
const sound = await new Promise<Sound>((resolve, reject) => {
|
||||
const s = new Sound(path.replace(/^file:\/\//, ''), '', (err) =>
|
||||
err ? reject(err) : resolve(s));
|
||||
});
|
||||
// Audio-Focus anfordern damit Spotify pausiert
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.requestDuck().catch(() => {});
|
||||
this._firePlaybackStarted();
|
||||
this.isPlaying = true;
|
||||
this.resumeSound = sound;
|
||||
console.log('[Audio] Resume von Position %ss aus %s',
|
||||
positionSec.toFixed(2), path);
|
||||
sound.setCurrentTime(Math.max(0, positionSec));
|
||||
sound.play((success) => {
|
||||
if (!success) console.warn('[Audio] Resume-Wiedergabe fehlgeschlagen');
|
||||
try { sound.release(); } catch {}
|
||||
if (this.resumeSound === sound) this.resumeSound = null;
|
||||
this.isPlaying = false;
|
||||
this.playbackFinishedListeners.forEach(cb => {
|
||||
try { cb(); } catch (e) { console.warn('[Audio] cb err:', e); }
|
||||
});
|
||||
this._releaseFocusDeferred();
|
||||
});
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.warn('[Audio] _playFromPathAtPosition fehlgeschlagen:', err?.message || err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** True wenn ARIA gerade was abspielt — egal ob WAV-Queue oder PCM-Stream.
|
||||
* Nuetzlich fuer "Barge-In": wenn der User spricht waehrend ARIA spricht,
|
||||
* soll die ARIA-Wiedergabe abgebrochen + die neue User-Message verarbeitet
|
||||
@@ -876,6 +968,9 @@ class AudioService {
|
||||
}
|
||||
|
||||
private _firePlaybackStarted(): void {
|
||||
// Tracking fuer Auto-Resume nach Anruf-Pause
|
||||
this.playbackStartTime = Date.now();
|
||||
this.currentPlaybackMsgId = this.pcmMessageId || '';
|
||||
this.playbackStartedListeners.forEach(cb => {
|
||||
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
||||
});
|
||||
|
||||
@@ -176,6 +176,8 @@ class PhoneCallService {
|
||||
}
|
||||
|
||||
private _haltForCall(toast: string): void {
|
||||
// Position merken bevor wir den Stream killen — fuer Auto-Resume.
|
||||
audioService.captureInterruption();
|
||||
audioService.haltAllPlayback(toast);
|
||||
wakeWordService.pauseForCall().catch(() => {});
|
||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||
@@ -184,6 +186,14 @@ class PhoneCallService {
|
||||
private _resumeAfterCall(toast: string): void {
|
||||
wakeWordService.resumeFromCall().catch(() => {});
|
||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||
// Auto-Resume: ab gemerkter Position weiterspielen wenn ARIA vor dem
|
||||
// Anruf gerade redete. Wartet bis zu 30s auf den WAV-Cache (falls
|
||||
// final-Marker erst nach dem Anruf-Ende kam).
|
||||
audioService.resumeFromInterruption(30000).then(ok => {
|
||||
if (ok) {
|
||||
console.log('[PhoneCall] Auto-Resume von gemerkter Position gestartet');
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user