|
|
|
@ -7,6 +7,7 @@ |
|
|
|
#include "../../../lib/platform_compat.h" |
|
|
|
#include "../../../lib/platform_compat.h" |
|
|
|
#include "../../../lib/u_async.h" |
|
|
|
#include "../../../lib/u_async.h" |
|
|
|
#include "../../../src/chat/chat_core.h" |
|
|
|
#include "../../../src/chat/chat_core.h" |
|
|
|
|
|
|
|
#include "../../../src/media_async/attachment_send.h" |
|
|
|
#include <stdio.h> |
|
|
|
#include <stdio.h> |
|
|
|
#include <stdlib.h> |
|
|
|
#include <stdlib.h> |
|
|
|
#include <string.h> |
|
|
|
#include <string.h> |
|
|
|
@ -17,12 +18,7 @@ |
|
|
|
#include <unistd.h> |
|
|
|
#include <unistd.h> |
|
|
|
#include <errno.h> |
|
|
|
#include <errno.h> |
|
|
|
|
|
|
|
|
|
|
|
#define OPUS_MAGIC 0x5355504F |
|
|
|
|
|
|
|
#define FRAME_MS 20 |
|
|
|
#define FRAME_MS 20 |
|
|
|
#define MAX_PACKET 4000 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static const int s_bitrates[] = {16000, 32000, 64000}; |
|
|
|
|
|
|
|
static const int s_complexities[] = {2, 5, 10}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct voice_recorder { |
|
|
|
struct voice_recorder { |
|
|
|
pthread_mutex_t mtx; |
|
|
|
pthread_mutex_t mtx; |
|
|
|
@ -206,202 +202,45 @@ static void voice_cleanup_locked(struct voice_recorder* rec) { |
|
|
|
rec->active = 0; |
|
|
|
rec->active = 0; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Захват отдаёт собственные PCM/compressor; кодирование и отправка — в worker/uasync. */ |
|
|
|
int voice_recorder_stop(int* out_duration_ms) { |
|
|
|
int voice_recorder_stop(int* out_duration_ms) { |
|
|
|
pthread_mutex_lock(&g_init_mtx); |
|
|
|
pthread_mutex_lock(&g_init_mtx); |
|
|
|
if (!g_rec) { pthread_mutex_unlock(&g_init_mtx); return -1; } |
|
|
|
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); |
|
|
|
pthread_mutex_lock(&g_rec->mtx); |
|
|
|
if (!g_rec->active) { |
|
|
|
if (!g_rec->active) { |
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: not active"); |
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice stop: recorder inactive"); |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; |
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
|
|
|
|
return -1; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
int duration = (int)(now_ms() - g_rec->start_time_ms); |
|
|
|
int64_t elapsed_ms = now_ms() - g_rec->start_time_ms; |
|
|
|
if (out_duration_ms) *out_duration_ms = duration; |
|
|
|
if (out_duration_ms) *out_duration_ms = (int)elapsed_ms; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct UASYNC* ua = instance_lite_get_uasync(); |
|
|
|
struct UASYNC* ua = instance_lite_get_uasync(); |
|
|
|
if (!ua) { |
|
|
|
struct attachment_send_req* req = u_calloc(1, sizeof(*req)); |
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: no uasync, discarding"); |
|
|
|
struct posted_task* post = u_calloc(1, sizeof(*post)); |
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
if (!uasync_is_running(ua) || !req || !post || duration < 500) { |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice stop: discarded duration=%d core=%d allocated=%d", duration, uasync_is_running(ua), !!req && !!post); |
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
u_free(req); u_free(post); voice_cleanup_locked(g_rec); |
|
|
|
return -1; |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); |
|
|
|
} |
|
|
|
return duration < 500 ? 0 : -1; |
|
|
|
|
|
|
|
|
|
|
|
if (elapsed_ms < 1000) { |
|
|
|
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: too short %lldms, discarding", (long long)elapsed_ms); |
|
|
|
|
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
|
|
|
|
return 0; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
req->inst = instance_lite_get_instance(); req->info.kind = ATTACHMENT_VOICE; req->preset = g_rec->preset; |
|
|
|
int16_t* final_pcm = NULL; |
|
|
|
req->is_dm = !strncmp(g_rec->channel_id, "dm:", 3); |
|
|
|
size_t final_pcm_count = 0; |
|
|
|
snprintf(req->target, sizeof(req->target), "%s", g_rec->channel_id + (req->is_dm ? 3 : 0)); |
|
|
|
|
|
|
|
|
|
|
|
if (g_rec->compressor && g_rec->compressor_enabled) { |
|
|
|
if (g_rec->compressor && g_rec->compressor_enabled) { |
|
|
|
audio_compressor_flush(g_rec->compressor); |
|
|
|
struct audio_compressor* replacement = audio_compressor_clone_config(g_rec->compressor); |
|
|
|
final_pcm = (int16_t*)audio_compressor_output(g_rec->compressor); |
|
|
|
if (!replacement) { |
|
|
|
final_pcm_count = audio_compressor_output_size(g_rec->compressor); |
|
|
|
u_free(req); u_free(post); voice_cleanup_locked(g_rec); |
|
|
|
} else { |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; |
|
|
|
final_pcm = g_rec->pcm_buffer; |
|
|
|
|
|
|
|
final_pcm_count = g_rec->pcm_count; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!final_pcm || final_pcm_count == 0) { |
|
|
|
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: no PCM data"); |
|
|
|
|
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
|
|
|
|
return -1; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* generate file name parts */ |
|
|
|
|
|
|
|
time_t t = time(NULL); |
|
|
|
|
|
|
|
struct tm tm_buf; localtime_r(&t, &tm_buf); |
|
|
|
|
|
|
|
char dt[32], basename[256], suffix[8]; |
|
|
|
|
|
|
|
snprintf(dt, sizeof(dt), "%04d%02d%02d-%02d%02d%02d", |
|
|
|
|
|
|
|
tm_buf.tm_year + 1900, tm_buf.tm_mon + 1, tm_buf.tm_mday, |
|
|
|
|
|
|
|
tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec); |
|
|
|
|
|
|
|
int rnd = rand() & 0xFFFF; |
|
|
|
|
|
|
|
snprintf(basename, sizeof(basename), "voice_%s_%04x", dt, rnd); |
|
|
|
|
|
|
|
snprintf(suffix, sizeof(suffix), "%04x", rnd); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ensure media dir exists */ |
|
|
|
|
|
|
|
char media_dir[1024]; |
|
|
|
|
|
|
|
snprintf(media_dir, sizeof(media_dir), "%s/media/%s", g_rec->db_path, g_rec->channel_id); |
|
|
|
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: db_path=%s ch=%s media_dir=%s", |
|
|
|
|
|
|
|
g_rec->db_path, g_rec->channel_id, media_dir); |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
char tmp[1024]; size_t off = 0; |
|
|
|
|
|
|
|
for (size_t i = 0; media_dir[i] && off < sizeof(tmp) - 1; i++) { |
|
|
|
|
|
|
|
tmp[off++] = media_dir[i]; |
|
|
|
|
|
|
|
if (media_dir[i] == '/' && off > 1) { |
|
|
|
|
|
|
|
tmp[off - 1] = '\0'; |
|
|
|
|
|
|
|
if (mkdir(tmp, 0755) != 0 && errno != EEXIST) |
|
|
|
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: mkdir(%s) errno=%d", tmp, errno); |
|
|
|
|
|
|
|
tmp[off - 1] = '/'; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (mkdir(tmp, 0755) != 0 && errno != EEXIST) |
|
|
|
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: mkdir(%s) errno=%d", tmp, errno); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* encode to Opus and write blocks */ |
|
|
|
|
|
|
|
opus_codec_encoder_t* enc = opus_codec_encoder_create(g_rec->sample_rate, g_rec->channels); |
|
|
|
|
|
|
|
if (!enc) { |
|
|
|
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: encoder create failed"); |
|
|
|
|
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
|
|
|
|
return -1; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
opus_codec_encoder_bitrate_set(enc, s_bitrates[g_rec->preset]); |
|
|
|
|
|
|
|
opus_codec_encoder_complexity_set(enc, s_complexities[g_rec->preset]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
char temp_path[1280]; |
|
|
|
|
|
|
|
snprintf(temp_path, sizeof(temp_path), "%s/%s_%s_%s.opus", media_dir, dt, basename, suffix); |
|
|
|
|
|
|
|
FILE* of = fopen(temp_path, "wb"); |
|
|
|
|
|
|
|
if (!of) { |
|
|
|
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: cannot create %s (errno=%d)", temp_path, errno); |
|
|
|
|
|
|
|
opus_codec_encoder_destroy(enc); |
|
|
|
|
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
|
|
|
|
return -1; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Opus header */ |
|
|
|
|
|
|
|
uint32_t magic = OPUS_MAGIC; |
|
|
|
|
|
|
|
uint32_t sr = (uint32_t)g_rec->sample_rate; |
|
|
|
|
|
|
|
uint16_t ch = (uint16_t)g_rec->channels; |
|
|
|
|
|
|
|
uint16_t fs = (uint16_t)g_rec->frame_samples; |
|
|
|
|
|
|
|
fwrite(&magic, 4, 1, of); |
|
|
|
|
|
|
|
fwrite(&sr, 4, 1, of); |
|
|
|
|
|
|
|
fwrite(&ch, 2, 1, of); |
|
|
|
|
|
|
|
fwrite(&fs, 2, 1, of); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t packet[MAX_PACKET]; |
|
|
|
|
|
|
|
int frame_count = 0; |
|
|
|
|
|
|
|
size_t pcm_off = 0; |
|
|
|
|
|
|
|
size_t total_samples = g_rec->channels == 1 ? final_pcm_count : final_pcm_count / g_rec->channels; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (pcm_off + (size_t)g_rec->frame_samples * g_rec->channels <= final_pcm_count) { |
|
|
|
|
|
|
|
int len = opus_codec_encode(enc, final_pcm + pcm_off, g_rec->frame_samples, packet, MAX_PACKET); |
|
|
|
|
|
|
|
pcm_off += (size_t)g_rec->frame_samples * g_rec->channels; |
|
|
|
|
|
|
|
if (len > 0) { |
|
|
|
|
|
|
|
uint16_t plen = (uint16_t)len; |
|
|
|
|
|
|
|
fwrite(&plen, 2, 1, of); |
|
|
|
|
|
|
|
fwrite(packet, 1, (size_t)len, of); |
|
|
|
|
|
|
|
frame_count++; |
|
|
|
|
|
|
|
} else if (len < 0) { |
|
|
|
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: encode error frame %d: %d", frame_count, len); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
uint16_t zero = 0; |
|
|
|
|
|
|
|
fwrite(&zero, 2, 1, of); |
|
|
|
|
|
|
|
fclose(of); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
opus_codec_encoder_destroy(enc); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
float duration_sec = (float)frame_count * FRAME_MS / 1000.0f; |
|
|
|
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: wrote %d frames (%.1fs) dur=%lldms compressor=%d path=%s", |
|
|
|
|
|
|
|
frame_count, duration_sec, (long long)elapsed_ms, g_rec->compressor_enabled, temp_path); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (frame_count <= 0) { |
|
|
|
|
|
|
|
unlink(temp_path); |
|
|
|
|
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
|
|
|
|
pthread_mutex_unlock(&g_init_mtx); |
|
|
|
|
|
|
|
return -1; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* file is already at temp_path in media dir – no splitting needed */ |
|
|
|
|
|
|
|
/* media_index_register_async will handle block size calculation, signing, and DB */ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* build compact waveform: 100 bytes base64 (A-Za-z0-9+/), log scale */ |
|
|
|
|
|
|
|
static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
|
|
|
|
|
|
|
char wf_data[256]; |
|
|
|
|
|
|
|
snprintf(wf_data, sizeof(wf_data), "wf="); |
|
|
|
|
|
|
|
int wf_off = 3; |
|
|
|
|
|
|
|
{ int wfN = g_rec->waveform_count > 0 ? g_rec->waveform_count : 1; |
|
|
|
|
|
|
|
for (int i = 0; i < 100; i++) { |
|
|
|
|
|
|
|
int s = i * wfN / 100, e = (i + 1) * wfN / 100; |
|
|
|
|
|
|
|
if (s >= wfN) s = wfN - 1; if (e <= s) e = s + 1; if (e > wfN) e = wfN; |
|
|
|
|
|
|
|
float sum = 0; int cnt = 0; |
|
|
|
|
|
|
|
for (int j = s; j < e; j++) { sum += g_rec->waveform_levels[j]; cnt++; } |
|
|
|
|
|
|
|
float v = cnt > 0 ? sum / cnt : 0; |
|
|
|
|
|
|
|
float db = v > 0.00001f ? 20.0f * log10f(v) : -100.0f; |
|
|
|
|
|
|
|
int idx = (int)((db + 60.0f) / 60.0f * 64.0f); |
|
|
|
|
|
|
|
if (idx < 0) idx = 0; if (idx > 63) idx = 63; |
|
|
|
|
|
|
|
wf_data[wf_off++] = b64_table[idx]; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
wf_off += snprintf(wf_data + wf_off, sizeof(wf_data) - wf_off, ";dur=%lld;", (long long)elapsed_ms); |
|
|
|
|
|
|
|
size_t total_data_len = (size_t)(wf_off); |
|
|
|
|
|
|
|
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + total_data_len + 1); |
|
|
|
|
|
|
|
if (!req) { voice_cleanup_locked(g_rec); pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
req->inst = instance_lite_get_instance(); |
|
|
|
|
|
|
|
snprintf(req->channel_id, sizeof(req->channel_id), "%s", g_rec->channel_id); |
|
|
|
|
|
|
|
snprintf(req->content_type, sizeof(req->content_type), "audio/opus"); |
|
|
|
|
|
|
|
snprintf(req->media_src, sizeof(req->media_src), "%s", temp_path); |
|
|
|
|
|
|
|
snprintf(req->media_dest, sizeof(req->media_dest), "%s", temp_path); |
|
|
|
|
|
|
|
req->media_copy = 0; |
|
|
|
|
|
|
|
req->data = (uint8_t*)(req + 1); |
|
|
|
|
|
|
|
req->data_len = (uint32_t)total_data_len; |
|
|
|
|
|
|
|
memcpy(req->data, wf_data, total_data_len); |
|
|
|
|
|
|
|
req->timestamp = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: posting media ch=%s path=%s", |
|
|
|
|
|
|
|
req->channel_id, temp_path); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uasync_post(ua, chat_core_submit_trampoline, req); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
voice_cleanup_locked(g_rec); |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); |
|
|
|
pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_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; |
|
|
|
return 0; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|