You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
190 lines
9.0 KiB
190 lines
9.0 KiB
#include "audio_diagnostics.h" |
|
#ifdef __linux__ |
|
#include <dlfcn.h> |
|
#include <time.h> |
|
static void audio_diag_pulse_init(void* device); |
|
static void audio_timing_begin(void* device, unsigned int frames, int capture); |
|
static void audio_timing_end(void); |
|
static void audio_capture_hole(uint64_t offset); |
|
#define MA_AUDIO_TIMING_BEGIN(device, frames, capture) audio_timing_begin(device, frames, capture) |
|
#define MA_AUDIO_TIMING_END() audio_timing_end() |
|
#define MA_AUDIO_CAPTURE_HOLE(offset) audio_capture_hole(offset) |
|
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) audio_diag_event(device, kind, stage, value, detail) |
|
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message) |
|
#define MA_DIAGNOSTIC_DEVICE_INIT(device) audio_diag_pulse_init(device) |
|
#endif |
|
#define MINIAUDIO_IMPLEMENTATION |
|
#include "../../../lib/miniaudio.h" |
|
|
|
#ifdef __linux__ |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
static void audio_diag_pulse_underflow(ma_pa_stream* stream, void* userdata) { |
|
(void)stream; |
|
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0); |
|
} |
|
static void audio_diag_pulse_overflow(ma_pa_stream* stream, void* userdata) { |
|
(void)stream; |
|
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_WRITE, 1, 0); |
|
} |
|
static void audio_diag_pulse_started(ma_pa_stream* stream, void* userdata) { |
|
(void)stream; |
|
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_WRITE, 1, 0); |
|
} |
|
#endif |
|
|
|
/* Только init-поток: backend-callbacks остаются короткими наблюдателями. */ |
|
static void audio_diag_pulse_init(void* device) { |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
ma_device* dev = (ma_device*)device; |
|
ma_pa_stream_set_suspended_callback_proc underflow, overflow, started; |
|
if (dev->pContext->backend != ma_backend_pulseaudio || !dev->pulse.pStreamPlayback) return; |
|
#ifndef MA_NO_RUNTIME_LINKING |
|
underflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_underflow_callback"); |
|
overflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_overflow_callback"); |
|
started = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_started_callback"); |
|
#else |
|
underflow = pa_stream_set_underflow_callback; |
|
overflow = pa_stream_set_overflow_callback; |
|
started = pa_stream_set_started_callback; |
|
#endif |
|
if (!underflow || !overflow || !started) { |
|
audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_WARNING, "PulseAudio xrun/started callbacks unavailable"); |
|
return; |
|
} |
|
underflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_underflow, dev); |
|
overflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_overflow, dev); |
|
started((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_started, dev); |
|
audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_INFO, "PulseAudio xrun/started observers registered"); |
|
#else |
|
(void)device; |
|
#endif |
|
} |
|
#endif |
|
|
|
/* Вызывается только владельцем общего контекста (GUI), вне device callbacks. */ |
|
int audio_context_failed(ma_context* context) { |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
if (context && context->backend == ma_backend_pulseaudio) { |
|
int result = ((ma_pa_mainloop_iterate_proc)context->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)context->pulse.pMainLoop, 0, NULL); |
|
ma_pa_context_state_t state = ((ma_pa_context_get_state_proc)context->pulse.pa_context_get_state)((ma_pa_context*)context->pulse.pPulseContext); |
|
if (result < 0 || state == MA_PA_CONTEXT_FAILED || state == MA_PA_CONTEXT_TERMINATED) { |
|
ma_log_postf(ma_context_get_log(context), MA_LOG_LEVEL_ERROR, "[PulseAudio] Shared context failed iterate=%d state=%d errno=%d.\n", |
|
result, state, ((ma_pa_context_errno_proc)context->pulse.pa_context_errno)((ma_pa_context*)context->pulse.pPulseContext)); |
|
return 1; |
|
} |
|
} |
|
#endif |
|
return 0; |
|
} |
|
|
|
/* Только device callback того же PA mainloop: запрос использует готовый timing snapshot без roundtrip. */ |
|
int64_t audio_stream_latency_us(ma_device* device, int capture, int* valid) { |
|
*valid = 0; |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
if (device && device->pContext->backend == ma_backend_pulseaudio && device->pContext->pulse.pa_stream_get_latency) { |
|
ma_pa_stream* stream = capture ? device->pulse.pStreamCapture : device->pulse.pStreamPlayback; |
|
ma_uint64 latency = 0; int negative = 0; |
|
if (stream && ((ma_pa_stream_get_latency_proc)device->pContext->pulse.pa_stream_get_latency)(stream, &latency, &negative) == 0) { |
|
*valid = 1; |
|
return negative ? -(int64_t)latency : (int64_t)latency; |
|
} |
|
} |
|
#endif |
|
return 0; |
|
} |
|
|
|
/* PA device name читается в его mainloop; GUI получает только непрозрачный идентификатор маршрута. */ |
|
uint64_t audio_stream_route(ma_device* device, int capture) { |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
if (device && device->pContext->backend == ma_backend_pulseaudio) { |
|
ma_pa_stream* stream = capture ? device->pulse.pStreamCapture : device->pulse.pStreamPlayback; |
|
const char* name = stream ? ((ma_pa_stream_get_device_name_proc)device->pContext->pulse.pa_stream_get_device_name)(stream) : NULL; |
|
if (name && *name) { |
|
uint64_t hash = UINT64_C(14695981039346656037); |
|
for (; *name; ++name) { hash ^= (unsigned char)*name; hash *= UINT64_C(1099511628211); } |
|
return hash; |
|
} |
|
} |
|
#else |
|
(void)device; (void)capture; |
|
#endif |
|
return 0; |
|
} |
|
|
|
/* Reporter копирует сообщение в bounded очередь; DEBUG/file I/O в аппаратном потоке не выполняются. */ |
|
void audio_stream_log_route(ma_device* device, int capture) { |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
if (device && device->pContext->backend == ma_backend_pulseaudio) { |
|
ma_pa_stream* stream = capture ? device->pulse.pStreamCapture : device->pulse.pStreamPlayback; |
|
const char* name = stream ? ((ma_pa_stream_get_device_name_proc)device->pContext->pulse.pa_stream_get_device_name)(stream) : NULL; |
|
if (name) { |
|
char message[320]; |
|
snprintf(message, sizeof(message), "PulseAudio physical %s route: %.255s", capture ? "capture" : "playback", name); |
|
audio_diag_log(ma_device_get_log(device), MA_LOG_LEVEL_INFO, message); |
|
} |
|
} |
|
#else |
|
(void)device; (void)capture; |
|
#endif |
|
} |
|
|
|
#ifdef __linux__ |
|
/* Один backend dispatch может породить несколько client callbacks при конвертации. |
|
* Начало snapshot фиксируется один раз; offset растёт по фактически выданному client PCM. */ |
|
static _Thread_local struct { |
|
ma_device* device; |
|
int valid; |
|
int64_t first_us; |
|
uint64_t frames; |
|
uint64_t hole_offset; |
|
int missing, hole_pending; |
|
} audio_timing; |
|
|
|
static void audio_timing_begin(void* opaque, unsigned int frames, int capture) { |
|
ma_device* device = opaque; |
|
struct timespec now; |
|
clock_gettime(CLOCK_MONOTONIC, &now); |
|
int valid = 0; |
|
int64_t latency = audio_stream_latency_us(device, capture, &valid); |
|
ma_uint32 rate = capture ? device->capture.internalSampleRate : device->playback.internalSampleRate; |
|
ma_uint64 converter_delay = capture ? ma_data_converter_get_output_latency(&device->capture.converter) |
|
: ma_data_converter_get_input_latency(&device->playback.converter); |
|
audio_timing.device = device; |
|
audio_timing.missing = audio_timing.hole_pending; |
|
audio_timing.valid = valid && device->noFixedSizedCallback; |
|
int64_t time = now.tv_sec * INT64_C(1000000) + now.tv_nsec / 1000; |
|
if (capture) { |
|
time -= valid ? latency : (int64_t)frames * 1000000 / rate; |
|
time -= (int64_t)converter_delay * 1000000 / device->sampleRate; |
|
if (audio_timing.missing) time += (int64_t)audio_timing.hole_offset * 1000000 / rate; |
|
} else { |
|
time += latency + (int64_t)(converter_delay + device->playback.inputCacheRemaining) * 1000000 / device->sampleRate; |
|
} |
|
audio_timing.first_us = time; audio_timing.frames = 0; |
|
audio_timing.hole_pending = 0; |
|
} |
|
static void audio_timing_end(void) { audio_timing.device = NULL; } |
|
static void audio_capture_hole(uint64_t offset) { audio_timing.hole_pending = 1; audio_timing.hole_offset = offset; } |
|
#endif |
|
|
|
int64_t audio_stream_frame_time(ma_device* device, unsigned int frames, int* valid) { |
|
*valid = 0; |
|
#ifdef __linux__ |
|
if (audio_timing.device == device) { |
|
int64_t time = audio_timing.first_us + (int64_t)audio_timing.frames * 1000000 / device->sampleRate; |
|
audio_timing.frames += frames; *valid = audio_timing.valid; |
|
return time; |
|
} |
|
#else |
|
(void)device; (void)frames; |
|
#endif |
|
return 0; |
|
} |
|
|
|
int audio_stream_capture_missing(ma_device* device) { |
|
#ifdef __linux__ |
|
return audio_timing.device == device && audio_timing.missing; |
|
#else |
|
(void)device; return 0; |
|
#endif |
|
}
|
|
|