Browse Source

Add shared desktop microphone sessions with stream profile priority

master
evgeny 3 days ago
parent
commit
6d7b3e8e02
  1. 144
      tools/chatgui/src/audio_capture.cpp
  2. 42
      tools/chatgui/src/audio_capture.h

144
tools/chatgui/src/audio_capture.cpp

@ -0,0 +1,144 @@
#include "audio_capture.h"
#include "audio_device.h"
#include "sound_manager.h"
#include "debug_config.h"
#include <array>
#include <atomic>
#include <thread>
#include <vector>
struct AudioCapture::Session {
struct Slot {
AudioCapture* client = nullptr; // Только GUI.
std::atomic<AudioCapture*> active{nullptr};
std::atomic<unsigned> readers{0};
};
AudioDevice device;
QString key;
std::array<Slot, 2> slots;
bool streamProfile = false;
int requestedChannels = 1, channels = 1;
uint64_t tag = 0;
QElapsedTimer clock;
qint64 prepareAt = 0;
explicit Session(QString input) : key(std::move(input)) { clock.start(); }
~Session() { stop(); }
void detach(Slot& slot) {
slot.active.store(nullptr);
// seq_cst: callback либо увидит nullptr, либо завершится до освобождения клиента.
while (slot.readers.load()) std::this_thread::yield();
if (slot.client->m_prepared) {
slot.client->m_prepared = false;
slot.client->m_stopped();
}
}
void stop() {
device.close(); // join до stopped: workers больше не получат PCM старого формата.
for (auto& slot : slots) if (slot.client) detach(slot);
}
void prepare() {
if (clock.elapsed() < prepareAt) return;
prepareAt = clock.elapsed() + 250;
for (auto& slot : slots) {
auto* client = slot.client;
if (!client || client->m_prepared) continue;
if (!client->m_ready(channels, device.device()->capture.internalChannels)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "capture: consumer start failed role=%s channels=%d",
client->m_role == Role::Stream ? "stream" : "radio", channels);
continue;
}
client->m_prepared = true;
slot.active.store(client);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: attached role=%s profile=%s channels=%d rate=48000",
client->m_role == Role::Stream ? "stream" : "radio", streamProfile ? "stream" : "radio", channels);
}
}
void recover() {
if (device.needsRecovery()) { stop(); device.retryLater(); }
if (device.initialized()) { prepare(); return; }
auto* context = SoundManager::instance()->context();
if (!context || !device.retryReady()) return;
ma_device_id id;
bool selected = SoundManager::deviceId(key, &id);
if (!key.isEmpty() && !selected) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "capture: selected input unavailable; retaining selection");
device.retryLater(); return;
}
for (int count = requestedChannels; count >= 1; --count) {
auto config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16; config.capture.channels = count; config.sampleRate = 48000;
config.capture.pDeviceID = selected ? &id : nullptr;
if (!streamProfile) AudioDevice::requestLowLatency(context, config);
auto callback = [this](const void* in, void*, unsigned frames) {
Frame frame{static_cast<const int16_t*>(in), frames, channels, device.callbackTimeUs(),
device.route(), device.timingMeasured(), device.captureMissing()};
// Трансляция получает исходный PCM первой; оба callback только копируют данные в свои очереди.
for (auto& slot : slots) {
slot.readers.fetch_add(1);
if (auto* client = slot.active.load()) client->m_callback(frame);
slot.readers.fetch_sub(1);
}
};
if (!device.init(context, config, streamProfile ? "stream-capture" : "radio-capture", tag, callback)) continue;
channels = count; prepareAt = 0; prepare();
if (device.start()) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: opened profile=%s channels=%d requested=%d input=%s",
streamProfile ? "stream" : "radio", channels, requestedChannels, qPrintable(key));
return;
}
stop();
}
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "capture: cannot open selected input profile=%s", streamProfile ? "stream" : "radio");
}
};
AudioCapture::~AudioCapture() { close(); }
bool AudioCapture::open(const QString& input, Role role, int channels, uint64_t tag, Ready ready,
std::function<void()> stopped, Callback callback) {
close();
static std::vector<std::weak_ptr<Session>> sessions; // Только GUI; владение у подписчиков.
QString key = input;
if (key.isEmpty()) for (const auto& item : SoundManager::enumerateCaptureDevices()) {
if (item.isDefault) { key = item.id; break; }
}
for (auto it = sessions.begin(); it != sessions.end();) {
auto session = it->lock();
if (!session) { it = sessions.erase(it); continue; }
if (session->key == key) m_session = session;
++it;
}
if (!m_session) { m_session = std::make_shared<Session>(key); sessions.push_back(m_session); }
auto& session = *m_session;
auto& slot = session.slots[role == Role::Stream ? 0 : 1];
if (slot.client) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "capture: duplicate consumer role=%s", role == Role::Stream ? "stream" : "radio");
m_session.reset(); return false;
}
bool takeover = role == Role::Stream && !session.streamProfile;
if (takeover) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: stream takes ownership of capture profile");
session.stop(); session.streamProfile = true;
}
m_role = role; m_ready = std::move(ready); m_stopped = std::move(stopped); m_callback = std::move(callback);
slot.client = this;
if (role == Role::Stream || !session.streamProfile) { session.requestedChannels = channels; session.tag = tag; }
session.prepareAt = 0; session.device.retryNow(); session.recover();
return running();
}
void AudioCapture::close() {
if (!m_session) return;
auto& slot = m_session->slots[m_role == Role::Stream ? 0 : 1];
m_session->detach(slot); slot.client = nullptr;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: detached role=%s remaining=%d", m_role == Role::Stream ? "stream" : "radio",
int(m_session->slots[0].client != nullptr) + int(m_session->slots[1].client != nullptr));
m_session.reset(); m_callback = {}; m_ready = {}; m_stopped = {};
}
void AudioCapture::recover() { if (m_session) m_session->recover(); }
bool AudioCapture::running() const {
return m_session && m_prepared && m_session->device.initialized() && ma_device_is_started(m_session->device.device());
}

42
tools/chatgui/src/audio_capture.h

@ -0,0 +1,42 @@
#pragma once
#include <QString>
#include <cstdint>
#include <functional>
#include <memory>
/* GUI владеет подпиской, сессия — устройством. Одинаковый выбранный вход открывается один раз.
* Профиль трансляции имеет приоритет и сохраняется для рации после её остановки.
* Callback только копирует PCM; ready/stopped вызываются в GUI вне аппаратного callback.
* close() ждёт завершения своего callback, не останавливая устройство другого потребителя. */
class AudioCapture {
public:
enum class Role { Stream, Radio };
struct Frame {
const int16_t* pcm;
unsigned frames;
int channels;
int64_t timeUs;
uint64_t route;
bool measured, missing;
};
using Ready = std::function<bool(int channels, int nativeChannels)>;
using Callback = std::function<void(const Frame&)>;
AudioCapture() = default;
~AudioCapture();
AudioCapture(const AudioCapture&) = delete;
AudioCapture& operator=(const AudioCapture&) = delete;
bool open(const QString& input, Role role, int channels, uint64_t tag, Ready ready,
std::function<void()> stopped, Callback callback);
void close();
void recover();
bool subscribed() const { return bool(m_session); }
bool running() const;
private:
struct Session;
std::shared_ptr<Session> m_session;
Ready m_ready;
std::function<void()> m_stopped;
Callback m_callback;
Role m_role = Role::Radio;
bool m_prepared = false;
};
Loading…
Cancel
Save