|
|
|
|
@ -48,6 +48,9 @@ static int g_prepared_capture = 0;
|
|
|
|
|
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */ |
|
|
|
|
static struct audio_compressor* g_compressor = NULL; |
|
|
|
|
static uint64_t g_tx_generation; |
|
|
|
|
static uint64_t g_capture_epoch; |
|
|
|
|
static int64_t g_capture_end = INT64_MIN; |
|
|
|
|
static int g_codec_delay; |
|
|
|
|
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported; |
|
|
|
|
static unsigned int g_tx_reasons; |
|
|
|
|
static int g_tx_encode_error; |
|
|
|
|
@ -59,6 +62,7 @@ struct call_audio_tx {
|
|
|
|
|
struct UTUN_INSTANCE* inst; |
|
|
|
|
uint64_t call_id, generation, created_tb; |
|
|
|
|
int len; |
|
|
|
|
struct call_capture_time timing; |
|
|
|
|
uint8_t opus[256]; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
@ -73,7 +77,7 @@ static void call_audio_send(void* arg) {
|
|
|
|
|
if (current) { ++g_tx_dropped; g_tx_reasons |= 16; } |
|
|
|
|
} |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len); |
|
|
|
|
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len, &tx->timing); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static const struct call_audio_ops g_ops = { |
|
|
|
|
@ -113,12 +117,12 @@ void call_audio_destroy(struct UTUN_INSTANCE* inst) {
|
|
|
|
|
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */ |
|
|
|
|
|
|
|
|
|
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id, |
|
|
|
|
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) { |
|
|
|
|
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing) { |
|
|
|
|
(void)inst; (void)seq; |
|
|
|
|
|
|
|
|
|
pthread_mutex_lock(&g_mtx); |
|
|
|
|
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) { |
|
|
|
|
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10)); |
|
|
|
|
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing); |
|
|
|
|
} |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
} |
|
|
|
|
@ -151,6 +155,13 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
|
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE); |
|
|
|
|
int codec_delay = 0; |
|
|
|
|
if (opus_codec_encoder_delay(enc, &codec_delay) < 0) { |
|
|
|
|
opus_codec_encoder_destroy(enc); |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder delay unavailable", CALL_AUDIO_ID); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1); |
|
|
|
|
if (!dec) { |
|
|
|
|
@ -218,6 +229,7 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
|
|
|
|
|
g_call_id = call_id; |
|
|
|
|
g_active = 1; |
|
|
|
|
++g_tx_generation; g_tx_dropped = g_tx_reported = 0; |
|
|
|
|
g_capture_epoch = 1; g_capture_end = INT64_MIN; g_codec_delay = codec_delay; |
|
|
|
|
g_tx_reasons = 0; g_tx_encode_error = 0; |
|
|
|
|
g_ending = 0; |
|
|
|
|
g_ended_started = 0; |
|
|
|
|
@ -300,6 +312,7 @@ void call_audio_reset_io(uint64_t call_id, int playback) {
|
|
|
|
|
if (g_active && g_call_id == call_id) { |
|
|
|
|
if (g_aec) speex_aec_reset(g_aec); |
|
|
|
|
if (!playback && g_compressor) audio_compressor_reset(g_compressor); |
|
|
|
|
if (!playback) { ++g_capture_epoch; g_capture_end = INT64_MIN; } |
|
|
|
|
if (playback) { |
|
|
|
|
call_jitter_reset(g_vj); |
|
|
|
|
g_gap_samples = 0; |
|
|
|
|
@ -353,8 +366,10 @@ void call_audio_set_aec_delay_frames(int frames) {
|
|
|
|
|
|
|
|
|
|
/* ── аудио-поток: TX / RX ── */ |
|
|
|
|
|
|
|
|
|
static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, int prepared) { |
|
|
|
|
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask)) { |
|
|
|
|
static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, |
|
|
|
|
int prepared, int64_t capture_pts_us) { |
|
|
|
|
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask) |
|
|
|
|
|| (capture_pts_us != INT64_MIN && (capture_pts_us < 0 || capture_pts_us > INT64_MAX / 2))) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid capture frame count=%d prepared=%d", CALL_AUDIO_ID, count, prepared); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
@ -375,6 +390,14 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count,
|
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy mismatch prepared=%d", CALL_AUDIO_ID, prepared); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
if (capture_pts_us != INT64_MIN) { |
|
|
|
|
if (g_capture_end != INT64_MIN && (capture_pts_us < g_capture_end - 2000 || capture_pts_us > g_capture_end + 2000)) { |
|
|
|
|
++g_capture_epoch; |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call capture discontinuity epoch=%llu expected=%lld actual=%lld", |
|
|
|
|
(unsigned long long)g_capture_epoch, (long long)g_capture_end, (long long)capture_pts_us); |
|
|
|
|
} |
|
|
|
|
g_capture_end = capture_pts_us + 20000; |
|
|
|
|
} else g_capture_end = INT64_MIN; |
|
|
|
|
const int16_t* tx = pcm; |
|
|
|
|
if (g_aec) { |
|
|
|
|
if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count) |
|
|
|
|
@ -415,6 +438,7 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count,
|
|
|
|
|
} |
|
|
|
|
task->inst = inst; task->call_id = call_id; |
|
|
|
|
task->generation = g_tx_generation; task->created_tb = get_time_tb(); |
|
|
|
|
task->timing = (struct call_capture_time){capture_pts_us, g_capture_epoch, (uint32_t)g_codec_delay}; |
|
|
|
|
task->len = len; |
|
|
|
|
memcpy(task->opus, opus, (size_t)len); |
|
|
|
|
task->task.callback = call_audio_send; task->task.arg = task; |
|
|
|
|
@ -425,16 +449,16 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count,
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Маска соответствует границам mute в том же упорядоченном потоке, что и PCM. */ |
|
|
|
|
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask) { |
|
|
|
|
return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1); |
|
|
|
|
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us) { |
|
|
|
|
return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1, capture_pts_us); |
|
|
|
|
} |
|
|
|
|
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) { |
|
|
|
|
return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0); |
|
|
|
|
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted, int64_t capture_pts_us) { |
|
|
|
|
return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0, capture_pts_us); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Незамьюченный захват для платформ без программного mute. */ |
|
|
|
|
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { |
|
|
|
|
return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0); |
|
|
|
|
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us) { |
|
|
|
|
return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0, capture_pts_us); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { |
|
|
|
|
@ -468,6 +492,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
|
|
|
|
} |
|
|
|
|
if (call_tones_ended_pending(tones)) |
|
|
|
|
n = call_tones_render(tones, out, max_samples); |
|
|
|
|
call_jitter_silence(j, (unsigned)n); |
|
|
|
|
} else { |
|
|
|
|
n = call_jitter_pull(j, out, max_samples); |
|
|
|
|
int previous_gap = g_gap_samples; |
|
|
|
|
@ -477,6 +502,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
|
|
|
|
g_gap_samples = 0; |
|
|
|
|
} |
|
|
|
|
int missing = max_samples - n; |
|
|
|
|
call_jitter_silence(j, (unsigned)missing); |
|
|
|
|
/* Подготовка устройств и начальное накопление PCM не являются потерей звука. */ |
|
|
|
|
int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing; |
|
|
|
|
if (quiet > missing) quiet = missing; |
|
|
|
|
@ -512,6 +538,23 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
|
|
|
|
return n; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void call_audio_playback_progress(uint64_t call_id, uint64_t epoch, uint64_t position, |
|
|
|
|
int64_t at_us, int64_t now_us, int measured) { |
|
|
|
|
pthread_mutex_lock(&g_mtx); |
|
|
|
|
if (g_active && call_id == g_call_id && g_vj) call_jitter_progress(g_vj, epoch, position, at_us, now_us, measured); |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int call_audio_get_clock(uint64_t call_id, int64_t now_us, struct call_clock_snapshot* out) { |
|
|
|
|
if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call clock missing output"); return -1; } |
|
|
|
|
memset(out, 0, sizeof(*out)); out->pts_us = -1; |
|
|
|
|
pthread_mutex_lock(&g_mtx); |
|
|
|
|
int present = g_active && call_id == g_call_id && g_vj; |
|
|
|
|
if (present) call_jitter_clock(g_vj, now_us, out); |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
return present ? 0 : -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── uasync-поток: метрики ── */ |
|
|
|
|
|
|
|
|
|
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, |
|
|
|
|
|