132 lines
5.8 KiB
C++
132 lines
5.8 KiB
C++
#include "audio_device.h"
|
|
#include "../transport/audio_diagnostics.h"
|
|
#include "debug_config.h"
|
|
#include <algorithm>
|
|
#include <utility>
|
|
#include <chrono>
|
|
extern "C" int64_t audio_stream_frame_time(ma_device* device, unsigned int frames, int* valid);
|
|
extern "C" uint64_t audio_stream_route(ma_device* device, int capture);
|
|
extern "C" int audio_stream_capture_missing(ma_device* device);
|
|
extern "C" void audio_stream_log_route(ma_device* device, int capture);
|
|
|
|
AudioDevice::AudioDevice() { m_clock.start(); }
|
|
AudioDevice::~AudioDevice() { close(); }
|
|
|
|
bool AudioDevice::init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback) {
|
|
close();
|
|
m_role = role;
|
|
m_callback = std::move(callback);
|
|
if (!context) { retryLater(); return false; }
|
|
auto* device = new ma_device{};
|
|
config.dataCallback = data;
|
|
config.noFixedSizedCallback = MA_TRUE; // Аппаратные чанки не требуют скрытого накопителя miniaudio.
|
|
config.notificationCallback = notification;
|
|
config.pUserData = this;
|
|
ma_result result = ma_device_init(context, &config, device);
|
|
if (result != MA_SUCCESS) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s init failed result=%d", role, result);
|
|
delete device;
|
|
retryLater();
|
|
return false;
|
|
}
|
|
m_device = device;
|
|
m_frames = 0;
|
|
m_route = 0; m_reportedRoute = 0;
|
|
m_timingMeasured = false; m_reportedTiming = -1;
|
|
m_seenFrames = 0;
|
|
m_suspended = false;
|
|
audio_diag_label(device, session, role);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s initialized backend=%s rate=%u capture='%s' playback='%s' period=%u/%u",
|
|
role, ma_get_backend_name(context->backend), device->sampleRate, device->capture.name, device->playback.name,
|
|
device->capture.internalPeriodSizeInFrames, device->playback.internalPeriodSizeInFrames);
|
|
return true;
|
|
}
|
|
|
|
bool AudioDevice::start() {
|
|
if (!m_device) return false;
|
|
m_progressAt = m_clock.elapsed();
|
|
ma_result result = ma_device_start(m_device);
|
|
if (result != MA_SUCCESS) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s start failed result=%d", m_role, result);
|
|
close();
|
|
retryLater();
|
|
return false;
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s started", m_role);
|
|
return true;
|
|
}
|
|
|
|
void AudioDevice::close() {
|
|
if (m_device) {
|
|
ma_device_uninit(m_device); /* uninit включает stop/join, в том числе после неудачного start. */
|
|
delete m_device;
|
|
m_device = nullptr;
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s closed", m_role);
|
|
}
|
|
m_callback = {};
|
|
m_route = 0;
|
|
}
|
|
|
|
void AudioDevice::pause() {
|
|
if (!m_device) return;
|
|
ma_result result = ma_device_stop(m_device);
|
|
if (result != MA_SUCCESS) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s pause failed result=%d", m_role, result);
|
|
close();
|
|
}
|
|
}
|
|
|
|
void AudioDevice::data(ma_device* device, void* out, const void* in, ma_uint32 frames) {
|
|
auto* self = static_cast<AudioDevice*>(device->pUserData);
|
|
int valid = 0;
|
|
bool capture = device->type == ma_device_type_capture;
|
|
int64_t time = audio_stream_frame_time(device, frames, &valid);
|
|
int64_t now = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
if (!time) time = capture ? now - (int64_t)frames * 1000000 / device->sampleRate : now;
|
|
self->m_callbackTime.store(time, std::memory_order_relaxed);
|
|
self->m_timingMeasured.store(valid, std::memory_order_relaxed);
|
|
uint64_t route = audio_stream_route(device, capture);
|
|
if (route != self->m_route.exchange(route, std::memory_order_relaxed)) audio_stream_log_route(device, capture);
|
|
self->m_captureMissing.store(audio_stream_capture_missing(device), std::memory_order_relaxed);
|
|
if (self->m_callback) self->m_callback(in, out, frames);
|
|
self->m_frames.fetch_add(frames, std::memory_order_relaxed);
|
|
}
|
|
|
|
void AudioDevice::notification(const ma_device_notification* event) {
|
|
auto* self = static_cast<AudioDevice*>(event->pDevice->pUserData);
|
|
if (event->type == ma_device_notification_type_interruption_began) self->m_suspended = true;
|
|
if (event->type == ma_device_notification_type_interruption_ended) self->m_suspended = false;
|
|
}
|
|
|
|
bool AudioDevice::needsRecovery() {
|
|
if (!m_device) return false;
|
|
uint64_t frames = m_frames.load(std::memory_order_relaxed);
|
|
uint64_t route = m_route.load();
|
|
int measured = m_timingMeasured.load();
|
|
if (frames && (route != m_reportedRoute || measured != m_reportedTiming)) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_AEC, "audio: %s timing=%s physical_route=%016llx", m_role,
|
|
measured ? "backend-snapshot" : "callback-estimate", (unsigned long long)route);
|
|
m_reportedRoute = route; m_reportedTiming = measured;
|
|
}
|
|
qint64 now = m_clock.elapsed();
|
|
if (frames != m_seenFrames || m_suspended.load()) {
|
|
m_seenFrames = frames;
|
|
m_progressAt = now;
|
|
if (frames) m_retryDelay = 250;
|
|
}
|
|
bool stopped = ma_device_get_state(m_device) == ma_device_state_stopped;
|
|
if (!stopped && now - m_progressAt < 1000) return false;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s recovery reason=%s idle=%lldms frames=%llu suspended=%d",
|
|
m_role, stopped ? "stopped" : "no-progress", (long long)(now - m_progressAt),
|
|
(unsigned long long)frames, m_suspended.load());
|
|
return true;
|
|
}
|
|
|
|
bool AudioDevice::retryReady() const { return !m_device && m_clock.elapsed() >= m_retryAt; }
|
|
void AudioDevice::retryLater() {
|
|
m_retryAt = m_clock.elapsed() + m_retryDelay;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s retry in %dms", m_role, m_retryDelay);
|
|
m_retryDelay = std::min(m_retryDelay * 2, 2000);
|
|
}
|
|
void AudioDevice::retryNow() { m_retryAt = 0; m_retryDelay = 250; }
|