2 changed files with 186 additions and 0 deletions
@ -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()); |
||||
} |
||||
@ -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…
Reference in new issue