From 40b974b29c44d6354637976fe03a5253998ff533 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 2 Oct 2026 01:56:10 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=B2=D1=8B=D0=B9=20AGC=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D1=8F=D0=B9=20mut?= =?UTF-8?q?e=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20AEC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/audio_compressor.c | 422 ++++++++++-------------- lib/audio_compressor.h | 8 +- src/call/call_audio.c | 31 +- src/call/call_audio.h | 2 + src/media_async/attachment_send.c | 2 +- src/radio/radio_audio.c | 54 +-- tests/test_audio_compressor.c | 55 +++ tools/chatgui/src/audiorecorder.cpp | 1 + tools/chatgui/src/call_audio_engine.cpp | 4 +- 9 files changed, 272 insertions(+), 307 deletions(-) diff --git a/lib/audio_compressor.c b/lib/audio_compressor.c index 68dd2b5c..5cef4ba5 100644 --- a/lib/audio_compressor.c +++ b/lib/audio_compressor.c @@ -3,244 +3,144 @@ #include "mem.h" #include #include -#include - -#define BLOCK_LEVELS_CHUNK 64 -#define PENDING_CHUNK 32 +#include +/* История и задержанные блоки ограничены окном настройки. Только запись накапливает output. */ struct audio_compressor { audio_compressor_config_t cfg; - int enabled; - - int block_samples; - int lookback_blocks; - int lookahead_blocks; - float rise_factor_per_block; - float release_factor_per_block; - float max_gain; - float gain_smoothed; - - float* block_levels; - size_t block_levels_count; - size_t block_levels_cap; - - struct pending_block { - int16_t* samples; - size_t count; - size_t level_index; - }* pending; - size_t pending_count; - size_t pending_cap; - - int16_t* accumulator; - size_t accum_count; - + int enabled, configured, failed, block_samples, lookback_blocks, lookahead_blocks; + float rise_factor_per_block, release_factor_per_block, max_gain, gain_smoothed; + float* block_levels; + size_t levels_cap, blocks_seen; + int16_t* pending; + size_t pending_cap, pending_head, pending_count, accum_count; int16_t* output; - size_t output_size; - size_t output_cap; - + size_t output_size, output_cap; uint32_t clip_count; - int dbg_counter; + unsigned dbg_counter; }; -/* Пустой компрессор с теми же настройками для следующей записи. */ -struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) { - if (!source) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone NULL configuration"); return NULL; } - struct audio_compressor* copy = audio_compressor_create(); - if (copy) { audio_compressor_configure(copy, &source->cfg); audio_compressor_set_enabled(copy, source->enabled); } - return copy; -} - -/* Начальный gain — середина диапазона [0, max_gain] в дБ (геометрическое среднее). */ -static float initial_gain(float max_gain) { - return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; -} +/* Начальное усиление в середине диапазона в дБ. */ +static float initial_gain(float max_gain) { return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; } struct audio_compressor* audio_compressor_create(void) { struct audio_compressor* ac = u_calloc(1, sizeof(*ac)); - if (!ac) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: OOM"); - return NULL; - } + if (!ac) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: state allocation failed"); return NULL; } ac->enabled = 1; - ac->gain_smoothed = 1.0f; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: ok"); return ac; } void audio_compressor_destroy(struct audio_compressor* ac) { if (!ac) return; - for (size_t i = 0; i < ac->pending_count; i++) - u_free(ac->pending[i].samples); - u_free(ac->pending); - u_free(ac->block_levels); - u_free(ac->accumulator); - u_free(ac->output); - u_free(ac); - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor_destroy"); -} - -void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg) { - if (!ac || !cfg) return; - ac->cfg = *cfg; - if (ac->cfg.sample_rate <= 0) ac->cfg.sample_rate = 48000; - if (ac->cfg.channels <= 0) ac->cfg.channels = 1; - if (ac->cfg.block_duration_ms <= 0) ac->cfg.block_duration_ms = 20; - if (ac->cfg.lookback_ms <= 0) ac->cfg.lookback_ms = 200; - if (ac->cfg.lookahead_ms <= 0) ac->cfg.lookahead_ms = 100; - if (ac->cfg.target_level <= 0.0f) ac->cfg.target_level = 0.25f; - - ac->block_samples = ac->cfg.sample_rate * ac->cfg.block_duration_ms / 1000 * ac->cfg.channels; - ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms; - ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms; - ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f); - if (ac->cfg.release_rate_per_sec <= 0.0f) - ac->cfg.release_rate_per_sec = ac->cfg.rise_rate_per_sec * 5.0f; - ac->release_factor_per_block = powf(ac->cfg.release_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f); - ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f); - ac->gain_smoothed = initial_gain(ac->max_gain); - - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB initGain=%.1fdB riseRate=%.1f/sec releaseRate=%.1f/sec target=%.0fdBFS", - ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms, - ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db, - 20.0f * log10f(ac->gain_smoothed), - ac->cfg.rise_rate_per_sec, ac->cfg.release_rate_per_sec, 20.0f * log10f(ac->cfg.target_level)); + u_free(ac->pending); u_free(ac->block_levels); u_free(ac->output); u_free(ac); } void audio_compressor_reset(struct audio_compressor* ac) { if (!ac) return; - for (size_t i = 0; i < ac->pending_count; i++) - u_free(ac->pending[i].samples); - ac->pending_count = 0; - ac->block_levels_count = 0; - ac->accum_count = 0; - ac->output_size = 0; + ac->pending_head = ac->pending_count = ac->accum_count = ac->blocks_seen = ac->output_size = 0; ac->gain_smoothed = initial_gain(ac->max_gain); - ac->clip_count = 0; - ac->dbg_counter = 0; + ac->clip_count = ac->dbg_counter = 0; + ac->failed = 0; +} + +/* Ноль lookahead означает отсутствие задержки; defaults остальных полей задаются явно здесь. */ +int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* config) { + if (!ac || !config) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: missing configuration"); return -1; } + audio_compressor_config_t cfg = *config; + if (!cfg.sample_rate) cfg.sample_rate = 48000; + if (!cfg.channels) cfg.channels = 1; + if (!cfg.block_duration_ms) cfg.block_duration_ms = 20; + if (!cfg.lookback_ms) cfg.lookback_ms = 200; + if (!cfg.target_level) cfg.target_level = 0.25f; + if (!cfg.rise_rate_per_sec) cfg.rise_rate_per_sec = 4.0f; + if (!cfg.release_rate_per_sec) cfg.release_rate_per_sec = cfg.rise_rate_per_sec * 5.0f; + if (cfg.sample_rate < 8000 || cfg.sample_rate > 192000 || cfg.channels < 1 || cfg.channels > 2 || + cfg.block_duration_ms < 1 || cfg.block_duration_ms > 1000 || cfg.lookback_ms < 0 || cfg.lookback_ms > 5000 || + cfg.lookahead_ms < 0 || cfg.lookahead_ms > 5000 || !isfinite(cfg.target_level) || cfg.target_level <= 0 || + !isfinite(cfg.max_gain_db) || cfg.max_gain_db < 0 || cfg.max_gain_db > 60 || + !isfinite(cfg.rise_rate_per_sec) || cfg.rise_rate_per_sec <= 0 || + !isfinite(cfg.release_rate_per_sec) || cfg.release_rate_per_sec <= 0) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid rate=%d ch=%d block=%d back=%d ahead=%d", + cfg.sample_rate, cfg.channels, cfg.block_duration_ms, cfg.lookback_ms, cfg.lookahead_ms); + return -1; + } + int block = cfg.sample_rate * cfg.block_duration_ms / 1000 * cfg.channels; + int back = cfg.lookback_ms / cfg.block_duration_ms, ahead = cfg.lookahead_ms / cfg.block_duration_ms; + size_t levels_cap = (size_t)back + ahead + 2, pending_cap = (size_t)ahead + 2; + float* levels = u_calloc(levels_cap, sizeof(float)); + int16_t* pending = u_calloc(pending_cap * (size_t)block, sizeof(int16_t)); + if (!levels || !pending) { + u_free(levels); u_free(pending); + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: buffers allocation failed"); + return -1; + } + u_free(ac->block_levels); u_free(ac->pending); + ac->cfg = cfg; ac->block_samples = block; + ac->lookback_blocks = back; ac->lookahead_blocks = ahead; + ac->block_levels = levels; ac->levels_cap = levels_cap; + ac->pending = pending; ac->pending_cap = pending_cap; + ac->rise_factor_per_block = powf(cfg.rise_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f); + ac->release_factor_per_block = powf(cfg.release_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f); + ac->max_gain = powf(10.0f, cfg.max_gain_db / 20.0f); + ac->configured = 1; + audio_compressor_reset(ac); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "compressor: configured rate=%d ch=%d ahead=%dms history=%zu pending=%zu", + cfg.sample_rate, cfg.channels, cfg.lookahead_ms, levels_cap, pending_cap); + return 0; } -void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { - if (!ac) return; - ac->enabled = enabled ? 1 : 0; +struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) { + if (!source || !source->configured) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone configuration"); return NULL; } + struct audio_compressor* copy = audio_compressor_create(); + if (copy && audio_compressor_configure(copy, &source->cfg) != 0) { audio_compressor_destroy(copy); return NULL; } + if (copy) copy->enabled = source->enabled; + return copy; } -int audio_compressor_is_enabled(const struct audio_compressor* ac) { - return ac ? ac->enabled : 0; -} +void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { if (ac) ac->enabled = !!enabled; } +int audio_compressor_is_enabled(const struct audio_compressor* ac) { return ac && ac->enabled; } +/* Одна оценка для всех каналов сохраняет баланс стерео. */ static float compute_block_level(const int16_t* samples, size_t count) { double sum_sq = 0.0; float peak = 0.0f; for (size_t i = 0; i < count; i++) { float v = (float)samples[i] / 32768.0f; sum_sq += (double)v * v; - float av = fabsf(v); - if (av > peak) peak = av; - } - float rms = (float)sqrt(sum_sq / (double)count); - return (rms * 2.0f + peak) / 2.0f; -} - -static void ensure_output_cap(struct audio_compressor* ac, size_t needed) { - size_t want = ac->output_size + needed; - if (want <= ac->output_cap) return; - size_t new_cap = ac->output_cap ? ac->output_cap * 2 : 4096; - while (new_cap < want) new_cap *= 2; - int16_t* tmp = u_realloc(ac->output, new_cap * sizeof(int16_t)); - if (!tmp) return; - ac->output = tmp; - ac->output_cap = new_cap; -} - -static void append_output(struct audio_compressor* ac, const int16_t* samples, size_t count) { - ensure_output_cap(ac, count); - memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t)); - ac->output_size += count; -} - -static void ensure_block_levels_cap(struct audio_compressor* ac) { - if (ac->block_levels_count < ac->block_levels_cap) return; - size_t new_cap = ac->block_levels_cap ? ac->block_levels_cap * 2 : BLOCK_LEVELS_CHUNK; - float* tmp = u_realloc(ac->block_levels, new_cap * sizeof(float)); - if (!tmp) return; - ac->block_levels = tmp; - ac->block_levels_cap = new_cap; -} - -static void add_block_level(struct audio_compressor* ac, float level) { - ensure_block_levels_cap(ac); - ac->block_levels[ac->block_levels_count++] = level; -} - -static void ensure_pending_cap(struct audio_compressor* ac) { - if (ac->pending_count < ac->pending_cap) return; - size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK; - struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp)); - if (!tmp) return; - ac->pending = tmp; - ac->pending_cap = new_cap; -} - -static void process_pending_block(struct audio_compressor* ac); - -void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) { - if (!ac || !samples || count == 0) return; - - if (!ac->enabled) { - append_output(ac, samples, count); - return; + if (fabsf(v) > peak) peak = fabsf(v); } - - size_t remaining = count; - const int16_t* src = samples; - - while (remaining > 0) { - size_t need = (size_t)ac->block_samples - ac->accum_count; - size_t take = remaining < need ? remaining : need; - - size_t new_cnt = ac->accum_count + take; - int16_t* tmp = u_realloc(ac->accumulator, new_cnt * sizeof(int16_t)); - if (!tmp) return; - ac->accumulator = tmp; - memcpy(ac->accumulator + ac->accum_count, src, take * sizeof(int16_t)); - ac->accum_count = new_cnt; - - src += take; - remaining -= take; - - if (ac->accum_count == (size_t)ac->block_samples) { - float level = compute_block_level(ac->accumulator, ac->accum_count); - add_block_level(ac, level); - - ensure_pending_cap(ac); - ac->pending[ac->pending_count].samples = ac->accumulator; - ac->pending[ac->pending_count].count = ac->accum_count; - ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1; - ac->pending_count++; - - ac->accumulator = NULL; - ac->accum_count = 0; - } + return (2.0f * sqrtf((float)(sum_sq / count)) + peak) / 2.0f; +} + +/* Выход записи растёт только вне аппаратного callback; OOM переводит запись в failed. */ +static int reserve_output(struct audio_compressor* ac, size_t count) { + if (count > SIZE_MAX / sizeof(int16_t) - ac->output_size) goto failed; + size_t want = ac->output_size + count; + if (want <= ac->output_cap) return 0; + size_t capacity = ac->output_cap ? ac->output_cap : 4096; + while (capacity < want) { + if (capacity > SIZE_MAX / sizeof(int16_t) / 2) { capacity = want; break; } + capacity *= 2; } - - while (ac->pending_count > (size_t)ac->lookahead_blocks) - process_pending_block(ac); -} - -static void process_pending_block(struct audio_compressor* ac) { - if (ac->pending_count == 0) return; - - size_t idx = ac->pending[0].level_index; - - size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0; - size_t win_end = idx + (size_t)ac->lookahead_blocks; - if (win_end >= ac->block_levels_count) win_end = ac->block_levels_count - 1; - - float envelope = 0.0f; - for (size_t i = win_start; i <= win_end; i++) { - if (ac->block_levels[i] > envelope) envelope = ac->block_levels[i]; + int16_t* samples = u_realloc(ac->output, capacity * sizeof(int16_t)); + if (!samples) goto failed; + ac->output = samples; ac->output_cap = capacity; + return 0; +failed: + ac->failed = 1; + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: recording output allocation failed samples=%zu", count); + return -1; +} + +/* Общий алгоритм усиления для записи с lookahead и потокового кадра без lookahead. */ +static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t count, size_t idx) { + size_t begin = idx >= (size_t)ac->lookback_blocks ? idx - ac->lookback_blocks : 0; + size_t end = idx + ac->lookahead_blocks; + if (end >= ac->blocks_seen) end = ac->blocks_seen - 1; + float envelope = 0; + for (size_t i = begin; i <= end; i++) { + float level = ac->block_levels[i % ac->levels_cap]; + if (level > envelope) envelope = level; } if (envelope < 0.0001f) envelope = 0.0001f; @@ -263,64 +163,88 @@ static void process_pending_block(struct audio_compressor* ac) { float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f); if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u", - idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count); + idx, ac->block_levels[idx % ac->levels_cap], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count); } ac->dbg_counter++; - for (size_t i = 0; i < ac->pending[0].count; i++) { - float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed; + for (size_t i = 0; i < count; i++) { + float v = (float)samples[i] * ac->gain_smoothed; if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; } if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; } - ac->pending[0].samples[i] = (int16_t)(int)v; + samples[i] = (int16_t)(int)v; } - append_output(ac, ac->pending[0].samples, ac->pending[0].count); - u_free(ac->pending[0].samples); - - /* shift remaining pending blocks down */ - if (ac->pending_count > 1) - memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending)); - ac->pending_count--; } -void audio_compressor_flush(struct audio_compressor* ac) { - if (!ac) return; - if (!ac->enabled) return; - - if (ac->accum_count > 0) { - float level = compute_block_level(ac->accumulator, ac->accum_count); - add_block_level(ac, level); - - ensure_pending_cap(ac); - ac->pending[ac->pending_count].samples = ac->accumulator; - ac->pending[ac->pending_count].count = ac->accum_count; - ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1; - ac->pending_count++; - - ac->accumulator = NULL; - ac->accum_count = 0; - } - - if (ac->pending_count > 0) - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)", - ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f)); - - while (ac->pending_count > 0) - process_pending_block(ac); +/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */ +static int drain_block(struct audio_compressor* ac, size_t count) { + if (reserve_output(ac, count) != 0) return -1; + int16_t* samples = ac->pending + ac->pending_head * ac->block_samples; + apply_gain(ac, samples, count, ac->blocks_seen - ac->pending_count); + memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t)); + ac->output_size += count; + ac->pending_head = (ac->pending_head + 1) % ac->pending_cap; + --ac->pending_count; + return 0; } -const int16_t* audio_compressor_output(const struct audio_compressor* ac) { - return ac ? ac->output : NULL; +/* Ровно один кадр, без output-накопителя и выделений памяти. in/out могут совпадать. */ +int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count) { + if (!ac || !ac->configured || ac->failed || !in || !out || count != (size_t)ac->block_samples || + ac->lookahead_blocks || ac->pending_count || ac->accum_count) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid streaming frame count=%zu", count); + return -1; + } + memmove(out, in, count * sizeof(int16_t)); + if (ac->enabled) { + size_t idx = ac->blocks_seen++; + ac->block_levels[idx % ac->levels_cap] = compute_block_level(in, count); + apply_gain(ac, out, count, idx); + } + return 0; } -size_t audio_compressor_output_size(const struct audio_compressor* ac) { - return ac ? ac->output_size : 0; +int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) { + if (!ac || !ac->configured || ac->failed || (!samples && count)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid recording input"); return -1; + } + if (!ac->enabled) { + if (reserve_output(ac, count) != 0) return -1; + if (count) memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t)); + ac->output_size += count; + return 0; + } + while (count) { + size_t take = (size_t)ac->block_samples - ac->accum_count; + if (take > count) take = count; + int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples; + memcpy(block + ac->accum_count, samples, take * sizeof(int16_t)); + samples += take; count -= take; ac->accum_count += take; + if (ac->accum_count == (size_t)ac->block_samples) { + ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, ac->accum_count); + ac->accum_count = 0; ++ac->pending_count; + if (ac->pending_count > (size_t)ac->lookahead_blocks && drain_block(ac, ac->block_samples) != 0) return -1; + } + } + return 0; } -void audio_compressor_clear_output(struct audio_compressor* ac) { - if (!ac) return; - ac->output_size = 0; +int audio_compressor_flush(struct audio_compressor* ac) { + if (!ac || !ac->configured || ac->failed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid flush"); return -1; } + if (!ac->enabled) return 0; + size_t tail = ac->accum_count; + if (tail) { + int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples; + ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, tail); + ac->accum_count = 0; ++ac->pending_count; + } + while (ac->pending_count) { + size_t count = tail && ac->pending_count == 1 ? tail : (size_t)ac->block_samples; + if (drain_block(ac, count) != 0) return -1; + } + return 0; } -uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { - return ac ? ac->clip_count : 0; -} +const int16_t* audio_compressor_output(const struct audio_compressor* ac) { return ac ? ac->output : NULL; } +size_t audio_compressor_output_size(const struct audio_compressor* ac) { return ac ? ac->output_size : 0; } +void audio_compressor_clear_output(struct audio_compressor* ac) { if (ac) ac->output_size = 0; } +uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { return ac ? ac->clip_count : 0; } diff --git a/lib/audio_compressor.h b/lib/audio_compressor.h index cf13a479..196b975f 100644 --- a/lib/audio_compressor.h +++ b/lib/audio_compressor.h @@ -27,14 +27,16 @@ struct audio_compressor* audio_compressor_create(void); struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source); void audio_compressor_destroy(struct audio_compressor* ac); -void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg); +int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg); +/* Потоковый кадр без lookahead: результат немедленный, память не выделяется; 0 / -1. */ +int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count); void audio_compressor_reset(struct audio_compressor* ac); void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled); int audio_compressor_is_enabled(const struct audio_compressor* ac); -void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count); -void audio_compressor_flush(struct audio_compressor* ac); +int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count); +int audio_compressor_flush(struct audio_compressor* ac); const int16_t* audio_compressor_output(const struct audio_compressor* ac); size_t audio_compressor_output_size(const struct audio_compressor* ac); diff --git a/src/call/call_audio.c b/src/call/call_audio.c index 2ee7d4d9..6a179059 100644 --- a/src/call/call_audio.c +++ b/src/call/call_audio.c @@ -200,8 +200,8 @@ int call_audio_start(uint64_t call_id) { accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15); accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2); accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */ - audio_compressor_configure(ac, &accfg); - audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "call_compressor_enabled", 1)); + if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; } + if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "call_compressor_enabled", 1)); } else { DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: compressor create failed — call without AGC", CALL_AUDIO_ID); } @@ -292,6 +292,7 @@ void call_audio_reset_io(uint64_t call_id, int playback) { pthread_mutex_lock(&g_mtx); if (g_active && g_call_id == call_id) { if (g_aec) speex_aec_reset(g_aec); + if (g_compressor) audio_compressor_reset(g_compressor); if (playback) { call_jitter_reset(g_vj); g_gap_samples = 0; @@ -345,7 +346,7 @@ void call_audio_set_aec_delay_frames(int frames) { /* ── аудио-поток: TX / RX ── */ -int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { +int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) { if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1; uint8_t opus[256]; @@ -364,18 +365,17 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count) tx = cleaned; } - /* компрессор (AGC): push → кодируем прямо из выхода до clear_output. - * lookahead=0 ⇒ выход ровно count сэмплов. */ + int16_t amplified[CALL_AUDIO_FRAME_SAMPLES]; if (g_compressor) { - audio_compressor_push(g_compressor, tx, (size_t)count); - const int16_t* out = audio_compressor_output(g_compressor); - if (out && audio_compressor_output_size(g_compressor) >= (size_t)count) - tx = out; - len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); - audio_compressor_clear_output(g_compressor); - } else { - len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); + if (audio_compressor_process_frame(g_compressor, tx, amplified, count) != 0) { + pthread_mutex_unlock(&g_mtx); + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: compressor frame failed", CALL_AUDIO_ID); + return -1; + } + tx = amplified; } + if (muted) { memset(amplified, 0, sizeof(amplified)); tx = amplified; } + len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); ua = g_inst ? g_inst->ua : NULL; inst = g_inst; if (len <= 0 || !ua || !inst || g_ending || g_tx_pending >= 8) { @@ -405,6 +405,11 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { return 0; } +/* Незамьюченный захват для платформ без программного mute. */ +int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { + return call_audio_feed_pcm_muted(call_id, pcm, count, 0); +} + int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { if (!out || max_samples <= 0) return 0; diff --git a/src/call/call_audio.h b/src/call/call_audio.h index bdc01e05..5bfe5ce7 100644 --- a/src/call/call_audio.h +++ b/src/call/call_audio.h @@ -49,6 +49,8 @@ void call_audio_stop(void); /* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count); +/* Mute после AEC/AGC: фильтр получает настоящий микрофон, Opus — нулевой PCM. */ +int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted); /* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны). * Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */ diff --git a/src/media_async/attachment_send.c b/src/media_async/attachment_send.c index 109f7c5f..a8b93ebd 100644 --- a/src/media_async/attachment_send.c +++ b/src/media_async/attachment_send.c @@ -39,7 +39,7 @@ static void attachment_prepare_work(void* arg) { if (req->info.kind == ATTACHMENT_VOICE) { const int16_t* pcm = req->pcm; size_t count = req->pcm_count; if (req->compressor) { - audio_compressor_flush(req->compressor); + if (audio_compressor_flush(req->compressor) != 0) goto done; pcm = audio_compressor_output(req->compressor); count = audio_compressor_output_size(req->compressor); } req->error = voice_file_encode(pcm, count, 48000, 1, req->preset, req->output, &req->info); diff --git a/src/radio/radio_audio.c b/src/radio/radio_audio.c index 9f9cd292..a45842d6 100644 --- a/src/radio/radio_audio.c +++ b/src/radio/radio_audio.c @@ -380,8 +380,8 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "radio_compressor_max_gain_db", 15); accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2); accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */ - audio_compressor_configure(ac, &accfg); - audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "radio_compressor_enabled", 1)); + if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; } + if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "radio_compressor_enabled", 1)); } if (!ac) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: compressor unavailable", RADIO_AUDIO_ID); @@ -546,10 +546,19 @@ void radio_audio_talk_end(uint64_t group_id) { static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame, struct UASYNC* ua, struct UTUN_INSTANCE* inst) { if (!ua || !inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: encode without instance", RADIO_AUDIO_ID); return; } + int16_t amplified[RADIO_AUDIO_MIX_SAMPLES]; + if (g_compressor) { + if (audio_compressor_process_frame(g_compressor, frame, amplified, + (size_t)RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: compressor frame failed", RADIO_AUDIO_ID); + return; + } + frame = amplified; + } uint8_t opus[RADIO_MAX_OPUS]; int l = opus_codec_encode(g_encoder, frame, RADIO_AUDIO_FRAME_SAMPLES, opus, (int)sizeof(opus)); if (l <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, l); return; } - radio_audio_post_frame(inst, group_id, opus, l); + if (radio_audio_post_frame(inst, group_id, opus, l) == 0) { ++g_tx_frames; g_tx_bytes += (uint32_t)l; } } /* VAD-ветка feed_pcm: ресемпл → Silero → фильтр → старт/стоп/передача (держит g_mtx). */ @@ -626,47 +635,14 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) { if (!pcm || count != frame_total) { pthread_mutex_unlock(&g_mtx); return -1; } if (g_vad_mode) { pthread_mutex_unlock(&g_mtx); return radio_vad_feed(group_id, pcm, count); } - enum { MAX_OUT_FRAMES = 4 }; - uint8_t opus[MAX_OUT_FRAMES][RADIO_MAX_OPUS]; - int opus_len[MAX_OUT_FRAMES]; - int n_frames = 0; - struct UTUN_INSTANCE* inst = NULL; - if (!g_active || group_id != g_group_id || !g_encoder) { pthread_mutex_unlock(&g_mtx); + DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: frame without capture", RADIO_AUDIO_ID); return -1; } - - /* компрессор микрофона: push → слив выходных блоков → encode каждого. - * lookahead=0 ⇒ ровно 1 frame-блок на вход; при disabled — passthrough. */ - const int16_t* src = pcm; - size_t src_count = (size_t)count; - if (g_compressor) { - audio_compressor_push(g_compressor, pcm, (size_t)count); - src = audio_compressor_output(g_compressor); - src_count = audio_compressor_output_size(g_compressor); - } - - size_t off = 0; - while (off + (size_t)frame_total <= src_count && n_frames < MAX_OUT_FRAMES) { - int l = opus_codec_encode(g_encoder, src + off, RADIO_AUDIO_FRAME_SAMPLES, - opus[n_frames], (int)sizeof(opus[n_frames])); - if (l > 0) { opus_len[n_frames] = l; n_frames++; } - off += (size_t)frame_total; - } - if (off != src_count) { - g_tx_tail += (uint32_t)(src_count - off); - DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: feed_pcm dropped %zu tail samples (compressor)", RADIO_AUDIO_ID, src_count - off); - } - if (g_compressor) audio_compressor_clear_output(g_compressor); - g_tx_frames += (uint32_t)n_frames; - for (int i = 0; i < n_frames; i++) g_tx_bytes += (uint32_t)opus_len[i]; - inst = g_inst; - int result = n_frames ? 0 : -1; - for (int i = 0; i < n_frames; i++) - if (radio_audio_post_frame(inst, group_id, opus[i], opus_len[i]) != 0) result = -1; + if (g_vad_manual_ptt) radio_audio_encode_post(group_id, pcm, g_inst ? g_inst->ua : NULL, g_inst); pthread_mutex_unlock(&g_mtx); - return result; + return 0; } /* ── RX (uasync-поток): кодированный кадр → rj_push ── */ diff --git a/tests/test_audio_compressor.c b/tests/test_audio_compressor.c index 24a7807b..722d9d5a 100644 --- a/tests/test_audio_compressor.c +++ b/tests/test_audio_compressor.c @@ -190,6 +190,59 @@ static void test_stress(void) { } } +/* Zero lookahead должен возвращать текущий кадр, а streaming — не выделять память за сеанс. */ +static void test_zero_lookahead(void) { + struct audio_compressor* ac = audio_compressor_create(); + audio_compressor_config_t cfg = {0}; + cfg.lookahead_ms = 0; cfg.target_level = 1.0f; + TEST(audio_compressor_configure(ac, &cfg) == 0, "zero lookahead configured"); + int16_t input[960], output[960]; + for (int frame = 1; frame <= 20; ++frame) { + for (int i = 0; i < 960; ++i) input[i] = (int16_t)(frame * 100); + TEST(audio_compressor_push(ac, input, 960) == 0, "push current frame"); + TEST(audio_compressor_output_size(ac) == 960, "no hidden lookahead delay"); + TEST(memcmp(input, audio_compressor_output(ac), sizeof(input)) == 0, "no repeated earlier frames"); + audio_compressor_clear_output(ac); + } + audio_compressor_reset(ac); + size_t allocations = u_get_allocated_count(); + int good = 1; + for (int frame = 0; frame < 10000; ++frame) { + for (int i = 0; i < 960; ++i) input[i] = (int16_t)(100 + frame); + if (audio_compressor_process_frame(ac, input, output, 960) != 0 || memcmp(input, output, sizeof(input))) good = 0; + } + TEST(good, "long streaming keeps frame identity"); + TEST(u_get_allocated_count() == allocations, "streaming uses preallocated buffers"); + TEST(audio_compressor_output_size(ac) == 0, "streaming does not accumulate recording output"); + audio_compressor_destroy(ac); + TEST(u_get_allocated_count() == 0, "streaming no leaks"); +} + +/* Одинаковое усиление в двух API и баланс стерео при многократном обороте истории. */ +static void test_streaming_matches_recording(void) { + struct audio_compressor* recording = audio_compressor_create(); + struct audio_compressor* streaming = audio_compressor_create(); + audio_compressor_config_t cfg = {0}; + cfg.channels = 2; cfg.max_gain_db = 15; cfg.target_level = 0.25f; + TEST(audio_compressor_configure(recording, &cfg) == 0, "stereo recording configured"); + TEST(audio_compressor_configure(streaming, &cfg) == 0, "stereo streaming configured"); + int16_t input[1920], output[1920]; + int equal = 1; + for (int frame = 0; frame < 500; ++frame) { + for (int i = 0; i < 960; ++i) { + input[2*i] = (int16_t)(sin((double)(frame * 960 + i) * 0.2) * (1000 + frame * 30)); + input[2*i+1] = -input[2*i]; + } + if (audio_compressor_push(recording, input, 1920) != 0 || + audio_compressor_process_frame(streaming, input, output, 1920) != 0 || + audio_compressor_output_size(recording) != 1920 || + memcmp(output, audio_compressor_output(recording), sizeof(output))) equal = 0; + audio_compressor_clear_output(recording); + } + TEST(equal, "streaming and recording share gain algorithm after ring wrap"); + audio_compressor_destroy(recording); audio_compressor_destroy(streaming); +} + int main(void) { debug_set_level(DEBUG_LEVEL_NONE); @@ -199,6 +252,8 @@ int main(void) { test_silence_in_silence_out(); test_reset(); test_stress(); + test_zero_lookahead(); + test_streaming_matches_recording(); fprintf(stderr, "\n%d/%d tests passed\n", tests_passed, tests_run); return tests_passed == tests_run ? 0 : 1; diff --git a/tools/chatgui/src/audiorecorder.cpp b/tools/chatgui/src/audiorecorder.cpp index b8371de5..68115bcf 100644 --- a/tools/chatgui/src/audiorecorder.cpp +++ b/tools/chatgui/src/audiorecorder.cpp @@ -44,6 +44,7 @@ AudioRecorder::AudioRecorder(QObject* parent) audio_compressor_config_t cfg = {0}; cfg.sample_rate = m_sampleRate; cfg.channels = m_channels; + cfg.lookahead_ms = 100; m_compressorConfig = cfg; audio_compressor_configure(m_compressor, &cfg); } diff --git a/tools/chatgui/src/call_audio_engine.cpp b/tools/chatgui/src/call_audio_engine.cpp index 6bb3f634..35b3b0c5 100644 --- a/tools/chatgui/src/call_audio_engine.cpp +++ b/tools/chatgui/src/call_audio_engine.cpp @@ -148,9 +148,9 @@ void CallAudioEngine::dataCallback(const void* in, void* out, unsigned int frame const auto* pcm = static_cast(in); bool muted = m_muted.load(); for (unsigned int i = 0; i < frames; ++i) { - m_captureBuf[m_capturePos++] = muted ? 0 : pcm[i]; + m_captureBuf[m_capturePos++] = pcm[i]; if (m_capturePos == CALL_AUDIO_FRAME_SAMPLES) { - int result = call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES); + int result = call_audio_feed_pcm_muted(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES, muted); if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result); m_capturePos = 0; }