328 lines
12 KiB
C
328 lines
12 KiB
C
#include "voice_recorder.h"
|
||
#include "audio_compressor.h"
|
||
#include "instance_lite.h"
|
||
#include "../../../lib/opus_codec.h"
|
||
#include "../../../lib/debug_config.h"
|
||
#include "../../../lib/mem.h"
|
||
#include "../../../lib/platform_compat.h"
|
||
#include "../../../lib/u_async.h"
|
||
#include "../../../src/chat/chat_core.h"
|
||
#include "../../../src/media_async/attachment_send.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <math.h>
|
||
#include <time.h>
|
||
#include <pthread.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
#include <errno.h>
|
||
|
||
#define FRAME_MS 20
|
||
|
||
struct voice_recorder {
|
||
pthread_mutex_t mtx;
|
||
int active;
|
||
|
||
char channel_id[64];
|
||
int sample_rate;
|
||
int channels;
|
||
int frame_samples;
|
||
|
||
int16_t* pcm_buffer;
|
||
size_t pcm_count;
|
||
size_t pcm_cap;
|
||
|
||
struct audio_compressor* compressor;
|
||
int compressor_enabled;
|
||
|
||
char db_path[512];
|
||
int preset;
|
||
|
||
float peak_level;
|
||
|
||
int64_t start_time_ms;
|
||
};
|
||
|
||
static struct voice_recorder* g_rec = NULL;
|
||
static pthread_mutex_t g_init_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||
|
||
static int64_t now_ms(void) {
|
||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
|
||
}
|
||
|
||
int voice_recorder_init(const char* db_path) {
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) { pthread_mutex_unlock(&g_init_mtx); return 0; }
|
||
|
||
g_rec = u_calloc(1, sizeof(*g_rec));
|
||
if (!g_rec) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_init: OOM");
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return -1;
|
||
}
|
||
pthread_mutex_init(&g_rec->mtx, NULL);
|
||
snprintf(g_rec->db_path, sizeof(g_rec->db_path), "%s", db_path ? db_path : "");
|
||
g_rec->preset = 1;
|
||
g_rec->compressor_enabled = 1;
|
||
|
||
g_rec->compressor = audio_compressor_create();
|
||
if (g_rec->compressor) {
|
||
audio_compressor_config_t cfg = {0};
|
||
cfg.sample_rate = 48000;
|
||
cfg.channels = 1;
|
||
cfg.block_duration_ms = 20;
|
||
cfg.lookback_ms = 200;
|
||
cfg.lookahead_ms = 100;
|
||
cfg.max_gain_db = 15.0f;
|
||
cfg.rise_rate_per_sec = 6.0f;
|
||
cfg.target_level = powf(10.0f, -3.0f / 20.0f); /* -3 dBFS — едино с Android UI */
|
||
audio_compressor_configure(g_rec->compressor, &cfg);
|
||
}
|
||
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_init: db=%s preset=%d compressor=%d",
|
||
g_rec->db_path, g_rec->preset, g_rec->compressor_enabled);
|
||
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO);
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return 0;
|
||
}
|
||
|
||
void voice_recorder_deinit(void) {
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (!g_rec) { pthread_mutex_unlock(&g_init_mtx); return; }
|
||
if (g_rec->active) {
|
||
u_free(g_rec->pcm_buffer);
|
||
g_rec->pcm_buffer = NULL;
|
||
g_rec->pcm_count = 0;
|
||
g_rec->pcm_cap = 0;
|
||
g_rec->active = 0;
|
||
}
|
||
audio_compressor_destroy(g_rec->compressor);
|
||
pthread_mutex_destroy(&g_rec->mtx);
|
||
u_free(g_rec);
|
||
g_rec = NULL;
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_deinit");
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
}
|
||
|
||
int voice_recorder_start(const char* channel_id, int sample_rate, int channels) {
|
||
if (!channel_id || !channel_id[0] || strlen(channel_id) >= 64 || sample_rate != 48000 || channels != 1) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice start: invalid target or capture format rate=%d channels=%d", sample_rate, channels);
|
||
return -1;
|
||
}
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (!g_rec) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_start: not initialized (g_rec=NULL)");
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return -1;
|
||
}
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
if (g_rec->active) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_start: already active");
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return -1;
|
||
}
|
||
|
||
g_rec->sample_rate = sample_rate;
|
||
g_rec->channels = channels;
|
||
g_rec->frame_samples = sample_rate * FRAME_MS / 1000;
|
||
snprintf(g_rec->channel_id, sizeof(g_rec->channel_id), "%s", channel_id ? channel_id : "");
|
||
|
||
g_rec->pcm_count = 0;
|
||
g_rec->active = 1;
|
||
|
||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
g_rec->start_time_ms = ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
|
||
|
||
if (g_rec->compressor && g_rec->compressor_enabled) {
|
||
audio_compressor_reset(g_rec->compressor);
|
||
audio_compressor_set_enabled(g_rec->compressor, 1);
|
||
}
|
||
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_start: ch=%s rate=%d chans=%d frame=%d",
|
||
g_rec->channel_id, g_rec->sample_rate, g_rec->channels, g_rec->frame_samples);
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return 0;
|
||
}
|
||
|
||
int voice_recorder_feed(const int16_t* samples, int count) {
|
||
if (!samples || count <= 0) return -1;
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (!g_rec) { pthread_mutex_unlock(&g_init_mtx); return -1; }
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
if (!g_rec->active) { pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; }
|
||
|
||
/* compute peak level for UI */
|
||
{ float sum_sq = 0.0f; float peak = 0.0f;
|
||
for (int i = 0; i < count; i++) {
|
||
float v = (float)samples[i] / 32768.0f;
|
||
sum_sq += v * v; float av = fabsf(v); if (av > peak) peak = av;
|
||
}
|
||
g_rec->peak_level = (sqrtf(sum_sq / (float)count) * 2.0f + peak) / 2.0f;
|
||
}
|
||
|
||
if (g_rec->compressor && g_rec->compressor_enabled) {
|
||
audio_compressor_push(g_rec->compressor, samples, (size_t)count);
|
||
} else {
|
||
size_t new_cnt = g_rec->pcm_count + (size_t)count;
|
||
if (new_cnt > g_rec->pcm_cap) {
|
||
size_t new_cap = g_rec->pcm_cap ? g_rec->pcm_cap * 2 : (size_t)count * 2;
|
||
while (new_cap < new_cnt) new_cap *= 2;
|
||
int16_t* tmp = u_realloc(g_rec->pcm_buffer, new_cap * sizeof(int16_t));
|
||
if (!tmp) { pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; }
|
||
g_rec->pcm_buffer = tmp;
|
||
g_rec->pcm_cap = new_cap;
|
||
}
|
||
memcpy(g_rec->pcm_buffer + g_rec->pcm_count, samples, (size_t)count * sizeof(int16_t));
|
||
g_rec->pcm_count = new_cnt;
|
||
}
|
||
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return 0;
|
||
}
|
||
|
||
static void voice_cleanup_locked(struct voice_recorder* rec) {
|
||
u_free(rec->pcm_buffer); rec->pcm_buffer = NULL;
|
||
rec->pcm_count = 0; rec->pcm_cap = 0;
|
||
rec->active = 0;
|
||
}
|
||
|
||
/* Захват отдаёт собственные PCM/compressor; кодирование и отправка — в worker/uasync. */
|
||
int voice_recorder_stop(int* out_duration_ms) {
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (!g_rec) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice stop: recorder missing"); pthread_mutex_unlock(&g_init_mtx); return -1; }
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
if (!g_rec->active) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice stop: recorder inactive");
|
||
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1;
|
||
}
|
||
int duration = (int)(now_ms() - g_rec->start_time_ms);
|
||
if (out_duration_ms) *out_duration_ms = duration;
|
||
struct UASYNC* ua = instance_lite_get_uasync();
|
||
struct attachment_send_req* req = u_calloc(1, sizeof(*req));
|
||
struct posted_task* post = u_calloc(1, sizeof(*post));
|
||
if (!uasync_is_running(ua) || !req || !post || duration < 500) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice stop: discarded duration=%d core=%d allocated=%d", duration, uasync_is_running(ua), !!req && !!post);
|
||
u_free(req); u_free(post); voice_cleanup_locked(g_rec);
|
||
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx);
|
||
return duration < 500 ? 0 : -1;
|
||
}
|
||
req->inst = instance_lite_get_instance(); req->info.kind = ATTACHMENT_VOICE; req->preset = g_rec->preset;
|
||
req->is_dm = !strncmp(g_rec->channel_id, "dm:", 3);
|
||
snprintf(req->target, sizeof(req->target), "%s", g_rec->channel_id + (req->is_dm ? 3 : 0));
|
||
if (g_rec->compressor && g_rec->compressor_enabled) {
|
||
struct audio_compressor* replacement = audio_compressor_clone_config(g_rec->compressor);
|
||
if (!replacement) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice stop: replacement compressor allocation failed");
|
||
u_free(req); u_free(post); voice_cleanup_locked(g_rec);
|
||
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1;
|
||
}
|
||
req->compressor = g_rec->compressor; g_rec->compressor = replacement;
|
||
} else {
|
||
req->pcm = g_rec->pcm_buffer; req->pcm_count = g_rec->pcm_count;
|
||
g_rec->pcm_buffer = NULL;
|
||
}
|
||
voice_cleanup_locked(g_rec);
|
||
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx);
|
||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "voice stop: detached target=%s pm=%d duration=%d", req->target, req->is_dm, duration);
|
||
post->callback = attachment_send_trampoline; post->arg = req;
|
||
uasync_post_reserved(ua, post);
|
||
return 0;
|
||
}
|
||
|
||
void voice_recorder_cancel(void) {
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (!g_rec) { pthread_mutex_unlock(&g_init_mtx); return; }
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
if (!g_rec->active) { pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return; }
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_cancel");
|
||
voice_cleanup_locked(g_rec);
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
}
|
||
|
||
int voice_recorder_is_active(void) {
|
||
int active = 0;
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) {
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
active = g_rec->active;
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
}
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return active;
|
||
}
|
||
|
||
void voice_recorder_set_preset(int preset) {
|
||
if (preset < 0) preset = 0; if (preset > 2) preset = 2;
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) {
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
g_rec->preset = preset;
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_set_preset: %d", preset);
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
}
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
}
|
||
|
||
int voice_recorder_get_preset(void) {
|
||
int p = 1;
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) { pthread_mutex_lock(&g_rec->mtx); p = g_rec->preset; pthread_mutex_unlock(&g_rec->mtx); }
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return p;
|
||
}
|
||
|
||
void voice_recorder_set_compressor_enabled(int enabled) {
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) {
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
g_rec->compressor_enabled = enabled ? 1 : 0;
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_set_compressor: %s", g_rec->compressor_enabled ? "on" : "off");
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
}
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
}
|
||
|
||
int voice_recorder_is_compressor_enabled(void) {
|
||
int e = 1;
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) { pthread_mutex_lock(&g_rec->mtx); e = g_rec->compressor_enabled; pthread_mutex_unlock(&g_rec->mtx); }
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return e;
|
||
}
|
||
|
||
void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
|
||
float rise_rate_per_sec, float target_level_db) {
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec && g_rec->compressor) {
|
||
pthread_mutex_lock(&g_rec->mtx);
|
||
audio_compressor_config_t cfg = {0};
|
||
cfg.sample_rate = 48000;
|
||
cfg.channels = 1;
|
||
cfg.block_duration_ms = 20;
|
||
cfg.lookback_ms = lookback_ms;
|
||
cfg.lookahead_ms = lookahead_ms;
|
||
cfg.max_gain_db = (float)max_gain_db;
|
||
cfg.rise_rate_per_sec = rise_rate_per_sec;
|
||
cfg.target_level = powf(10.0f, target_level_db / 20.0f);
|
||
audio_compressor_configure(g_rec->compressor, &cfg);
|
||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder compressor config: gain=%ddB lookback=%d lookahead=%d rise=%.1f target=%.0fdBFS",
|
||
max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_sec, target_level_db);
|
||
pthread_mutex_unlock(&g_rec->mtx);
|
||
}
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
}
|
||
|
||
float voice_recorder_get_peak_level(void) {
|
||
float v = 0.0f;
|
||
pthread_mutex_lock(&g_init_mtx);
|
||
if (g_rec) { pthread_mutex_lock(&g_rec->mtx); v = g_rec->peak_level; pthread_mutex_unlock(&g_rec->mtx); }
|
||
pthread_mutex_unlock(&g_init_mtx);
|
||
return v;
|
||
}
|