#include "audio_capture.h" #include "audio_device.h" #include "sound_manager.h" #include "debug_config.h" #include #include #include #include struct AudioCapture::Session { struct Slot { AudioCapture* client = nullptr; // Только GUI. std::atomic active{nullptr}; std::atomic readers{0}; }; AudioDevice device; QString key; std::array consumers; 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 : consumers) if (slot.client) detach(slot); } void prepare() { if (clock.elapsed() < prepareAt) return; prepareAt = clock.elapsed() + 250; for (auto& slot : consumers) { 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(in), frames, channels, device.callbackTimeUs(), device.route(), device.timingMeasured(), device.captureMissing()}; // Трансляция получает исходный PCM первой; оба callback только копируют данные в свои очереди. for (auto& slot : consumers) { 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 stopped, Callback callback) { close(); static std::vector> 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(key); sessions.push_back(m_session); } auto& session = *m_session; auto& slot = session.consumers[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->consumers[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->consumers[0].client != nullptr) + int(m_session->consumers[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()); }