Browse Source

Исправь потоковый AGC и применяй mute после AEC

master
evgeny 2 weeks ago
parent
commit
40b974b29c
  1. 422
      lib/audio_compressor.c
  2. 8
      lib/audio_compressor.h
  3. 31
      src/call/call_audio.c
  4. 2
      src/call/call_audio.h
  5. 2
      src/media_async/attachment_send.c
  6. 54
      src/radio/radio_audio.c
  7. 55
      tests/test_audio_compressor.c
  8. 1
      tools/chatgui/src/audiorecorder.cpp
  9. 4
      tools/chatgui/src/call_audio_engine.cpp

422
lib/audio_compressor.c

@ -3,244 +3,144 @@
#include "mem.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define BLOCK_LEVELS_CHUNK 64
#define PENDING_CHUNK 32
#include <stdint.h>
/* История и задержанные блоки ограничены окном настройки. Только запись накапливает 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; }

8
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);

31
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;

2
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 — андерфлоу/конец тона). */

2
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);

54
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 ── */

55
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;

1
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);
}

4
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<const int16_t*>(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;
}

Loading…
Cancel
Save