Files
utun2/tools/chatgui/src/call_audio_engine.h
T
evgeny f521bb37f4 fix(call): align audio frontend ownership with core lifecycle
Keep the borrowed audio provider on UTUN_INSTANCE across chat service restarts.
Attach and detach Android voice from the core worker and reject DSP startup
after detach. Preserve TX owner epochs, pending slots and foreign-instance RX
isolation. Remove the unused cross-thread online API and keep its check local
to call.c.

Add eight lifecycle regressions with real cores and the Android host bridge.
Audit public call, DSP, clock, headless and desktop audio contracts and record
the findings and validation scope.

Validation: voice/desktop 38/38, four native call suites, native and both Android
ABI builds, full check 204/204 plus proxy/burst/load. JVM results remain Gradle
UP-TO-DATE (155 app and 15 router). No hardware runtime validation.
2026-10-26 20:22:46 +01:00

86 lines
5.5 KiB
C++

#pragma once
#include <QObject>
#include <atomic>
#include <cstdint>
#include <vector>
#include <QTimer>
#include "audio_device.h"
#include "audio_capture_lease.h"
#include "voice_audio_io.h"
struct ma_device;
/* Desktop-контур одного звонка: GUI владеет capture device и жизненным циклом.
* Все методы/QString-свойства — GUI-поток; dataCallback — аппаратный поток захвата,
* атомарные isActive/callId/isMuted и снимок outputLatency допустимы из других потоков.
* Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo.
* Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker
* получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми
* звуками приложения на том же выходе и возвращает итоговый stereo reference.
* Обычный stop: close capture → join capture worker → detach output → join RX → release core DSP.
* Уже запущенный close worker может закончиться позже stop; microphone lease сохраняется до его завершения. */
class CallAudioEngine : public QObject {
Q_OBJECT
public:
static CallAudioEngine* instance();
/* После ACCEPTED при живой core-привязке: создать DSP/RX worker и запустить восстановление устройств.
* 0 = сессия запущена, готовность аппаратуры не гарантируется; -1 = core/worker/microphone lease недоступны
* либо предыдущий capture ещё закрывается. Повтор ID до beginEnd идемпотентен;
* после beginEnd создаёт новое поколение. Запуск другого ID сначала останавливает прежний. */
int start(quint64 callId);
/* По CALL_ENDED: остановить TX, начать тон450мс и запланировать stop через500мс.
* Неизвестный ID/повтор игнорируются; старый таймер не завершает новое поколение сессии. */
void beginEnd(quint64 callId);
/* Остановить сессию и освободить DSP call_audio; привязка к ядру сохраняется.
* Если устройство уже закрывается в worker, GUI не ждёт его здесь; lease освобождается recovery timer. */
void stop();
bool isActive() const { return m_active.load(); } // Сессия, включая финальный тон; не признак готовности устройств.
quint64 callId() const { return m_callId.load(); }
VoiceAudioIo::OutputLatency outputLatency() const { return m_voice.outputLatency(); }
QString captureError() const { return m_captureError; } // Нет PCM1с: TX отключён до смены входа/нового start, RX сохраняется.
/* Упорядоченная команда capture worker: AEC получает настоящий PCM, mask mute применяется перед Opus после AGC. */
void setMuted(bool muted);
bool isMuted() const { return m_muted.load(); }
/* Backend ID (пустой = default). Output переаттачивается через SoundManager с общим микшером;
* capture останавливает свой worker, снимает captureError и повторяет открытие, с fallback на default. */
void setPlaybackDevice(const QString& id);
QString playbackDevice() const { return m_playbackId; }
void setCaptureDevice(const QString& id);
/* Hardware capture callback: без DSP и растущих буферов. */
void dataCallback(const void* in, void* out, unsigned int frames);
signals:
void captureErrorChanged();
private:
CallAudioEngine();
bool openCapture();
void recoverDevices();
void pauseDevices();
std::atomic<bool> m_active{false};
std::atomic<uint64_t> m_callId{0};
std::atomic<bool> m_muted{false};
QString m_playbackId; /* только GUI-поток */
QString m_captureId; // Запрошенный backend ID, только GUI.
QString m_captureError; // Fatal отсутствие PCM; цифровая тишина считается рабочим входом.
AudioDevice m_capture; // Единственное устройство захвата, без собственного playback device.
AudioCaptureLease m_microphone; // Исключительное владение микрофоном, включая асинхронное закрытие.
VoiceAudioIo m_voice; // Capture/RX workers и ограниченные очереди PCM/команд.
bool m_outputAttached = false; // Источник зарегистрирован в микшере, устройство может ещё восстанавливаться.
QTimer m_recoveryTimer; // Опрос устройств/PCM каждые200мс, после stop также ждёт close worker.
uint64_t m_generation = 0; // Защищает новое start от старого таймера beginEnd.
bool m_ending = false; // Capture остановлен, RX выдаёт финальный тон до stop.
};